Spaces:
Running
Running
Update index.html
Browse files- index.html +791 -0
index.html
CHANGED
@@ -1,2 +1,793 @@
|
|
1 |
<div id="tool-box" class="overlay"></div>
|
2 |
<div id="inspector" class="overlay sidebar right scroll monospaced"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
<div id="tool-box" class="overlay"></div>
|
2 |
<div id="inspector" class="overlay sidebar right scroll monospaced"></div>
|
3 |
+
<script>import * as polyDecomp from "https://cdn.skypack.dev/[email protected]";
|
4 |
+
|
5 |
+
const {
|
6 |
+
Bodies,
|
7 |
+
Body,
|
8 |
+
Bounds,
|
9 |
+
Common,
|
10 |
+
Composite,
|
11 |
+
Constraint,
|
12 |
+
Engine,
|
13 |
+
Events,
|
14 |
+
Mouse,
|
15 |
+
MouseConstraint,
|
16 |
+
Query,
|
17 |
+
Render,
|
18 |
+
Runner,
|
19 |
+
Vector,
|
20 |
+
Vertices,
|
21 |
+
} = Matter;
|
22 |
+
|
23 |
+
Common.setDecomp(polyDecomp);
|
24 |
+
|
25 |
+
const engine = Engine.create({
|
26 |
+
gravity: {
|
27 |
+
scale: 0.001,
|
28 |
+
x: 0,
|
29 |
+
y: 1,
|
30 |
+
},
|
31 |
+
});
|
32 |
+
|
33 |
+
const rect = document.documentElement.getBoundingClientRect();
|
34 |
+
const render = Render.create({
|
35 |
+
element: document.body,
|
36 |
+
engine: engine,
|
37 |
+
options: {
|
38 |
+
width: rect.height,
|
39 |
+
height: rect.height,
|
40 |
+
pixelRatio: 1,
|
41 |
+
hasBounds: true,
|
42 |
+
showConvexHulls: true,
|
43 |
+
showAxes: true,
|
44 |
+
},
|
45 |
+
});
|
46 |
+
|
47 |
+
const boxA = Bodies.rectangle(100, -50, 80, 80);
|
48 |
+
const boxB = Bodies.rectangle(400 * 0.60, -50, 80, 80);
|
49 |
+
const ground = Bodies.rectangle(0, 0, 1000, 50, { isStatic: true });
|
50 |
+
|
51 |
+
const mouse = Mouse.create(render.canvas);
|
52 |
+
const mouseConstraint = MouseConstraint.create(engine, {
|
53 |
+
mouse,
|
54 |
+
constraint: {
|
55 |
+
angularStiffness: 0.8,
|
56 |
+
render: {
|
57 |
+
visible: true,
|
58 |
+
},
|
59 |
+
},
|
60 |
+
collisionFilter: {
|
61 |
+
category: 0x0000,
|
62 |
+
mask: 0x00000000,
|
63 |
+
group: 0,
|
64 |
+
},
|
65 |
+
/*{ // Default
|
66 |
+
category: 0x0001,
|
67 |
+
mask: 0xFFFFFFFF,
|
68 |
+
group: 0,
|
69 |
+
},*/
|
70 |
+
});
|
71 |
+
render.mouse = mouse;
|
72 |
+
|
73 |
+
Composite.add(engine.world, [boxA, boxB, ground, mouseConstraint]);
|
74 |
+
|
75 |
+
const runner = Runner.create();
|
76 |
+
Runner.run(runner, engine);
|
77 |
+
Render.run(render);
|
78 |
+
|
79 |
+
// Setup
|
80 |
+
const KEYS_DOWN = new Set();
|
81 |
+
setInterval(() => console.log(KEYS_DOWN), 1000);
|
82 |
+
|
83 |
+
const SHAPE_FACTORIES = [
|
84 |
+
{ key: 'rectangle',
|
85 |
+
icon: 'square',
|
86 |
+
numPoints: 2,
|
87 |
+
factory: ([pointStart, pointEnd], isAlternateMode) => {
|
88 |
+
if (!pointStart || !pointEnd) return;
|
89 |
+
|
90 |
+
|
91 |
+
const min = Vector.create(Math.min(pointStart.position.x, pointEnd.position.x), Math.min(pointStart.position.y, pointEnd.position.y));
|
92 |
+
const max = Vector.create(Math.max(pointStart.position.x, pointEnd.position.x), Math.max(pointStart.position.y, pointEnd.position.y));
|
93 |
+
|
94 |
+
let width = max.x - min.x
|
95 |
+
let height = max.y - min.y
|
96 |
+
let position = Vector.div(Vector.add(pointStart.position, pointEnd.position), 2);
|
97 |
+
if (isAlternateMode) {
|
98 |
+
width *= 2;
|
99 |
+
height *= 2;
|
100 |
+
position = pointStart.position;
|
101 |
+
}
|
102 |
+
|
103 |
+
if (width > 0 && height > 0) {
|
104 |
+
return Bodies.rectangle(position.x, position.y, width, height, {});
|
105 |
+
}
|
106 |
+
return null;
|
107 |
+
},
|
108 |
+
},
|
109 |
+
{ key: 'circle',
|
110 |
+
icon: 'circle',
|
111 |
+
numPoints: 2,
|
112 |
+
factory: ([pointStart, pointEnd], isAlternateMode) => {
|
113 |
+
if (!pointStart || !pointEnd) return;
|
114 |
+
|
115 |
+
let radius = Vector.magnitude(Vector.sub(pointStart.position, pointEnd.position));
|
116 |
+
if (isAlternateMode) radius /= 2;
|
117 |
+
|
118 |
+
if (radius > 0) {
|
119 |
+
let position = pointStart.position;
|
120 |
+
if (isAlternateMode) position = Vector.div(Vector.add(pointStart.position, pointEnd.position), 2);
|
121 |
+
return Bodies.circle(position.x, position.y, radius, {}, 50);
|
122 |
+
}
|
123 |
+
return null;
|
124 |
+
},
|
125 |
+
},
|
126 |
+
{ key: 'capsule',
|
127 |
+
icon: 'capsules',
|
128 |
+
numPoints: 3,
|
129 |
+
factory: ([pointStart, pointEnd, pointTangent], isAlternateMode) => {
|
130 |
+
if (!pointStart || !pointEnd || !pointTangent) return;
|
131 |
+
|
132 |
+
// Note: There is no way any of this is efficient
|
133 |
+
const midpoint = Vector.div(Vector.add(pointStart.position, pointEnd.position), 2);
|
134 |
+
const delta = Vector.sub(pointEnd.position, pointStart.position);
|
135 |
+
const deltaMagSq = Vector.magnitudeSquared(delta);
|
136 |
+
const deltaMag = Math.sqrt(deltaMagSq);
|
137 |
+
const deltaDir = Vector.div(delta, deltaMag);
|
138 |
+
|
139 |
+
let radius = 0;
|
140 |
+
if (deltaMagSq <= (Number.EPSILON * Number.EPSILON)) {
|
141 |
+
radius = Vector.magnitude(Vector.sub(pointStart.position, pointTangent.position));
|
142 |
+
} else {
|
143 |
+
let proj, scale;
|
144 |
+
if (true) {
|
145 |
+
proj = Vector.mult(deltaDir, Vector.dot(Vector.sub(pointTangent.position, pointStart.position), deltaDir));
|
146 |
+
scale = Vector.magnitudeSquared(proj) / deltaMagSq * Math.sign(Vector.dot(deltaDir, Vector.normalise(Vector.sub(pointTangent.position, pointStart.position))));
|
147 |
+
}
|
148 |
+
if (scale < 0)
|
149 |
+
radius = Vector.magnitude(Vector.sub(pointStart.position, pointTangent.position));
|
150 |
+
else if (scale > 1)
|
151 |
+
radius = Vector.magnitude(Vector.sub(pointEnd.position, pointTangent.position));
|
152 |
+
else
|
153 |
+
radius = Math.abs((pointEnd.position.x - pointStart.position.x) * (pointStart.position.y - pointTangent.position.y) - (pointStart.position.x - pointTangent.position.x) * (pointEnd.position.y - pointStart.position.y)) / Vector.magnitude(Vector.sub(pointEnd.position, pointStart.position));
|
154 |
+
}
|
155 |
+
|
156 |
+
const width = !isAlternateMode ? deltaMag + radius * 2 : deltaMag;
|
157 |
+
const height = radius * 2;
|
158 |
+
|
159 |
+
if (width > 0 && height > 0 && radius > 0) {
|
160 |
+
const body = Bodies.rectangle(midpoint.x, midpoint.y, width, height, { chamfer: { radius } });
|
161 |
+
Body.setAngle(body, Vector.angle(pointStart.position, pointEnd.position));
|
162 |
+
return body;
|
163 |
+
}
|
164 |
+
return null;
|
165 |
+
},
|
166 |
+
},
|
167 |
+
{ key: 'polygon',
|
168 |
+
icon: 'draw-polygon',
|
169 |
+
numPoints: 0,
|
170 |
+
factory: (points, isAlternateMode) => {
|
171 |
+
if (points.length < 3) return;
|
172 |
+
|
173 |
+
const positions = points.map(point => point.position);
|
174 |
+
const body = Bodies.fromVertices(0, 0, positions, {}, true);
|
175 |
+
|
176 |
+
const positionBounds = Bounds.create(positions);
|
177 |
+
const offset = Vector.create(positionBounds.min.x - body.bounds.min.x, positionBounds.min.y - body.bounds.min.y);
|
178 |
+
Body.setPosition(body, Vector.add(body.position, offset));
|
179 |
+
console.log(offset);
|
180 |
+
|
181 |
+
return body;
|
182 |
+
},
|
183 |
+
},
|
184 |
+
{ key: 'clone',
|
185 |
+
icon: 'clone',
|
186 |
+
numPoints: 2,
|
187 |
+
doUnfreeze: false,
|
188 |
+
factory: ([original, placement]) => {
|
189 |
+
if (!original || !original.body || !placement) return;
|
190 |
+
const body = Object.assign(clone(original.body), {
|
191 |
+
id: Common.nextId(),
|
192 |
+
collisionFilter: { category: 0x0000,
|
193 |
+
mask: 0x00000000,
|
194 |
+
group: 0,
|
195 |
+
},
|
196 |
+
});
|
197 |
+
Body.setAngle(body, original.angle);
|
198 |
+
Body.setPosition(body, Vector.sub(placement.position, original.offset));
|
199 |
+
return body;
|
200 |
+
},
|
201 |
+
onFinalize: (body, [original, placement]) => {
|
202 |
+
body.collisionFilter = clone(original.body.collisionFilter);
|
203 |
+
Body.setStatic(body, original.body.isStatic);
|
204 |
+
return true; // Override default finalization
|
205 |
+
},
|
206 |
+
},
|
207 |
+
];
|
208 |
+
const SHAPE_TOOL_BUILDER = ({ key, icon, iconStyle, numPoints, doUnfreeze, factory, onFinalize }) => {
|
209 |
+
const createPoint = (position, bodies, ignoreBodies) => {
|
210 |
+
position = Vector.clone(position);
|
211 |
+
bodies = bodies || Composite.allBodies(engine.world);
|
212 |
+
ignoreBodies = !Array.isArray(ignoreBodies) ? [ignoreBodies] : ignoreBodies;
|
213 |
+
|
214 |
+
if (KEYS_DOWN.has('ControlLeft')) {
|
215 |
+
position.x = Math.floor(position.x / 10 + 0.5) * 10;
|
216 |
+
position.y = Math.floor(position.y / 10 + 0.5) * 10;
|
217 |
+
}
|
218 |
+
|
219 |
+
const queryResult = Query.point(bodies, position);
|
220 |
+
const body = _.first(ignoreBodies ? queryResult.filter(body => !ignoreBodies.includes(body)) : queryResult);
|
221 |
+
if (body && KEYS_DOWN.has('AltLeft')) {
|
222 |
+
position = Vector.clone(body.position);
|
223 |
+
}
|
224 |
+
|
225 |
+
return {
|
226 |
+
body,
|
227 |
+
position,
|
228 |
+
angle: body ? body.angle : 0,
|
229 |
+
offset: body? Vector.sub(position, body.position) : Vector.create(0, 0),
|
230 |
+
};
|
231 |
+
};
|
232 |
+
|
233 |
+
const updateBody = (state) => {
|
234 |
+
if (!state.isActive) return;
|
235 |
+
|
236 |
+
if (state.body) {
|
237 |
+
Composite.remove(engine.world, state.body);
|
238 |
+
state.body = null;
|
239 |
+
}
|
240 |
+
|
241 |
+
try {
|
242 |
+
state.body = factory(state.points, state.isAlternateMode);
|
243 |
+
if (state.body) {
|
244 |
+
Body.setStatic(state.body, true);
|
245 |
+
Composite.add(engine.world, [state.body]);
|
246 |
+
}
|
247 |
+
} catch (err) {
|
248 |
+
// TODO: Include points
|
249 |
+
console.error(`Unable to generate body from '${key}' factory: ${err.message}\n${err.stack || err}`);
|
250 |
+
}
|
251 |
+
};
|
252 |
+
|
253 |
+
const finalizeBody = (state) => {
|
254 |
+
if (!state.isActive) return;
|
255 |
+
|
256 |
+
state.points = state.points.slice(0, state.pointsCommitted);
|
257 |
+
updateBody(state);
|
258 |
+
|
259 |
+
if (state.body) {
|
260 |
+
if (!onFinalize || !onFinalize(state.body, state.points)) {
|
261 |
+
if (!KEYS_DOWN.has('ShiftLeft')) {
|
262 |
+
Body.setStatic(state.body, false);
|
263 |
+
} else {
|
264 |
+
Body.setStatic(state.body, true);
|
265 |
+
}
|
266 |
+
}
|
267 |
+
}
|
268 |
+
};
|
269 |
+
|
270 |
+
return {
|
271 |
+
key,
|
272 |
+
innerHTML: `
|
273 |
+
<i class="fa-solid fa-${icon} fa-xl" style="${iconStyle || ''}"></i>
|
274 |
+
<i class="fa-solid fa-plus"></i>
|
275 |
+
`,
|
276 |
+
onEvent: {
|
277 |
+
mousedown: (event, state) => {
|
278 |
+
const bodies = Composite.allBodies(engine.world);
|
279 |
+
|
280 |
+
if (!state.isActive) {
|
281 |
+
state.isActive = true;
|
282 |
+
state.isAlternateMode = event.mouse.button === 2;
|
283 |
+
state.points = [createPoint(event.mouse.position, bodies, state.body)];
|
284 |
+
state.pointsCommitted = 1;
|
285 |
+
state.body = null;
|
286 |
+
} else if (event.mouse.button === 0) {
|
287 |
+
state.points[state.pointsCommitted] = createPoint(event.mouse.position, bodies, state.body);
|
288 |
+
state.pointsCommitted++;
|
289 |
+
}
|
290 |
+
|
291 |
+
updateBody(state);
|
292 |
+
|
293 |
+
if ((numPoints > 0 && state.points.length >= numPoints) || (numPoints <= 0 && event.mouse.button === 2)) {
|
294 |
+
finalizeBody(state);
|
295 |
+
state.isActive = false;
|
296 |
+
state.body = null;
|
297 |
+
}
|
298 |
+
},
|
299 |
+
mousemove: (event, state) => {
|
300 |
+
if (!state.isActive) return;
|
301 |
+
state.points[state.pointsCommitted] = createPoint(event.mouse.position, null, state.body);
|
302 |
+
updateBody(state);
|
303 |
+
},
|
304 |
+
},
|
305 |
+
state: {
|
306 |
+
isActive: false,
|
307 |
+
points: null,
|
308 |
+
pointsCommitted: 0,
|
309 |
+
body: null,
|
310 |
+
},
|
311 |
+
};
|
312 |
+
};
|
313 |
+
|
314 |
+
const CONSTRAINT_FACTORIES = [
|
315 |
+
{ key: 'pin',
|
316 |
+
icon: 'thumbtack',
|
317 |
+
numPoints: 1,
|
318 |
+
factory: ([point]) => {
|
319 |
+
if (!point || !point.body) return;
|
320 |
+
return Constraint.create({
|
321 |
+
bodyB: point.body,
|
322 |
+
pointA: point.position,
|
323 |
+
pointB: point.offset,
|
324 |
+
length: 0,
|
325 |
+
});
|
326 |
+
},
|
327 |
+
},
|
328 |
+
...[1.00, 0.10, 0.01, 0.001].map(stiffness => ({
|
329 |
+
key: `connect-${stiffness.toPrecision(2)}`,
|
330 |
+
icon: stiffness !== 1.00 ? 'share-nodes' : 'circle-nodes',
|
331 |
+
iconStyle: stiffness !== 1.00 ? `color:rgba(${150 + 100 * (stiffness / 0.15)}, 255, ${150 + 100 * (1-(stiffness / 0.15))}, 1);` : '',
|
332 |
+
numPoints: 2,
|
333 |
+
factory: ([pointStart, pointEnd]) => {
|
334 |
+
if (!pointStart || !pointEnd) return;
|
335 |
+
if (!pointStart.body && !pointEnd.body) return;
|
336 |
+
return Constraint.create({
|
337 |
+
bodyA: pointStart.body,
|
338 |
+
bodyB: pointEnd.body,
|
339 |
+
pointA: pointStart.body ? pointStart.offset : pointStart.position,
|
340 |
+
pointB: pointEnd.body ? pointEnd.offset : pointEnd.position,
|
341 |
+
stiffness,
|
342 |
+
});
|
343 |
+
},
|
344 |
+
})),
|
345 |
+
{ key: 'union',
|
346 |
+
icon: 'object-group',
|
347 |
+
numPoints: 0,
|
348 |
+
factory: (points) => {
|
349 |
+
if (!points || points.length < 2) return;
|
350 |
+
|
351 |
+
const bodies = points.map(point => point.body).filter(Boolean);
|
352 |
+
for (let body of bodies) {
|
353 |
+
// Note: Deep search? Would be slow, right?
|
354 |
+
Composite.remove(engine.world, body);
|
355 |
+
}
|
356 |
+
|
357 |
+
// TODO: Clone only specific body properties needed
|
358 |
+
const parts = bodies.map(body => body.parts.length > 1 ? body.parts.filter(p => p !== body) : body.parts).flat();
|
359 |
+
const clones = parts.map(part => Object.assign(clone(part), { id: Common.nextId() }));
|
360 |
+
return Body.create({
|
361 |
+
parts: clones,
|
362 |
+
isStatic: clones.some(body => body.isStatic),
|
363 |
+
});
|
364 |
+
},
|
365 |
+
},
|
366 |
+
];
|
367 |
+
const CONSTRAINT_TOOL_BUILDER = ({ key, icon, iconStyle, numPoints, factory }) => {
|
368 |
+
const createPoint = (position, bodies, ignoreBodies) => {
|
369 |
+
position = Vector.clone(position);
|
370 |
+
bodies = bodies || Composite.allBodies(engine.world);
|
371 |
+
ignoreBodies = !Array.isArray(ignoreBodies) ? [ignoreBodies] : ignoreBodies;
|
372 |
+
|
373 |
+
if (KEYS_DOWN.has('ControlLeft')) {
|
374 |
+
position.x = Math.floor(position.x / 10 + 0.5) * 10;
|
375 |
+
position.y = Math.floor(position.y / 10 + 0.5) * 10;
|
376 |
+
}
|
377 |
+
|
378 |
+
const queryResult = Query.point(bodies, position);
|
379 |
+
const body = _.first(ignoreBodies ? queryResult.filter(body => !ignoreBodies.includes(body)) : queryResult);
|
380 |
+
if (body && KEYS_DOWN.has('AltLeft')) {
|
381 |
+
position = Vector.clone(body.position);
|
382 |
+
}
|
383 |
+
|
384 |
+
return {
|
385 |
+
body,
|
386 |
+
position,
|
387 |
+
angle: body ? body.angle : 0,
|
388 |
+
offset: body? Vector.sub(position, body.position) : Vector.create(0, 0),
|
389 |
+
};
|
390 |
+
};
|
391 |
+
|
392 |
+
const finalizeConstraint = (state) => {
|
393 |
+
if (!state.isActive) return;
|
394 |
+
|
395 |
+
try {
|
396 |
+
const result = factory(state.points);
|
397 |
+
if (result) {
|
398 |
+
Composite.add(engine.world, [result]);
|
399 |
+
}
|
400 |
+
} catch (err) {
|
401 |
+
// TODO: Include points
|
402 |
+
console.error(`Unable to generate constraint from '${key}' factory: ${err.message}\n${err.stack || err}`);
|
403 |
+
}
|
404 |
+
};
|
405 |
+
|
406 |
+
return {
|
407 |
+
key,
|
408 |
+
innerHTML: `
|
409 |
+
<i class="fa-solid fa-${icon} fa-xl" style="${iconStyle || ''}"></i>
|
410 |
+
<i class="fa-solid fa-link"></i>
|
411 |
+
`,
|
412 |
+
onEvent: {
|
413 |
+
mousedown: (event, state) => {
|
414 |
+
const bodies = Composite.allBodies(engine.world);
|
415 |
+
|
416 |
+
if (!state.isActive) {
|
417 |
+
state.isActive = true;
|
418 |
+
state.points = [createPoint(event.mouse.position, bodies)];
|
419 |
+
state.pointsCommitted = 1;
|
420 |
+
} else {
|
421 |
+
state.points[state.pointsCommitted] = createPoint(event.mouse.position, bodies);
|
422 |
+
state.pointsCommitted++;
|
423 |
+
}
|
424 |
+
|
425 |
+
if ((numPoints > 0 && state.points.length >= numPoints) || (numPoints <= 0 && event.mouse.button === 2)) {
|
426 |
+
finalizeConstraint(state);
|
427 |
+
state.isActive = false;
|
428 |
+
}
|
429 |
+
},
|
430 |
+
},
|
431 |
+
state: {
|
432 |
+
isActive: false,
|
433 |
+
points: null,
|
434 |
+
pointsCommitted: 0,
|
435 |
+
},
|
436 |
+
};
|
437 |
+
};
|
438 |
+
|
439 |
+
let INSPECTED_OBJECT = null;
|
440 |
+
let INSPECTED_OBJECT_PROPERTIES = null;
|
441 |
+
let INSPECTED_OBJECT_LAST_UPDATE = 0;
|
442 |
+
|
443 |
+
let ACTIVE_TOOL = null;
|
444 |
+
const TOOL_BOX = [
|
445 |
+
{ key: 'general',
|
446 |
+
tools: [
|
447 |
+
{ key: 'pointer',
|
448 |
+
innerHTML: `
|
449 |
+
<i class="fa-solid fa-arrow-pointer"></i>
|
450 |
+
`,
|
451 |
+
onDeactivate: (state) => {
|
452 |
+
if (state.isDown) {
|
453 |
+
state.isDown = false;
|
454 |
+
state.isExpired = true;
|
455 |
+
}
|
456 |
+
|
457 |
+
if (state.timeout) {
|
458 |
+
clearTimeout(state.timeout);
|
459 |
+
state.timeout = null;
|
460 |
+
}
|
461 |
+
|
462 |
+
if (state.contextMenu) {
|
463 |
+
state.contextMenu.hide();
|
464 |
+
state.contextMenu = null;
|
465 |
+
}
|
466 |
+
},
|
467 |
+
onEvent: {
|
468 |
+
mousedown: (event, state) => {
|
469 |
+
state.isDown = true;
|
470 |
+
state.isExpired = false;
|
471 |
+
|
472 |
+
state.position = Vector.clone(event.mouse.position);
|
473 |
+
state.bodies = Query.point(Composite.allBodies(engine.world), state.position);
|
474 |
+
|
475 |
+
if (state.timeout) {
|
476 |
+
clearTimeout(state.timeout);
|
477 |
+
state.timeout = null;
|
478 |
+
}
|
479 |
+
|
480 |
+
if (state.contextMenu) {
|
481 |
+
state.contextMenu.hide();
|
482 |
+
state.contextMenu = null;
|
483 |
+
}
|
484 |
+
|
485 |
+
state.timeout = setTimeout(() => {
|
486 |
+
state.isExpired = true;
|
487 |
+
|
488 |
+
const menuItems = state.bodies.map(body => ({
|
489 |
+
text: body.label,
|
490 |
+
hotkey: body.id,
|
491 |
+
onclick: () => inspectObject(body),
|
492 |
+
}));
|
493 |
+
|
494 |
+
const contextMenu = state.contextMenu = new ContextMenu(document.body, menuItems);
|
495 |
+
contextMenu.show(event.mouse.absolute.x, event.mouse.absolute.y);
|
496 |
+
}, 1500);
|
497 |
+
},
|
498 |
+
mouseup: (event, state) => {
|
499 |
+
if (!state.isDown || state.isExpired) return;
|
500 |
+
|
501 |
+
state.isDown = false;
|
502 |
+
|
503 |
+
if (state.timeout) {
|
504 |
+
clearTimeout(state.timeout);
|
505 |
+
state.timeout = null;
|
506 |
+
}
|
507 |
+
|
508 |
+
inspectObject(_.first(state.bodies));
|
509 |
+
},
|
510 |
+
},
|
511 |
+
state: {
|
512 |
+
isDown: false,
|
513 |
+
isExpired: false,
|
514 |
+
timeout: null,
|
515 |
+
contextMenu: null,
|
516 |
+
position: null,
|
517 |
+
bodies: null,
|
518 |
+
},
|
519 |
+
},
|
520 |
+
{ key: 'move',
|
521 |
+
innerHTML: `
|
522 |
+
<i class="fa-solid fa-arrows-up-down-left-right"></i>
|
523 |
+
`,
|
524 |
+
onEvent: {
|
525 |
+
mousedown: (event, state) => {
|
526 |
+
if (state.isDown) return;
|
527 |
+
state.isDown = true;
|
528 |
+
state.position = Vector.clone(event.mouse.position);
|
529 |
+
state.body = _.first(Query.point(Composite.allBodies(engine.world), state.position));
|
530 |
+
if (state.body) {
|
531 |
+
state.wasStatic = state.body.isStatic;
|
532 |
+
Body.setStatic(state.body, true);
|
533 |
+
state.localPosition = Vector.sub(state.body.position, state.position);
|
534 |
+
}
|
535 |
+
},
|
536 |
+
mousemove: (event, state) => {
|
537 |
+
if (!state.isDown) return;
|
538 |
+
|
539 |
+
if (state.body) {
|
540 |
+
Body.setPosition(state.body, Vector.add(event.mouse.position, state.localPosition));
|
541 |
+
}
|
542 |
+
},
|
543 |
+
mouseup: (event, state) => {
|
544 |
+
if (!state.isDown) return;
|
545 |
+
|
546 |
+
if (state.body) {
|
547 |
+
Body.setStatic(state.body, state.wasStatic);
|
548 |
+
}
|
549 |
+
|
550 |
+
state.isDown = false;
|
551 |
+
state.body = null;
|
552 |
+
state.wasStatic = false;
|
553 |
+
state.position = null;
|
554 |
+
state.localPosition = null;
|
555 |
+
},
|
556 |
+
},
|
557 |
+
state: { isDown: false, body: null, wasStatic: false, position: null, localPosition: null },
|
558 |
+
},
|
559 |
+
{ key: 'grab',
|
560 |
+
innerHTML: `
|
561 |
+
<i class="fa-solid fa-hand"></i>
|
562 |
+
`,
|
563 |
+
onActivate: () => {
|
564 |
+
mouseConstraint.collisionFilter = { // Default
|
565 |
+
category: 0x0001,
|
566 |
+
mask: 0xFFFFFFFF,
|
567 |
+
group: 0,
|
568 |
+
};
|
569 |
+
},
|
570 |
+
onDeactivate: () => {
|
571 |
+
mouseConstraint.collisionFilter = { // Empty
|
572 |
+
category: 0x0000,
|
573 |
+
mask: 0x00000000,
|
574 |
+
group: 0,
|
575 |
+
};
|
576 |
+
},
|
577 |
+
},
|
578 |
+
],
|
579 |
+
},
|
580 |
+
{ key: 'destructive',
|
581 |
+
tools: [
|
582 |
+
{ key: 'remove',
|
583 |
+
innerHTML: `
|
584 |
+
<i class="fa-solid fa-delete-left"></i>
|
585 |
+
`,
|
586 |
+
onEvent: {
|
587 |
+
mousedown: (event, state) => {
|
588 |
+
if (state.isDown) return;
|
589 |
+
state.isDown = true;
|
590 |
+
state.bodies = Query.point(Composite.allBodies(engine.world), event.mouse.position);
|
591 |
+
},
|
592 |
+
mouseup: (event, state) => {
|
593 |
+
if (!state.isDown) return;
|
594 |
+
|
595 |
+
const confirm = Query.point(Composite.allBodies(engine.world), event.mouse.position);
|
596 |
+
const body = _.first(state.bodies, _.intersection(state.bodies, confirm));
|
597 |
+
if (body) {
|
598 |
+
Composite.remove(engine.world, body);
|
599 |
+
}
|
600 |
+
|
601 |
+
state.isDown = false;
|
602 |
+
state.bodies = null;
|
603 |
+
},
|
604 |
+
},
|
605 |
+
state: { isDown: false, bodies: null },
|
606 |
+
},
|
607 |
+
],
|
608 |
+
},
|
609 |
+
|
610 |
+
{ key: 'create',
|
611 |
+
tools: SHAPE_FACTORIES.map(shapeFactory => SHAPE_TOOL_BUILDER(shapeFactory)),
|
612 |
+
},
|
613 |
+
|
614 |
+
{ key: 'constrain',
|
615 |
+
tools: CONSTRAINT_FACTORIES.map(constraintFactory => CONSTRAINT_TOOL_BUILDER(constraintFactory)),
|
616 |
+
},
|
617 |
+
];
|
618 |
+
|
619 |
+
// Functions
|
620 |
+
function updateCanvasDimensions() {
|
621 |
+
const rect = document.documentElement.getBoundingClientRect();
|
622 |
+
const size = Vector.create(rect.width, rect.height);
|
623 |
+
|
624 |
+
render.options.width = size.x;
|
625 |
+
render.options.height = size.y;
|
626 |
+
render.canvas.width = size.x;
|
627 |
+
render.canvas.height = size.y;
|
628 |
+
render.bounds.min.x = size.x * -0.5;
|
629 |
+
render.bounds.min.y = size.y * -0.90;
|
630 |
+
render.bounds.max.x = size.x * 0.5;
|
631 |
+
render.bounds.max.y = size.y * 0.10;
|
632 |
+
}
|
633 |
+
|
634 |
+
function buildToolbox() {
|
635 |
+
const elToolBox = document.getElementById('tool-box');
|
636 |
+
|
637 |
+
let firstTool = null;
|
638 |
+
for (let category of TOOL_BOX) {
|
639 |
+
const elCategory = category.element = elToolBox.appendChild(document.createElement('div'));
|
640 |
+
elCategory.id = `tool-category-${category.key}`;
|
641 |
+
elCategory.classList.add('container')
|
642 |
+
|
643 |
+
for (let tool of category.tools) {
|
644 |
+
if (!firstTool) firstTool = tool;
|
645 |
+
|
646 |
+
tool.category = category;
|
647 |
+
tool.state = tool.state || {};
|
648 |
+
tool._onEventCallbacks = {};
|
649 |
+
|
650 |
+
const elTool = tool.element = elCategory.appendChild(document.createElement('div'));
|
651 |
+
elTool.id = `tool-tool-${category.key}-${tool.key}`;
|
652 |
+
elTool.classList.add('tool');
|
653 |
+
elTool.innerHTML = tool.innerHTML;
|
654 |
+
elTool.addEventListener('click', event => {
|
655 |
+
activateTool(tool);
|
656 |
+
});
|
657 |
+
}
|
658 |
+
|
659 |
+
if (firstTool) {
|
660 |
+
activateTool(firstTool);
|
661 |
+
}
|
662 |
+
}
|
663 |
+
}
|
664 |
+
|
665 |
+
function activateTool(tool) {
|
666 |
+
if (tool === ACTIVE_TOOL) return;
|
667 |
+
|
668 |
+
if (ACTIVE_TOOL !== null) {
|
669 |
+
if (ACTIVE_TOOL.onEvent) {
|
670 |
+
for (let [eventName, eventHandler] of Object.entries(ACTIVE_TOOL.onEvent)) {
|
671 |
+
const callback = ACTIVE_TOOL._onEventCallbacks[eventName];
|
672 |
+
if (callback) {
|
673 |
+
Events.off(mouseConstraint, eventName, callback);
|
674 |
+
delete ACTIVE_TOOL._onEventCallbacks[eventName];
|
675 |
+
}
|
676 |
+
}
|
677 |
+
}
|
678 |
+
|
679 |
+
if (ACTIVE_TOOL.onDeactivate) {
|
680 |
+
try {
|
681 |
+
ACTIVE_TOOL.onDeactivate(ACTIVE_TOOL.state);
|
682 |
+
} catch (err) {
|
683 |
+
const message = `Error attempting to deactivate tool '${ACTIVE_TOOL.category.key}:${ACTIVE_TOOL.key}': ${err.stack || err}`;
|
684 |
+
console.error(message);
|
685 |
+
alert(message);
|
686 |
+
}
|
687 |
+
}
|
688 |
+
|
689 |
+
if (ACTIVE_TOOL.element) ACTIVE_TOOL.element.classList.remove('active');
|
690 |
+
|
691 |
+
ACTIVE_TOOL = null;
|
692 |
+
}
|
693 |
+
|
694 |
+
if (tool !== null) {
|
695 |
+
ACTIVE_TOOL = tool;
|
696 |
+
|
697 |
+
if (ACTIVE_TOOL.onEvent) {
|
698 |
+
for (let [eventName, eventHandler] of Object.entries(ACTIVE_TOOL.onEvent)) {
|
699 |
+
const _tool = ACTIVE_TOOL;
|
700 |
+
const callback = _tool._onEventCallbacks[eventName] = (event) => eventHandler(event, _tool.state);
|
701 |
+
Events.on(mouseConstraint, eventName, callback);
|
702 |
+
}
|
703 |
+
}
|
704 |
+
|
705 |
+
if (ACTIVE_TOOL.onActivate) {
|
706 |
+
try {
|
707 |
+
ACTIVE_TOOL.onActivate(ACTIVE_TOOL.state);
|
708 |
+
} catch (err) {
|
709 |
+
const message = `Error attempting to activate tool '${ACTIVE_TOOL.category.key}:${ACTIVE_TOOL.key}': ${err.stack || err}`;
|
710 |
+
console.error(message);
|
711 |
+
alert(message);
|
712 |
+
}
|
713 |
+
}
|
714 |
+
|
715 |
+
if (ACTIVE_TOOL.element) ACTIVE_TOOL.element.classList.add('active');
|
716 |
+
}
|
717 |
+
}
|
718 |
+
|
719 |
+
function inspectObject(object) {
|
720 |
+
INSPECTED_OBJECT = object;
|
721 |
+
INSPECTED_OBJECT_PROPERTIES = object ? {} : null;
|
722 |
+
|
723 |
+
const elInspector = document.getElementById('inspector');
|
724 |
+
if (!object) return elInspector.replaceChildren();
|
725 |
+
|
726 |
+
const elTable = elInspector.appendChild(document.createElement('table'));
|
727 |
+
elTable.appendChild(document.createElement('thead')).innerHTML = `
|
728 |
+
<tr>
|
729 |
+
<th>Key</th>
|
730 |
+
<th>Value</th>
|
731 |
+
</tr>
|
732 |
+
`;
|
733 |
+
|
734 |
+
const elTBody = elTable.appendChild(document.createElement('tbody'));
|
735 |
+
for (let [key, value] of Object.entries(object)) {
|
736 |
+
const elRow = elTBody.appendChild(document.createElement('tr'));
|
737 |
+
elRow.setAttribute('data-key', key);
|
738 |
+
elRow.classList.add('property');
|
739 |
+
|
740 |
+
const elKey = elRow.appendChild(document.createElement('td'));
|
741 |
+
elKey.classList.add('key');
|
742 |
+
elKey.append(_.toString(key));
|
743 |
+
|
744 |
+
const elValue = elRow.appendChild(document.createElement('td'));
|
745 |
+
elValue.classList.add('value');
|
746 |
+
elValue.append(_.toString(value));
|
747 |
+
|
748 |
+
INSPECTED_OBJECT_PROPERTIES[key] = { key, value, elements: { row: elRow, key: elKey, value: elValue } };
|
749 |
+
}
|
750 |
+
|
751 |
+
elInspector.replaceChildren(elTable);
|
752 |
+
}
|
753 |
+
|
754 |
+
function updateInspectedObject() {
|
755 |
+
if (!INSPECTED_OBJECT) return;
|
756 |
+
|
757 |
+
for (let [key, property] of Object.entries(INSPECTED_OBJECT_PROPERTIES)) {
|
758 |
+
const value = property.value = INSPECTED_OBJECT[key];
|
759 |
+
const elValue = property.elements.value;
|
760 |
+
|
761 |
+
elValue.replaceChildren();
|
762 |
+
elValue.append(_.toString(value));
|
763 |
+
}
|
764 |
+
}
|
765 |
+
|
766 |
+
Events.on(engine, 'afterUpdate', () => {
|
767 |
+
const now = Date.now();
|
768 |
+
|
769 |
+
if (now - INSPECTED_OBJECT_LAST_UPDATE < 100) return;
|
770 |
+
INSPECTED_OBJECT_LAST_UPDATE = now;
|
771 |
+
|
772 |
+
updateInspectedObject();
|
773 |
+
});
|
774 |
+
|
775 |
+
Events.on(render, 'beforeRender', () => {
|
776 |
+
|
777 |
+
});
|
778 |
+
|
779 |
+
// Init
|
780 |
+
document.addEventListener('DOMContentLoaded', () => {
|
781 |
+
window.addEventListener('resize', updateCanvasDimensions);
|
782 |
+
document.addEventListener('keydown', event => {
|
783 |
+
KEYS_DOWN.add(event.code);
|
784 |
+
if (event.key === 'Alt') event.preventDefault();
|
785 |
+
});
|
786 |
+
document.addEventListener('keyup', event => {
|
787 |
+
KEYS_DOWN.delete(event.code);
|
788 |
+
if (event.key === 'Alt') event.preventDefault();
|
789 |
+
});
|
790 |
+
updateCanvasDimensions();
|
791 |
+
buildToolbox();
|
792 |
+
});
|
793 |
+
</script>
|