castle-web-cli 0.4.120 → 0.4.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/editorConfig.d.ts +6 -1
  2. package/dist/editorConfig.js +20 -1
  3. package/dist/ide.d.ts +1 -0
  4. package/dist/ide.js +47 -1
  5. package/dist/init.js +1 -1
  6. package/dist/shell/assets/{index-Cxmq9Sed.js → index-DXBpj3-y.js} +58 -58
  7. package/dist/shell/index.html +1 -1
  8. package/dist/vitePlugins.js +21 -8
  9. package/kits/physics-2d/CLAUDE.md +56 -5
  10. package/kits/physics-2d/behaviors/AnalogStick.jsx +40 -9
  11. package/kits/physics-2d/behaviors/Collider.jsx +12 -0
  12. package/kits/physics-2d/behaviors/Draggable.jsx +67 -23
  13. package/kits/physics-2d/behaviors/Goal.jsx +2 -0
  14. package/kits/physics-2d/behaviors/Joints.jsx +5 -0
  15. package/kits/physics-2d/behaviors/RigidBody.jsx +16 -0
  16. package/kits/physics-2d/behaviors/Slingshot.jsx +48 -16
  17. package/kits/physics-2d/behaviors/Sound.jsx +11 -0
  18. package/kits/physics-2d/behaviors/Sprite.jsx +7 -0
  19. package/kits/physics-2d/behaviors/Tone.jsx +12 -0
  20. package/kits/physics-2d/behaviors/Video.jsx +6 -0
  21. package/kits/physics-2d/castle.json +1 -1
  22. package/kits/physics-2d/editors/SceneEditor.jsx +42 -6
  23. package/kits/physics-2d/editors/SingleEditor.jsx +43 -2
  24. package/kits/physics-2d/editors/editorRegistry.jsx +34 -0
  25. package/kits/physics-2d/engine/ScenePlayer.jsx +9 -3
  26. package/kits/physics-2d/engine/autoInspector.jsx +28 -3
  27. package/kits/physics-2d/engine/physics/PhysicsSystem.js +117 -0
  28. package/kits/physics-2d/engine/physics/controls.js +43 -19
  29. package/kits/physics-2d/engine/propertyRanges.js +27 -0
  30. package/kits/physics-2d/engine/scene.js +86 -4
  31. package/kits/physics-2d/engine/ui.jsx +14 -1
  32. package/kits/physics-2d/main.jsx +11 -2
  33. package/package.json +2 -1
@@ -41,6 +41,12 @@ export class SceneRuntime {
41
41
  this.files = files ?? {};
42
42
  this.time = 0;
43
43
  this.keys = new Set();
44
+ // Every active pointer (finger / mouse / stylus), keyed by the browser's
45
+ // pointerId. A control that starts a gesture latches the id it started on
46
+ // and follows THAT pointer, so a second finger landing can't steal or drop
47
+ // an in-progress drag. `pointer` stays the primary (first-down) pointer for
48
+ // single-touch behaviors and for decks reading the documented API.
49
+ this.pointers = new Map();
44
50
  this.pointer = { x: 0, y: 0, down: false };
45
51
  this.data = { actors: [] };
46
52
  this.actors = new Map();
@@ -210,11 +216,85 @@ export class SceneRuntime {
210
216
  // input path -- the standalone ScenePlayer and the editor's play mode --
211
217
  // agree on it; behaviors read `scene.pointer` in world coordinates. Pass
212
218
  // `down` to also update the press state.
213
- setPointerFromScreen(canvas, clientX, clientY, down) {
219
+ // `pointerId` identifies which finger/device this event came from; the
220
+ // browser reuses it for every event of one contact, press through release.
221
+ setPointerFromScreen(canvas, clientX, clientY, down, pointerId = 0) {
214
222
  const point = screenToCard(canvas, clientX, clientY);
215
- this.pointer.x = point.x + (this.camera?.x ?? 0);
216
- this.pointer.y = point.y + (this.camera?.y ?? 0);
217
- if (down !== undefined) this.pointer.down = down;
223
+ const x = point.x + (this.camera?.x ?? 0);
224
+ const y = point.y + (this.camera?.y ?? 0);
225
+ let p = this.pointers.get(pointerId);
226
+ if (!p) {
227
+ // Only a press creates a pointer. A move with no press is a mouse hover,
228
+ // and a stray event after release must not resurrect a finished contact.
229
+ if (down !== true) return this.syncPrimaryPointer(x, y);
230
+ p = { id: pointerId, x, y, down: false, justPressed: false, claimedBy: null, claimPriority: 0 };
231
+ this.pointers.set(pointerId, p);
232
+ }
233
+ p.x = x;
234
+ p.y = y;
235
+ // The press edge is per-pointer, so a finger landing while another is
236
+ // already held still registers as a new press (a shared `down` flag would
237
+ // show no rising edge and the second press would be invisible).
238
+ if (down === true && !p.down) p.justPressed = true;
239
+ if (down !== undefined) p.down = down;
240
+ this.syncPrimaryPointer(x, y);
241
+ }
242
+
243
+ // Claim a pointer for one gesture, so two controls can't both act on the same
244
+ // finger. Returns whether `ownerId` holds it afterwards.
245
+ //
246
+ // `priority` resolves the case that actor order otherwise decides arbitrarily:
247
+ // a TARGETED control (a Draggable whose collider was actually pressed) must
248
+ // beat a GREEDY one (a Slingshot set to grab anywhere, an AnalogStick that
249
+ // takes any press), no matter which actor updates first. A higher priority
250
+ // takes the pointer from a lower one; equal or lower leaves it alone.
251
+ //
252
+ // Losing a claim is how the greedy control learns to stand down, and it costs
253
+ // nothing visible: on the press frame its own displacement from its origin is
254
+ // still zero, so it has commanded no velocity yet.
255
+ claimPointer(pointerId, ownerId, priority = 0) {
256
+ const p = this.pointers.get(pointerId);
257
+ if (!p) return false;
258
+ if (p.claimedBy == null || p.claimedBy === ownerId || priority > p.claimPriority) {
259
+ p.claimedBy = ownerId;
260
+ p.claimPriority = priority;
261
+ }
262
+ return p.claimedBy === ownerId;
263
+ }
264
+
265
+ // True while `ownerId` still holds this pointer. A control that latched a
266
+ // pointer checks this each frame so it releases when outranked. Claims need no
267
+ // cleanup: they live on the pointer entry, which disappears when it lifts.
268
+ ownsPointer(pointerId, ownerId) {
269
+ return this.pointers.get(pointerId)?.claimedBy === ownerId;
270
+ }
271
+
272
+ // Released pointers are reaped at the end of the frame rather than on the
273
+ // event, so a tap that begins and ends between two frames is still seen once
274
+ // -- and so a control latched to a pointer observes `down: false` before the
275
+ // entry disappears.
276
+ reapPointers() {
277
+ for (const [id, p] of this.pointers) {
278
+ p.justPressed = false;
279
+ if (!p.down) this.pointers.delete(id);
280
+ }
281
+ this.syncPrimaryPointer();
282
+ }
283
+
284
+ // `pointer` mirrors the primary -- the oldest pointer still held (Map keeps
285
+ // insertion order) -- so single-touch behaviors and decks reading the
286
+ // documented `scene.pointer` API behave exactly as before.
287
+ syncPrimaryPointer(fallbackX, fallbackY) {
288
+ for (const p of this.pointers.values()) {
289
+ if (!p.down) continue;
290
+ this.pointer.x = p.x;
291
+ this.pointer.y = p.y;
292
+ this.pointer.down = true;
293
+ return;
294
+ }
295
+ if (fallbackX !== undefined) this.pointer.x = fallbackX;
296
+ if (fallbackY !== undefined) this.pointer.y = fallbackY;
297
+ this.pointer.down = false;
218
298
  }
219
299
 
220
300
  update(dt) {
@@ -228,6 +308,8 @@ export class SceneRuntime {
228
308
  for (const system of this.systems) {
229
309
  system.afterBehaviors?.(this, dt);
230
310
  }
311
+ // Every behavior and system has now seen this frame's presses and releases.
312
+ this.reapPointers();
231
313
  }
232
314
 
233
315
  forEachBehavior(actor, callback) {
@@ -458,6 +458,19 @@ function NumberInlineInput({ value, min, max, step, onCommit, onDone }) {
458
458
  );
459
459
  }
460
460
 
461
+ // Snap to a multiple of `step`. `Math.round(v / step) * step` alone reintroduces
462
+ // binary error the moment `step` isn't representable in base 2 -- 0.05 x 14 is
463
+ // 0.7000000000000001, which then gets stored and rendered in full -- so re-round
464
+ // to the decimal places the step itself implies.
465
+ function snapToStep(v, step) {
466
+ const snapped = Math.round(v / step) * step;
467
+ const text = String(step);
468
+ const decimals = text.includes('e-')
469
+ ? Number(text.split('e-')[1])
470
+ : (text.split('.')[1] ?? '').length;
471
+ return Number(snapped.toFixed(Math.min(decimals, 100)));
472
+ }
473
+
461
474
  export function NumberField({ label, value, onChange, min, max, step = 1, overridden, defaultValue, onReset }) {
462
475
  const current = Number.isFinite(value) ? (value ?? 0) : 0;
463
476
  // A field with BOTH a min and max is a true slider (fill + thumb), the pointer's X
@@ -481,7 +494,7 @@ export function NumberField({ label, value, onChange, min, max, step = 1, overri
481
494
  return v;
482
495
  }
483
496
  function scrubTo(v) {
484
- if (step) v = Math.round(v / step) * step;
497
+ if (step) v = snapToStep(v, step);
485
498
  v = clampVal(v);
486
499
  if (v !== current) onChange(v);
487
500
  }
@@ -12,13 +12,22 @@ if (!root) throw new Error('Missing root element');
12
12
  // File browsing and the code/text editor are now builtin shell panels, so the
13
13
  // kit only renders the deck (play) and its rich per-file editors (scene/pxart):
14
14
  // (no edit / ?edit=0) -> play the deck (playtest panel)
15
- // ?file=<path>[&editor=<id>] -> a single rich editor for that file
15
+ // ?file=<path>[&editorModule=<path>] -> a single rich editor for that file
16
16
  const params = new URLSearchParams(window.location.search);
17
17
  function pick() {
18
18
  const initialScene = params.get('scene') ?? undefined;
19
19
  if (!isEdit()) return <PlayOnly initialScene={initialScene} />;
20
20
  const file = params.get('file');
21
- if (file) return <SingleEditor path={file} editor={params.get('editor') ?? undefined} />;
21
+ if (file)
22
+ return (
23
+ <SingleEditor
24
+ path={file}
25
+ editor={params.get('editor') ?? undefined}
26
+ // Which module edits this file, resolved by the serve from whichever
27
+ // deck declared the type -- see `editor` in castle.json.
28
+ editorModule={params.get('editorModule') ?? undefined}
29
+ />
30
+ );
22
31
  return <PlayOnly initialScene={initialScene} />;
23
32
  }
24
33
  createRoot(root).render(<ErrorBoundary>{pick()}</ErrorBoundary>);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.120",
3
+ "version": "0.4.122",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"
@@ -37,6 +37,7 @@
37
37
  "@xterm/xterm": "^6.0.0",
38
38
  "codemirror": "^6.0.2",
39
39
  "dockview": "^4.13.1",
40
+ "html2canvas": "^1.4.1",
40
41
  "marked": "^18.0.5",
41
42
  "nanoid": "^5.1.7",
42
43
  "open": "^10.0.0",