incanto 0.48.0 → 0.50.0

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 (54) hide show
  1. package/bin/incanto-model.mjs +13 -1
  2. package/bin/incanto-play.mjs +42 -17
  3. package/bin/incanto-playtest.mjs +7 -0
  4. package/bin/incanto-verify.mjs +10 -2
  5. package/dist/2d.d.ts +21 -12
  6. package/dist/2d.js +57 -3
  7. package/dist/3d.d.ts +38 -1
  8. package/dist/3d.js +114 -4
  9. package/dist/{create-game-CniOiWzN.js → create-game-BLDjy_PW.js} +5 -5
  10. package/dist/{create-game-akjPkFv5.js → create-game-DqqxEax1.js} +5 -5
  11. package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
  12. package/dist/{environment-presets-D1b0ydTS.js → environment-presets-XFuqu5jv.js} +15 -20
  13. package/dist/{gameplay-BQOeAid6.js → gameplay-DbaI313d.js} +96 -11
  14. package/dist/gameplay.d.ts +17 -0
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.d.ts +40 -2
  17. package/dist/index.js +7 -7
  18. package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
  19. package/dist/net.js +3 -3
  20. package/dist/{physics-2d-BmgXBNDB.js → physics-2d-_9VBOHn6.js} +4 -4
  21. package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DrpF5hcG.js} +9 -9
  22. package/dist/react.js +1 -1
  23. package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
  24. package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
  25. package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
  26. package/dist/{replay-CAphXMyM.d.ts → replay-DYdy1wb0.d.ts} +5 -1
  27. package/dist/{replay-DilbZgQI.js → replay-j-m6lJ4W.js} +23 -3
  28. package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
  29. package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
  30. package/dist/{src-Bl1Kire1.js → src-B3HrKuAi.js} +1 -1
  31. package/dist/{test-E4-otKqK.js → test-D16igj4C.js} +245 -38
  32. package/dist/test.d.ts +39 -7
  33. package/dist/test.js +2 -2
  34. package/dist/vite.js +2 -2
  35. package/editor/assets/{agent8-_007gPF8.js → agent8-t3kl5q9K.js} +1 -1
  36. package/editor/assets/{debug-0DI_MJaq.js → debug-Bu3eeAlO.js} +1 -1
  37. package/editor/assets/{index-B-6eYZEi.js → index-Df5g8ofT.js} +92 -92
  38. package/editor/index.html +1 -1
  39. package/package.json +1 -1
  40. package/schemas/scene.schema.json +10 -3
  41. package/skills/incanto-3d-character.md +13 -2
  42. package/skills/incanto-3d-models.md +40 -0
  43. package/skills/incanto-assets.md +15 -0
  44. package/skills/incanto-building-2d-games.md +57 -5
  45. package/skills/incanto-building-3d-games.md +7 -0
  46. package/skills/incanto-gameplay-behaviors.md +29 -1
  47. package/skills/incanto-hud.md +39 -0
  48. package/skills/incanto-node-reference.md +5 -3
  49. package/skills/incanto-physics-and-input.md +1 -1
  50. package/skills/incanto-playtesting.md +22 -4
  51. package/skills/incanto-verifying-your-game.md +14 -0
  52. package/templates-app/beacon-isle-3d/package.json +1 -1
  53. package/templates-app/tps-3d/package.json +1 -1
  54. package/templates-app/village-quest-3d/package.json +1 -1
@@ -1,13 +1,13 @@
1
- import { n as loadScene, w as registerBehavior } from "./loader-r49nDwB4.js";
2
- import { h as Engine } from "./register-BSXV8T9F.js";
1
+ import { n as loadScene, s as resolveViewport, w as registerBehavior } from "./loader-DwazzlQb.js";
2
+ import { h as Engine } from "./register-uvaZj1KX.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { n as startRecording } from "./replay-DilbZgQI.js";
4
+ import { n as startRecording } from "./replay-j-m6lJ4W.js";
5
5
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
6
6
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-IyWCGe4q.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-BQOeAid6.js";
8
- import { t as registerNodes2D } from "./register-R2JTnIMw.js";
9
- import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-D1b0ydTS.js";
10
- import { t as registerNodesNet } from "./register-D651it1J.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-DbaI313d.js";
8
+ import { t as registerNodes2D } from "./register-BNPZYJmd.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-XFuqu5jv.js";
10
+ import { t as registerNodesNet } from "./register-CB11yp21.js";
11
11
  import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
12
12
  //#region src/test/framing.ts
13
13
  /**
@@ -38,6 +38,15 @@ const LIGHT_TYPES = new Set([
38
38
  "OmniLight3D",
39
39
  "SpotLight3D"
40
40
  ]);
41
+ /** Screen-space subtrees: they ignore the camera, so framing has nothing to say. */
42
+ const SCREEN_SPACE = new Set(["UILayer", "HudLayer"]);
43
+ /** Depth a 2D collider is given on the axis it does not have. */
44
+ const FLAT_DEPTH = 1e3;
45
+ /** How far two colliders must actually interpenetrate to be a finding. */
46
+ const TOUCH_EPS_2D = 1;
47
+ const TOUCH_EPS_3D = .01;
48
+ /** What a 2D scene shows when it declares neither a viewport nor a canvas. */
49
+ const DEFAULT_VIEW_2D = [960, 540];
41
50
  /**
42
51
  * World transform for every node, composed from the tree.
43
52
  *
@@ -50,7 +59,11 @@ function place(scene) {
50
59
  const type = node.constructor.typeName ?? "Node";
51
60
  const n = node;
52
61
  const local = new Matrix4();
53
- if (Array.isArray(n.position)) local.compose(new Vector3(n.position[0] ?? 0, n.position[1] ?? 0, n.position[2] ?? 0), new Quaternion().setFromEuler(new Euler((n.rotation?.[0] ?? 0) * DEG, (n.rotation?.[1] ?? 0) * DEG, (n.rotation?.[2] ?? 0) * DEG, "XYZ")), new Vector3(n.scale?.[0] ?? 1, n.scale?.[1] ?? 1, n.scale?.[2] ?? 1));
62
+ if (Array.isArray(n.position)) {
63
+ const r = n.rotation;
64
+ const euler = typeof r === "number" ? new Euler(0, 0, r * DEG) : new Euler((r?.[0] ?? 0) * DEG, (r?.[1] ?? 0) * DEG, (r?.[2] ?? 0) * DEG, "XYZ");
65
+ local.compose(new Vector3(n.position[0] ?? 0, n.position[1] ?? 0, n.position[2] ?? 0), new Quaternion().setFromEuler(euler), new Vector3(n.scale?.[0] ?? 1, n.scale?.[1] ?? 1, n.scale?.[2] ?? 1));
66
+ }
54
67
  const matrix = new Matrix4().multiplyMatrices(parent, local);
55
68
  out.push({
56
69
  node,
@@ -58,6 +71,7 @@ function place(scene) {
58
71
  matrix,
59
72
  position: new Vector3().setFromMatrixPosition(matrix)
60
73
  });
74
+ if (SCREEN_SPACE.has(type)) return;
61
75
  for (const child of node.children) walk(child, matrix);
62
76
  };
63
77
  walk(scene.root, new Matrix4());
@@ -74,6 +88,54 @@ function draws(node) {
74
88
  return proto._createObject3D !== base._createObject3D;
75
89
  }
76
90
  /**
91
+ * A 2D node draws something if its class puts pixels on its backing object.
92
+ *
93
+ * NOT `_createObject2D` — the 3D test's counterpart, and wrong here. 2D
94
+ * drawables all inherit the plain container and hang a quad off it in
95
+ * `_syncObject2D`, so the create hook is identical for a sprite and for a bare
96
+ * `Node2D`, and testing it reports a scene full of sprites as drawing nothing.
97
+ */
98
+ function draws2D(node) {
99
+ const proto = Object.getPrototypeOf(node);
100
+ let base = proto;
101
+ while (base && base.constructor?.typeName !== "Node2D") {
102
+ base = Object.getPrototypeOf(base);
103
+ if (!base) return false;
104
+ }
105
+ return proto._syncObject2D !== base._syncObject2D;
106
+ }
107
+ /**
108
+ * A 2D node's half-size in pixels, and where its box hangs off the origin.
109
+ *
110
+ * Most 2D nodes are centred on their origin, but a `TileMap2D` hangs cell (0,0)
111
+ * by its TOP-LEFT corner — a whole level reported as a point at its corner is
112
+ * how you conclude the map is off screen when you are standing on it.
113
+ */
114
+ function box2D(node) {
115
+ if ((node.constructor.typeName ?? "") === "TileMap2D") {
116
+ const map = node;
117
+ const rows = Array.isArray(map.cells) ? map.cells : [];
118
+ const cols = rows.reduce((n, r) => Math.max(n, String(r).length), 0);
119
+ const ts = map.tileSize ?? 32;
120
+ if (rows.length === 0 || cols === 0) return null;
121
+ const half = [cols * ts / 2, rows.length * ts / 2];
122
+ return {
123
+ half,
124
+ centre: half
125
+ };
126
+ }
127
+ const size = node.size;
128
+ if (Array.isArray(size) && typeof size[0] === "number" && typeof size[1] === "number") return {
129
+ half: [Math.abs(size[0]) / 2, Math.abs(size[1]) / 2],
130
+ centre: [0, 0]
131
+ };
132
+ const half = halfExtents(node);
133
+ return half ? {
134
+ half: [half.x, half.y],
135
+ centre: [0, 0]
136
+ } : null;
137
+ }
138
+ /**
77
139
  * How big the node is on screen, from whatever it declares.
78
140
  *
79
141
  * `size` first (a MeshInstance3D's box, a Terrain3D's ground), then a collider.
@@ -118,10 +180,110 @@ function halfExtents(node) {
118
180
  const r = collider.radius ?? .5;
119
181
  return new Vector3(r, (collider.height ?? 1) / 2 + r, r);
120
182
  }
183
+ if (shape === "rect") {
184
+ const size = collider.size ?? [1, 1];
185
+ return new Vector3((size[0] ?? 1) / 2, (size[1] ?? 1) / 2, .5);
186
+ }
187
+ if (shape === "circle") {
188
+ const r = collider.radius ?? .5;
189
+ return new Vector3(r, r, .5);
190
+ }
121
191
  return null;
122
192
  }
193
+ /**
194
+ * The same question in 2D: an orthographic window of design pixels, centred on
195
+ * the Camera2D and CLAMPED by its limits.
196
+ *
197
+ * The clamp is not a detail — it is the whole point of `limits`, and a report
198
+ * that skips it describes a view the renderer never draws. A following camera
199
+ * sits at the player's y, which in a 544 px world is hundreds of pixels past
200
+ * anything a 540 px window can show.
201
+ *
202
+ * With no camera at all a 2D scene still draws: the documented default view is
203
+ * `(0,0)`–`(w,h)`, so "no camera" is not "nothing is framed" the way it is in 3D.
204
+ */
205
+ function describeFraming2D(scene, placed, opts) {
206
+ const design = resolveViewport(scene.viewport)?.design;
207
+ const [vw, vh] = opts.viewport ?? design ?? DEFAULT_VIEW_2D;
208
+ const cameras = placed.filter((p) => p.type === "Camera2D");
209
+ const chosen = cameras.find((p) => p.node.current === true) ?? cameras[0] ?? null;
210
+ let camera = null;
211
+ let view = {
212
+ minX: 0,
213
+ minY: 0,
214
+ maxX: vw,
215
+ maxY: vh
216
+ };
217
+ if (chosen) {
218
+ const cam = chosen.node;
219
+ const zoom = Math.max(.01, cam.zoom ?? 1);
220
+ const halfW = vw / (2 * zoom);
221
+ const halfH = vh / (2 * zoom);
222
+ let cx = chosen.position.x;
223
+ let cy = chosen.position.y;
224
+ const limits = cam.limits ?? [];
225
+ if (limits.length === 4) {
226
+ const [minX, minY, maxX, maxY] = limits;
227
+ cx = clampCentred(cx, minX + halfW, maxX - halfW);
228
+ cy = clampCentred(cy, minY + halfH, maxY - halfH);
229
+ }
230
+ view = {
231
+ minX: cx - halfW,
232
+ minY: cy - halfH,
233
+ maxX: cx + halfW,
234
+ maxY: cy + halfH
235
+ };
236
+ camera = {
237
+ path: chosen.node.getPath(),
238
+ at: [
239
+ round(cx),
240
+ round(cy),
241
+ 0
242
+ ],
243
+ view: [vw, vh],
244
+ zoom
245
+ };
246
+ }
247
+ const wanted = opts.types ? new Set(opts.types) : null;
248
+ const centre = [(view.minX + view.maxX) / 2, (view.minY + view.maxY) / 2];
249
+ const entries = [];
250
+ for (const p of placed) {
251
+ if (p.type === "Camera2D") continue;
252
+ if (!(wanted ? wanted.has(p.type) : draws2D(p.node))) continue;
253
+ const b = box2D(p.node);
254
+ const cx = p.position.x + (b?.centre[0] ?? 0);
255
+ const cy = p.position.y + (b?.centre[1] ?? 0);
256
+ const hx = b?.half[0] ?? 0;
257
+ const hy = b?.half[1] ?? 0;
258
+ const onScreen = cx + hx >= view.minX && cx - hx <= view.maxX && cy + hy >= view.minY && cy - hy <= view.maxY;
259
+ entries.push({
260
+ path: p.node.getPath(),
261
+ type: p.type,
262
+ at: [
263
+ round(p.position.x),
264
+ round(p.position.y),
265
+ 0
266
+ ],
267
+ where: onScreen ? "onScreen" : "offscreen",
268
+ screen: [round((cx - centre[0]) / ((view.maxX - view.minX) / 2)), round((cy - centre[1]) / ((view.maxY - view.minY) / 2))],
269
+ distance: round(Math.hypot(cx - centre[0], cy - centre[1]))
270
+ });
271
+ }
272
+ return {
273
+ dimension: "2d",
274
+ camera,
275
+ entries,
276
+ lights: [],
277
+ overlaps: findOverlaps(placed, TOUCH_EPS_2D)
278
+ };
279
+ }
280
+ function clampCentred(v, lo, hi) {
281
+ if (lo > hi) return (lo + hi) / 2;
282
+ return Math.min(hi, Math.max(lo, v));
283
+ }
123
284
  function describeFraming(scene, opts = {}) {
124
285
  const placed = place(scene);
286
+ if (scene.dimension === "2d") return describeFraming2D(scene, placed, opts);
125
287
  const cameras = placed.filter((p) => CAMERA_TYPES.has(p.type));
126
288
  const chosen = cameras.find((p) => p.node.current === true) ?? cameras[0] ?? null;
127
289
  const lights = [];
@@ -185,10 +347,11 @@ function describeFraming(scene, opts = {}) {
185
347
  }
186
348
  }
187
349
  return {
350
+ dimension: "3d",
188
351
  camera,
189
352
  entries,
190
353
  lights,
191
- overlaps: findOverlaps(placed)
354
+ overlaps: findOverlaps(placed, TOUCH_EPS_3D)
192
355
  };
193
356
  }
194
357
  function cornersOnScreen(box, cam) {
@@ -207,55 +370,98 @@ function cornersOnScreen(box, cam) {
207
370
  * no loaded model — and a prop buried inside a platform is the thing you were
208
371
  * trying to see anyway.
209
372
  */
210
- function findOverlaps(placed) {
211
- const boxes = [];
373
+ function findOverlaps(placed, epsilon) {
374
+ const shapes = [];
212
375
  for (const p of placed) {
213
- const half = halfExtents(p.node);
214
- if (!half) continue;
215
- boxes.push({
376
+ const shape = shapeOf(p.node, p.position);
377
+ if (!shape) continue;
378
+ shapes.push({
216
379
  path: p.node.getPath(),
217
- box: new Box3().setFromCenterAndSize(p.position, half.clone().multiplyScalar(2))
380
+ internal: p.node.name.startsWith("__"),
381
+ shape
218
382
  });
219
383
  }
220
384
  const out = [];
221
- for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
222
- const a = boxes[i];
223
- const b = boxes[j];
385
+ for (let i = 0; i < shapes.length; i++) for (let j = i + 1; j < shapes.length; j++) {
386
+ const a = shapes[i];
387
+ const b = shapes[j];
224
388
  if (!a || !b) continue;
225
389
  if (a.path.startsWith(`${b.path}/`) || b.path.startsWith(`${a.path}/`)) continue;
226
- if (a.box.intersectsBox(b.box)) out.push({
390
+ if (a.internal && b.internal) continue;
391
+ if (penetrates(a.shape, b.shape, epsilon)) out.push({
227
392
  a: a.path,
228
393
  b: b.path
229
394
  });
230
395
  }
231
396
  return out;
232
397
  }
398
+ function shapeOf(node, centre) {
399
+ const collider = node.collider;
400
+ const kind = collider?.shape;
401
+ if (kind === "sphere" || kind === "circle") return {
402
+ kind: "ball",
403
+ centre,
404
+ radius: collider?.radius ?? .5
405
+ };
406
+ const half = halfExtents(node);
407
+ if (!half) return null;
408
+ return {
409
+ kind: "box",
410
+ centre,
411
+ half: kind === "rect" ? new Vector3(half.x, half.y, FLAT_DEPTH) : half
412
+ };
413
+ }
414
+ /** True when the two shapes share more than `eps` of space, in every axis. */
415
+ function penetrates(a, b, eps) {
416
+ if (a.kind === "ball" && b.kind === "ball") return a.centre.distanceTo(b.centre) < a.radius + b.radius - eps;
417
+ if (a.kind === "ball" || b.kind === "ball") {
418
+ const ball = a.kind === "ball" ? a : b;
419
+ const box = a.kind === "ball" ? b : a;
420
+ return new Vector3(Math.max(0, Math.abs(ball.centre.x - box.centre.x) - box.half.x), Math.max(0, Math.abs(ball.centre.y - box.centre.y) - box.half.y), Math.max(0, Math.abs(ball.centre.z - box.centre.z) - box.half.z)).length() < ball.radius - eps;
421
+ }
422
+ return Math.abs(a.centre.x - b.centre.x) < a.half.x + b.half.x - eps && Math.abs(a.centre.y - b.centre.y) < a.half.y + b.half.y - eps && Math.abs(a.centre.z - b.centre.z) < a.half.z + b.half.z - eps;
423
+ }
233
424
  function round(n) {
234
425
  return Math.round(n * 1e3) / 1e3;
235
426
  }
236
427
  /** The report as something to read in a terminal. */
237
428
  function framingText(report) {
238
429
  const lines = [];
239
- if (!report.camera) lines.push("camera: NONE — this scene frames nothing (no Camera3D)");
240
- else {
241
- const c = report.camera;
242
- lines.push(`camera ${c.path} at [${c.at.map(round).join(", ")}] looking [${c.forward.join(", ")}] fov ${c.fovDeg}`);
243
- }
244
- lines.push(report.lights.length ? `lit by: ${report.lights.join(", ")}` : "lit by: NOTHING — this scene renders black");
430
+ const is2d = report.dimension === "2d";
431
+ const unit = is2d ? "px" : "m";
432
+ const c = report.camera;
433
+ if (c && is2d) {
434
+ const [vw = 0, vh = 0] = c.view ?? [];
435
+ const zoom = c.zoom === 1 ? "" : ` zoom ${c.zoom}`;
436
+ lines.push(`camera ${c.path} centred [${c.at[0]}, ${c.at[1]}] showing ${vw}×${vh}px${zoom}`);
437
+ } else if (c) lines.push(`camera ${c.path} at [${c.at.map(round).join(", ")}] looking [${(c.forward ?? []).join(", ")}] fov ${c.fovDeg}`);
438
+ else if (is2d) lines.push("camera: NONE — the view is (0,0)–(design), which may be intentional");
439
+ else lines.push("camera: NONE — this scene frames nothing (no Camera3D)");
440
+ if (!is2d) lines.push(report.lights.length ? `lit by: ${report.lights.join(", ")}` : "lit by: NOTHING — this scene renders black");
245
441
  const on = report.entries.filter((e) => e.where === "onScreen");
246
442
  const off = report.entries.filter((e) => e.where === "offscreen");
247
443
  const behind = report.entries.filter((e) => e.where === "behind");
248
- lines.push(`${on.length} on screen, ${off.length} off screen, ${behind.length} behind the camera`);
444
+ lines.push(is2d ? `${on.length} in view, ${off.length} outside it` : `${on.length} on screen, ${off.length} off screen, ${behind.length} behind the camera`);
249
445
  for (const e of report.entries) {
250
- const at = `[${e.at.map(round).join(", ")}]`;
446
+ const at = `[${(is2d ? e.at.slice(0, 2) : e.at).map(round).join(", ")}]`;
251
447
  const screen = e.screen ? ` screen [${e.screen.join(", ")}]` : "";
252
- lines.push(` ${e.where.padEnd(9)} ${e.path} (${e.type}) ${at}${screen} ${e.distance}m`);
448
+ lines.push(` ${e.where.padEnd(9)} ${e.path} (${e.type}) ${at}${screen} ${e.distance}${unit}`);
253
449
  }
254
450
  for (const o of report.overlaps) lines.push(` overlap ${o.a} ∩ ${o.b}`);
255
451
  return lines.join("\n");
256
452
  }
257
453
  //#endregion
258
454
  //#region src/test/playtest.ts
455
+ /**
456
+ * Out of the world, in pixels — about two screens under the spawn.
457
+ *
458
+ * Generous on purpose: a player who has actually left the level is falling
459
+ * forever, so a far line costs a few frames and never mistakes a long drop
460
+ * down a shaft for one.
461
+ */
462
+ const FALL_2D_PX = 1e3;
463
+ /** "Reached it", in pixels — one 32 px tile, next to a ~34 px character. */
464
+ const REACH_2D_PX = 32;
259
465
  const CONTROLLERS = new Set(["CharacterController2D", "CharacterController3D"]);
260
466
  /**
261
467
  * Who the driver is playing as.
@@ -499,8 +705,9 @@ async function runOnce(json, seed, opts) {
499
705
  0,
500
706
  0
501
707
  ];
502
- const floor = opts.fallBelow ?? start[1] - 50;
503
- const radius = opts.reachRadius ?? 2;
708
+ const down = scene.dimension === "2d" ? 1 : -1;
709
+ const floor = opts.fallBelow ?? start[1] + down * (down > 0 ? FALL_2D_PX : 50);
710
+ const radius = opts.reachRadius ?? (down > 0 ? REACH_2D_PX : 2);
504
711
  const recorder = startRecording(engine);
505
712
  const rand = () => engine.rng.next();
506
713
  const driver = new Driver(engine, rand);
@@ -527,7 +734,7 @@ async function runOnce(json, seed, opts) {
527
734
  outcome = "error";
528
735
  break;
529
736
  }
530
- if (player && at[1] < floor) {
737
+ if (player && (at[1] - floor) * down > 0) {
531
738
  outcome = "fell";
532
739
  break;
533
740
  }
@@ -584,7 +791,8 @@ async function playtest(json, opts = {}) {
584
791
  signals: [...new Set(signals)],
585
792
  actions,
586
793
  inertActions: [],
587
- declaresWin
794
+ declaresWin,
795
+ seconds: opts.seconds ?? 60
588
796
  };
589
797
  }
590
798
  function pct(n, total) {
@@ -605,8 +813,7 @@ function playtestText(report) {
605
813
  const { runs, targets, signals } = report;
606
814
  const total = runs.length;
607
815
  const lines = [];
608
- const secs = Math.round((runs[0]?.timeMs ?? 0) / 1e3);
609
- lines.push(`${total} runs × up to ${secs}s`);
816
+ lines.push(`${total} runs × up to ${report.seconds}s`);
610
817
  lines.push("");
611
818
  const won = runs.filter((r) => r.outcome === "won");
612
819
  if (won.length > 0) lines.push(` ✓ reached "won" in ${pct(won.length, total)} median ${Math.round(median(won.map((r) => r.timeMs)) / 1e3)}s`);
@@ -1414,10 +1621,10 @@ async function runScript(json, opts) {
1414
1621
  engine.setScene(scene);
1415
1622
  const physics = opts.physics ?? "auto";
1416
1623
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
1417
- const { enablePhysics2D } = await import("./physics-2d-BmgXBNDB.js").then((n) => n.r);
1624
+ const { enablePhysics2D } = await import("./physics-2d-_9VBOHn6.js").then((n) => n.r);
1418
1625
  await enablePhysics2D(engine);
1419
1626
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
1420
- const { enablePhysics3D } = await import("./physics-3d-CSoGjM8P.js").then((n) => n.r);
1627
+ const { enablePhysics3D } = await import("./physics-3d-DrpF5hcG.js").then((n) => n.r);
1421
1628
  await enablePhysics3D(engine);
1422
1629
  }
1423
1630
  const failures = [];
@@ -1530,10 +1737,10 @@ async function createPlaySession(json, opts = {}) {
1530
1737
  engine.setScene(scene);
1531
1738
  const physics = opts.physics ?? "auto";
1532
1739
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
1533
- const { enablePhysics2D } = await import("./physics-2d-BmgXBNDB.js").then((n) => n.r);
1740
+ const { enablePhysics2D } = await import("./physics-2d-_9VBOHn6.js").then((n) => n.r);
1534
1741
  await enablePhysics2D(engine);
1535
1742
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
1536
- const { enablePhysics3D } = await import("./physics-3d-CSoGjM8P.js").then((n) => n.r);
1743
+ const { enablePhysics3D } = await import("./physics-3d-DrpF5hcG.js").then((n) => n.r);
1537
1744
  await enablePhysics3D(engine);
1538
1745
  }
1539
1746
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
package/dist/test.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Et as Node, P as Scene, b as Engine, kt as LogEntry, n as BehaviorCtor } from "./behavior-62q0HWBO.js";
2
2
  import { c as JsonValue, i as SceneJson } from "./schema-CFeioQRE.js";
3
3
  import { t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
4
- import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-CAphXMyM.js";
4
+ import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-DYdy1wb0.js";
5
5
 
6
6
  //#region src/test/facing.d.ts
7
7
  interface FacingOptions {
@@ -52,12 +52,23 @@ interface FramingEntry {
52
52
  distance: number;
53
53
  }
54
54
  interface FramingReport {
55
- /** Null when the scene declares no 3D camera — nothing frames anything. */
55
+ /**
56
+ * Which report this is. The question is the same — can the camera see it —
57
+ * but the machinery and the UNITS are not: 3D projects a perspective frustum
58
+ * and measures metres, 2D clips an orthographic window and measures pixels.
59
+ */
60
+ dimension: "2d" | "3d";
61
+ /**
62
+ * 3D: null when the scene declares no camera — nothing frames anything.
63
+ * 2D: null means the documented default view, `(0,0)`–`(design)`.
64
+ */
56
65
  camera: {
57
66
  path: string;
58
- at: [number, number, number]; /** Unit vector the camera points along. */
59
- forward: [number, number, number];
60
- fovDeg: number;
67
+ at: [number, number, number]; /** 3D: the unit vector it points along. Absent in 2D. */
68
+ forward?: [number, number, number]; /** 3D only. */
69
+ fovDeg?: number; /** 2D only: world pixels visible, after `zoom`. */
70
+ view?: [number, number]; /** 2D only. */
71
+ zoom?: number;
61
72
  } | null;
62
73
  entries: FramingEntry[];
63
74
  /**
@@ -76,6 +87,11 @@ interface FramingOptions {
76
87
  aspect?: number;
77
88
  /** Report only these node types (default: everything that draws). */
78
89
  types?: readonly string[];
90
+ /**
91
+ * 2D only: the window in world pixels. Defaults to the scene's
92
+ * `viewport.design`, then 960×540 — headless there is no canvas to ask.
93
+ */
94
+ viewport?: readonly [number, number];
79
95
  }
80
96
  declare function describeFraming(scene: Scene, opts?: FramingOptions): FramingReport;
81
97
  /** The report as something to read in a terminal. */
@@ -133,9 +149,16 @@ interface PlaytestOptions {
133
149
  behaviors?: Record<string, BehaviorCtor>;
134
150
  stubMissingBehaviors?: boolean;
135
151
  resolveScene?: LoadSceneOptions["resolveScene"];
136
- /** Y below which the player has left the world. Default: 50 under the start. */
152
+ /**
153
+ * The y past which the player has left the world, in the SCENE's own
154
+ * direction: 3D counts down (−y), 2D counts down the screen (+y).
155
+ * Default: 50 m under the start in 3D, 1000 px under it in 2D.
156
+ */
137
157
  fallBelow?: number;
138
- /** How close counts as "reached", in world units. Default 2. */
158
+ /**
159
+ * How close counts as "reached", in the scene's own units. Default 2 (metres)
160
+ * in 3D, 32 (pixels — one tile) in 2D.
161
+ */
139
162
  reachRadius?: number;
140
163
  }
141
164
  interface PlaytestRun {
@@ -174,6 +197,15 @@ interface PlaytestReport {
174
197
  * author looking for a bug that was never there.
175
198
  */
176
199
  declaresWin: boolean;
200
+ /**
201
+ * The per-run clock the caller asked for, in simulated seconds.
202
+ *
203
+ * The header used to read this off the FIRST run's length, which is the one
204
+ * number guaranteed to be wrong when something ends runs early: a 60-second
205
+ * playtest whose every run died at a second announced itself as "20 runs ×
206
+ * up to 1s", hiding the very fact it was reporting.
207
+ */
208
+ seconds: number;
177
209
  }
178
210
  /**
179
211
  * Who the driver is playing as.
package/dist/test.js CHANGED
@@ -1,3 +1,3 @@
1
- import { r as auditScene } from "./replay-DilbZgQI.js";
2
- import { _ as playtestText, a as registerAllNodes, c as ladderText, d as feelText, f as facingReport, g as playtest, h as findPlayer, i as findFloatingProps, l as ladderVerdict, m as failingReplays, n as createPlaySession, o as runScript, p as facingText, r as describeCapture, s as validateScene, t as captureScene, u as feelReport, v as describeFraming, y as framingText } from "./test-E4-otKqK.js";
1
+ import { r as auditScene } from "./replay-j-m6lJ4W.js";
2
+ import { _ as playtestText, a as registerAllNodes, c as ladderText, d as feelText, f as facingReport, g as playtest, h as findPlayer, i as findFloatingProps, l as ladderVerdict, m as failingReplays, n as createPlaySession, o as runScript, p as facingText, r as describeCapture, s as validateScene, t as captureScene, u as feelReport, v as describeFraming, y as framingText } from "./test-D16igj4C.js";
3
3
  export { auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, playtest, playtestText, registerAllNodes, runScript, validateScene };
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
- import { t as VERSION } from "./src-Bl1Kire1.js";
1
+ import { t as VERSION } from "./src-B3HrKuAi.js";
2
2
  import { n as diffSignatures } from "./frame-report-njybhZon.js";
3
- import { s as validateScene } from "./test-E4-otKqK.js";
3
+ import { s as validateScene } from "./test-D16igj4C.js";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, normalize, relative, resolve, sep } from "node:path";
6
6
  //#region src/vite/frame-endpoint.ts
@@ -1 +1 @@
1
- import{n as e}from"./index-B-6eYZEi.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};
1
+ import{n as e}from"./index-Df5g8ofT.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};
@@ -1,3 +1,3 @@
1
- import{i as e,r as t,t as n}from"./index-B-6eYZEi.js";function r(e,t,n,r=1){let i=Math.min(e.x0,e.x1)*r,a=Math.max(e.x0,e.x1)*r,o=Math.min(e.y0,e.y1)*r,s=Math.max(e.y0,e.y1)*r,c=Math.max(0,Math.min(Math.round(i),t-1)),l=Math.max(0,Math.min(Math.round(o),n-1));return{x:c,y:l,w:Math.max(1,Math.min(Math.round(a-i),t-c)),h:Math.max(1,Math.min(Math.round(s-o),n-l))}}function i(e,t,n,r){let{x:i,y:a,w:o,h:s}=r,c=new Uint8ClampedArray(o*s*4);for(let n=0;n<s;n++){let r=((a+n)*t+i)*4,s=n*o*4;c.set(e.subarray(r,r+o*4),s)}return{pixels:c,width:o,height:s}}function a(e){if(!e)return`// no scene loaded`;let t=e.source??e;return JSON.stringify(t,null,2)}async function o(e){let t=globalThis.navigator;if(!t?.clipboard?.writeText)return!1;try{return await t.clipboard.writeText(e),!0}catch{return!1}}async function s(e,t){let n=globalThis,r=n.navigator?.clipboard?.write;if(!r||!n.ImageData||!n.ClipboardItem)return!1;try{let i=e.createElement(`canvas`);i.width=t.width,i.height=t.height;let a=i.getContext(`2d`);if(!a)return!1;a.putImageData(new n.ImageData(t.pixels,t.width,t.height),0,0);let o=await new Promise(e=>i.toBlob(e,`image/png`));return o?(await r.call(n.navigator?.clipboard,[new n.ClipboardItem({"image/png":o})]),!0):!1}catch{return!1}}var c=[`nw`,`n`,`ne`,`e`,`se`,`s`,`sw`,`w`],l={nw:[0,0],n:[.5,0],ne:[1,0],e:[1,.5],se:[1,1],s:[.5,1],sw:[0,1],w:[0,.5]},u={nw:`nwse-resize`,se:`nwse-resize`,ne:`nesw-resize`,sw:`nesw-resize`,n:`ns-resize`,s:`ns-resize`,e:`ew-resize`,w:`ew-resize`},d=(e,t,n)=>Math.max(t,Math.min(n,e));function f(e){return{x:Math.min(e.x0,e.x1),y:Math.min(e.y0,e.y1),w:Math.abs(e.x1-e.x0),h:Math.abs(e.y1-e.y0)}}function p(e,t,n,r,i,a){let o=e.x,s=e.y,c=e.x+e.w,l=e.y+e.h;return t.includes(`w`)&&(o=d(o+n,0,i)),t.includes(`e`)&&(c=d(c+n,0,i)),t.includes(`n`)&&(s=d(s+r,0,a)),t.includes(`s`)&&(l=d(l+r,0,a)),{x:Math.min(o,c),y:Math.min(s,l),w:Math.max(1,Math.abs(c-o)),h:Math.max(1,Math.abs(l-s))}}function m(e,t,n,r,i){return{...e,x:d(e.x+t,0,Math.max(0,r-e.w)),y:d(e.y+n,0,Math.max(0,i-e.h))}}function h(e,t,n,r,i,a=8){let o=d(e.x+e.w/2-t/2,a,Math.max(a,r-t-a)),s=e.y+e.h+a,c=e.y-n-a;return{x:o,y:s+n<=i-a?s:c>=a?c:d(s,a,Math.max(a,i-n-a))}}function g(e,t,n,r,i,a,o=180,s=120){let c=Math.max(o,n),l=Math.max(s,r);return{x:Math.min(Math.max(0,e),Math.max(0,i-c)),y:Math.min(Math.max(0,t),Math.max(0,a-l)),w:c,h:l}}function _(e,t,n,r,i,a=4){let o=Math.min(Math.max(0,e.x),Math.max(0,r-t)),s=e.y+e.h+a,c=e.y-n-a;return s+n<=i?{x:o,y:s}:c>=0?{x:o,y:c}:{x:o,y:Math.min(Math.max(0,s),Math.max(0,i-n))}}function v(e,t,n){let r=e=>{e.key===`Escape`&&n()},i=e=>{let r=e.target;if(r){for(let e of t)if(e&&(e===r||e.contains?.(r)))return;n()}};return e.addEventListener?.(`keydown`,r),e.addEventListener?.(`pointerdown`,i),()=>{e.removeEventListener?.(`keydown`,r),e.removeEventListener?.(`pointerdown`,i)}}function y(e,t){for(let[n,r]of Object.entries(t))e.style[n]=r}var b=`rgba(18, 20, 26, 0.92)`,x=`1px solid rgba(255,255,255,0.14)`,S=`12px ui-monospace, SFMono-Regular, Menlo, monospace`,C=class{host;el;body;onClose=()=>{};x;y;w;h;constructor(e,t,n,r){this.host=t,this.x=r.x,this.y=r.y,this.w=r.w,this.h=r.h,this.el=e.createElement(`div`),y(this.el,{position:`absolute`,background:b,border:x,borderRadius:`8px`,color:`rgba(255,255,255,0.88)`,font:S,display:`flex`,flexDirection:`column`,overflow:`hidden`,zIndex:`40`,pointerEvents:`auto`,boxShadow:`0 8px 28px rgba(0,0,0,0.45)`});let i=e.createElement(`div`);i.textContent=n,y(i,{padding:`6px 28px 6px 10px`,background:`rgba(255,255,255,0.07)`,cursor:`move`,userSelect:`none`,touchAction:`none`,fontWeight:`700`}),this.el.appendChild(i);let a=e.createElement(`div`);a.textContent=`×`,a.title=`close`,y(a,{position:`absolute`,top:`2px`,right:`8px`,cursor:`pointer`,fontSize:`16px`,lineHeight:`20px`,opacity:`0.7`}),a.addEventListener(`click`,()=>this.onClose()),this.el.appendChild(a),this.body=e.createElement(`div`),y(this.body,{flex:`1`,overflow:`auto`,padding:`8px 10px`}),this.el.appendChild(this.body);let o=e.createElement(`div`);o.textContent=`◢`,y(o,{position:`absolute`,right:`2px`,bottom:`0`,cursor:`nwse-resize`,opacity:`0.5`,userSelect:`none`,touchAction:`none`}),this.el.appendChild(o),this.wireDrag(i,(e,t)=>{this.x+=e,this.y+=t,this.layout()}),this.wireDrag(o,(e,t)=>{this.w+=e,this.h+=t,this.layout()}),this.layout(),t.appendChild(this.el)}remove(){this.el.remove()}layout(){let e=this.host.getBoundingClientRect(),t=g(this.x,this.y,this.w,this.h,e.width,e.height);this.x=t.x,this.y=t.y,this.w=t.w,this.h=t.h,y(this.el,{left:`${t.x}px`,top:`${t.y}px`,width:`${t.w}px`,height:`${t.h}px`})}wireDrag(e,t){let n=null,r=0,i=0;e.addEventListener(`pointerdown`,t=>{n=t.pointerId,r=t.clientX,i=t.clientY,e.setPointerCapture?.(t.pointerId)}),e.addEventListener(`pointermove`,e=>{e.pointerId===n&&(t(e.clientX-r,e.clientY-i),r=e.clientX,i=e.clientY)});let a=e=>{e.pointerId===n&&(n=null)};e.addEventListener(`pointerup`,a),e.addEventListener(`pointercancel`,a)}},w=4,T=`#6ee7dc`,E=`rgba(18,20,26,0.94)`,D=`12px ui-monospace, Menlo, monospace`;function O(e){let{doc:t,container:n}=e,r=t.createElement(`div`);y(r,{position:`absolute`,inset:`0`,cursor:`crosshair`,zIndex:`80`,pointerEvents:`auto`,userSelect:`none`,touchAction:`none`});let i=t.createElement(`div`);y(i,{position:`absolute`,inset:`0`,background:`rgba(10,14,20,0.35)`,pointerEvents:`none`}),r.appendChild(i);let a=t.createElement(`div`);y(a,{position:`absolute`,outline:`1px solid ${T}`,boxShadow:`0 0 0 9999px rgba(10,14,20,0.35)`,cursor:`grab`,display:`none`,pointerEvents:`auto`}),r.appendChild(a);let o=t.createElement(`div`);y(o,{position:`absolute`,padding:`2px 6px`,borderRadius:`4px`,background:E,color:T,font:D,pointerEvents:`none`,display:`none`,whiteSpace:`nowrap`}),r.appendChild(o);let s=t.createElement(`div`);s.textContent=`drag to select · Esc to cancel`,y(s,{position:`absolute`,top:`10px`,left:`50%`,transform:`translateX(-50%)`,padding:`4px 10px`,borderRadius:`6px`,background:E,color:`rgba(255,255,255,0.85)`,font:D,pointerEvents:`none`,whiteSpace:`nowrap`}),r.appendChild(s);let d=null,g=`idle`,_={x:0,y:0},v={x:0,y:0,w:0,h:0},b=()=>({w:n.clientWidth||1,h:n.clientHeight||1});for(let e of c){let n=t.createElement(`div`),[r,i]=l[e];y(n,{position:`absolute`,left:`${r*100}%`,top:`${i*100}%`,width:`10px`,height:`10px`,marginLeft:`-5px`,marginTop:`-5px`,borderRadius:`2px`,background:T,border:`1px solid rgba(10,14,20,0.75)`,cursor:u[e],pointerEvents:`auto`}),n.addEventListener(`pointerdown`,t=>{k(t,e)}),a.appendChild(n)}let x=t.createElement(`div`);y(x,{position:`absolute`,display:`none`,gap:`6px`,padding:`6px`,borderRadius:`8px`,background:E,border:`1px solid rgba(255,255,255,0.18)`,font:D,pointerEvents:`auto`,whiteSpace:`nowrap`,boxShadow:`0 4px 16px rgba(0,0,0,0.45)`});let S=(e,n,r)=>{let i=t.createElement(`div`);return i.textContent=e,y(i,{padding:`5px 10px`,borderRadius:`5px`,cursor:`pointer`,color:n?`#08121a`:`rgba(255,255,255,0.85)`,background:n?T:`rgba(255,255,255,0.08)`,userSelect:`none`}),i.addEventListener(`pointerdown`,e=>{C(e),r()}),i};x.appendChild(S(`Copy to clipboard`,!0,()=>N())),x.appendChild(S(`Cancel`,!1,()=>I())),r.appendChild(x);function C(e){e.preventDefault?.(),e.stopPropagation?.()}function O(e){let t=e,r=n.getBoundingClientRect?.()??{left:0,top:0};return{x:(t.clientX??0)-r.left,y:(t.clientY??0)-r.top}}function k(e,t){C(e),_=O(e),g=t,t===`create`?d={x:_.x,y:_.y,w:0,h:0}:d&&(v=d),y(x,{display:`none`}),y(a,{cursor:t===`move`?`grabbing`:`crosshair`});let n=e.pointerId;n!==void 0&&r.setPointerCapture?.(n),A()}function A(){if(!d){y(a,{display:`none`}),y(o,{display:`none`}),y(i,{display:`block`});return}y(i,{display:`none`}),y(a,{display:`block`,left:`${d.x}px`,top:`${d.y}px`,width:`${d.w}px`,height:`${d.h}px`});let t=e.scale();o.textContent=`${Math.round(d.w*t)}×${Math.round(d.h*t)}`;let n=d.y-24;y(o,{display:`block`,left:`${Math.max(2,d.x)}px`,top:`${n>=2?n:d.y+4}px`})}function j(){if(!d||d.w<w||d.h<w)return;y(x,{display:`flex`});let e=b(),t=x.offsetWidth||200,n=x.offsetHeight||36,r=h(d,t,n,e.w,e.h);y(x,{left:`${r.x}px`,top:`${r.y}px`})}r.addEventListener(`pointerdown`,e=>{k(e,`create`)}),a.addEventListener(`pointerdown`,e=>{k(e,`move`)}),r.addEventListener(`pointermove`,e=>{if(g===`idle`||!d)return;let t=O(e),n=b();if(g===`create`){let e=f({x0:_.x,y0:_.y,x1:t.x,y1:t.y});d={x:Math.max(0,Math.min(e.x,n.w)),y:Math.max(0,Math.min(e.y,n.h)),w:Math.min(e.w,n.w-Math.max(0,Math.min(e.x,n.w))),h:Math.min(e.h,n.h-Math.max(0,Math.min(e.y,n.h)))}}else d=g===`move`?m(v,t.x-_.x,t.y-_.y,n.w,n.h):p(v,g,t.x-_.x,t.y-_.y,n.w,n.h);A()});let M=()=>{g!==`idle`&&(g===`create`&&d&&(d.w<w||d.h<w)&&(d=null),g=`idle`,y(a,{cursor:`grab`}),s.textContent=d?`drag the handles or the middle · Enter to copy · Esc to cancel`:`drag to select · Esc to cancel`,A(),j())};r.addEventListener(`pointerup`,M),r.addEventListener(`pointercancel`,M);function N(){let t=d;I(),t&&e.onCopy(t)}let P=e=>{let t=e.key;t===`Escape`?I():t===`Enter`&&d&&N()};t.addEventListener?.(`keydown`,P);let F=!1;function I(){F||(F=!0,t.removeEventListener?.(`keydown`,P),r.remove(),e.onClose())}return n.appendChild(r),{element:r,close:I}}var k=96,A=200,j=8192;function M(e){return Array.isArray(e)?`Array(${e.length})`:`Object(${Object.keys(e).length} keys)`}function N(e,t){if(Array.isArray(e)&&e.some(e=>typeof e==`object`&&!!e)){let t=e.slice(0,A).map(e=>JSON.stringify(e)),n=e.length-A;return t.join(`
1
+ import{i as e,r as t,t as n}from"./index-Df5g8ofT.js";function r(e,t,n,r=1){let i=Math.min(e.x0,e.x1)*r,a=Math.max(e.x0,e.x1)*r,o=Math.min(e.y0,e.y1)*r,s=Math.max(e.y0,e.y1)*r,c=Math.max(0,Math.min(Math.round(i),t-1)),l=Math.max(0,Math.min(Math.round(o),n-1));return{x:c,y:l,w:Math.max(1,Math.min(Math.round(a-i),t-c)),h:Math.max(1,Math.min(Math.round(s-o),n-l))}}function i(e,t,n,r){let{x:i,y:a,w:o,h:s}=r,c=new Uint8ClampedArray(o*s*4);for(let n=0;n<s;n++){let r=((a+n)*t+i)*4,s=n*o*4;c.set(e.subarray(r,r+o*4),s)}return{pixels:c,width:o,height:s}}function a(e){if(!e)return`// no scene loaded`;let t=e.source??e;return JSON.stringify(t,null,2)}async function o(e){let t=globalThis.navigator;if(!t?.clipboard?.writeText)return!1;try{return await t.clipboard.writeText(e),!0}catch{return!1}}async function s(e,t){let n=globalThis,r=n.navigator?.clipboard?.write;if(!r||!n.ImageData||!n.ClipboardItem)return!1;try{let i=e.createElement(`canvas`);i.width=t.width,i.height=t.height;let a=i.getContext(`2d`);if(!a)return!1;a.putImageData(new n.ImageData(t.pixels,t.width,t.height),0,0);let o=await new Promise(e=>i.toBlob(e,`image/png`));return o?(await r.call(n.navigator?.clipboard,[new n.ClipboardItem({"image/png":o})]),!0):!1}catch{return!1}}var c=[`nw`,`n`,`ne`,`e`,`se`,`s`,`sw`,`w`],l={nw:[0,0],n:[.5,0],ne:[1,0],e:[1,.5],se:[1,1],s:[.5,1],sw:[0,1],w:[0,.5]},u={nw:`nwse-resize`,se:`nwse-resize`,ne:`nesw-resize`,sw:`nesw-resize`,n:`ns-resize`,s:`ns-resize`,e:`ew-resize`,w:`ew-resize`},d=(e,t,n)=>Math.max(t,Math.min(n,e));function f(e){return{x:Math.min(e.x0,e.x1),y:Math.min(e.y0,e.y1),w:Math.abs(e.x1-e.x0),h:Math.abs(e.y1-e.y0)}}function p(e,t,n,r,i,a){let o=e.x,s=e.y,c=e.x+e.w,l=e.y+e.h;return t.includes(`w`)&&(o=d(o+n,0,i)),t.includes(`e`)&&(c=d(c+n,0,i)),t.includes(`n`)&&(s=d(s+r,0,a)),t.includes(`s`)&&(l=d(l+r,0,a)),{x:Math.min(o,c),y:Math.min(s,l),w:Math.max(1,Math.abs(c-o)),h:Math.max(1,Math.abs(l-s))}}function m(e,t,n,r,i){return{...e,x:d(e.x+t,0,Math.max(0,r-e.w)),y:d(e.y+n,0,Math.max(0,i-e.h))}}function h(e,t,n,r,i,a=8){let o=d(e.x+e.w/2-t/2,a,Math.max(a,r-t-a)),s=e.y+e.h+a,c=e.y-n-a;return{x:o,y:s+n<=i-a?s:c>=a?c:d(s,a,Math.max(a,i-n-a))}}function g(e,t,n,r,i,a,o=180,s=120){let c=Math.max(o,n),l=Math.max(s,r);return{x:Math.min(Math.max(0,e),Math.max(0,i-c)),y:Math.min(Math.max(0,t),Math.max(0,a-l)),w:c,h:l}}function _(e,t,n,r,i,a=4){let o=Math.min(Math.max(0,e.x),Math.max(0,r-t)),s=e.y+e.h+a,c=e.y-n-a;return s+n<=i?{x:o,y:s}:c>=0?{x:o,y:c}:{x:o,y:Math.min(Math.max(0,s),Math.max(0,i-n))}}function v(e,t,n){let r=e=>{e.key===`Escape`&&n()},i=e=>{let r=e.target;if(r){for(let e of t)if(e&&(e===r||e.contains?.(r)))return;n()}};return e.addEventListener?.(`keydown`,r),e.addEventListener?.(`pointerdown`,i),()=>{e.removeEventListener?.(`keydown`,r),e.removeEventListener?.(`pointerdown`,i)}}function y(e,t){for(let[n,r]of Object.entries(t))e.style[n]=r}var b=`rgba(18, 20, 26, 0.92)`,x=`1px solid rgba(255,255,255,0.14)`,S=`12px ui-monospace, SFMono-Regular, Menlo, monospace`,C=class{host;el;body;onClose=()=>{};x;y;w;h;constructor(e,t,n,r){this.host=t,this.x=r.x,this.y=r.y,this.w=r.w,this.h=r.h,this.el=e.createElement(`div`),y(this.el,{position:`absolute`,background:b,border:x,borderRadius:`8px`,color:`rgba(255,255,255,0.88)`,font:S,display:`flex`,flexDirection:`column`,overflow:`hidden`,zIndex:`40`,pointerEvents:`auto`,boxShadow:`0 8px 28px rgba(0,0,0,0.45)`});let i=e.createElement(`div`);i.textContent=n,y(i,{padding:`6px 28px 6px 10px`,background:`rgba(255,255,255,0.07)`,cursor:`move`,userSelect:`none`,touchAction:`none`,fontWeight:`700`}),this.el.appendChild(i);let a=e.createElement(`div`);a.textContent=`×`,a.title=`close`,y(a,{position:`absolute`,top:`2px`,right:`8px`,cursor:`pointer`,fontSize:`16px`,lineHeight:`20px`,opacity:`0.7`}),a.addEventListener(`click`,()=>this.onClose()),this.el.appendChild(a),this.body=e.createElement(`div`),y(this.body,{flex:`1`,overflow:`auto`,padding:`8px 10px`}),this.el.appendChild(this.body);let o=e.createElement(`div`);o.textContent=`◢`,y(o,{position:`absolute`,right:`2px`,bottom:`0`,cursor:`nwse-resize`,opacity:`0.5`,userSelect:`none`,touchAction:`none`}),this.el.appendChild(o),this.wireDrag(i,(e,t)=>{this.x+=e,this.y+=t,this.layout()}),this.wireDrag(o,(e,t)=>{this.w+=e,this.h+=t,this.layout()}),this.layout(),t.appendChild(this.el)}remove(){this.el.remove()}layout(){let e=this.host.getBoundingClientRect(),t=g(this.x,this.y,this.w,this.h,e.width,e.height);this.x=t.x,this.y=t.y,this.w=t.w,this.h=t.h,y(this.el,{left:`${t.x}px`,top:`${t.y}px`,width:`${t.w}px`,height:`${t.h}px`})}wireDrag(e,t){let n=null,r=0,i=0;e.addEventListener(`pointerdown`,t=>{n=t.pointerId,r=t.clientX,i=t.clientY,e.setPointerCapture?.(t.pointerId)}),e.addEventListener(`pointermove`,e=>{e.pointerId===n&&(t(e.clientX-r,e.clientY-i),r=e.clientX,i=e.clientY)});let a=e=>{e.pointerId===n&&(n=null)};e.addEventListener(`pointerup`,a),e.addEventListener(`pointercancel`,a)}},w=4,T=`#6ee7dc`,E=`rgba(18,20,26,0.94)`,D=`12px ui-monospace, Menlo, monospace`;function O(e){let{doc:t,container:n}=e,r=t.createElement(`div`);y(r,{position:`absolute`,inset:`0`,cursor:`crosshair`,zIndex:`80`,pointerEvents:`auto`,userSelect:`none`,touchAction:`none`});let i=t.createElement(`div`);y(i,{position:`absolute`,inset:`0`,background:`rgba(10,14,20,0.35)`,pointerEvents:`none`}),r.appendChild(i);let a=t.createElement(`div`);y(a,{position:`absolute`,outline:`1px solid ${T}`,boxShadow:`0 0 0 9999px rgba(10,14,20,0.35)`,cursor:`grab`,display:`none`,pointerEvents:`auto`}),r.appendChild(a);let o=t.createElement(`div`);y(o,{position:`absolute`,padding:`2px 6px`,borderRadius:`4px`,background:E,color:T,font:D,pointerEvents:`none`,display:`none`,whiteSpace:`nowrap`}),r.appendChild(o);let s=t.createElement(`div`);s.textContent=`drag to select · Esc to cancel`,y(s,{position:`absolute`,top:`10px`,left:`50%`,transform:`translateX(-50%)`,padding:`4px 10px`,borderRadius:`6px`,background:E,color:`rgba(255,255,255,0.85)`,font:D,pointerEvents:`none`,whiteSpace:`nowrap`}),r.appendChild(s);let d=null,g=`idle`,_={x:0,y:0},v={x:0,y:0,w:0,h:0},b=()=>({w:n.clientWidth||1,h:n.clientHeight||1});for(let e of c){let n=t.createElement(`div`),[r,i]=l[e];y(n,{position:`absolute`,left:`${r*100}%`,top:`${i*100}%`,width:`10px`,height:`10px`,marginLeft:`-5px`,marginTop:`-5px`,borderRadius:`2px`,background:T,border:`1px solid rgba(10,14,20,0.75)`,cursor:u[e],pointerEvents:`auto`}),n.addEventListener(`pointerdown`,t=>{k(t,e)}),a.appendChild(n)}let x=t.createElement(`div`);y(x,{position:`absolute`,display:`none`,gap:`6px`,padding:`6px`,borderRadius:`8px`,background:E,border:`1px solid rgba(255,255,255,0.18)`,font:D,pointerEvents:`auto`,whiteSpace:`nowrap`,boxShadow:`0 4px 16px rgba(0,0,0,0.45)`});let S=(e,n,r)=>{let i=t.createElement(`div`);return i.textContent=e,y(i,{padding:`5px 10px`,borderRadius:`5px`,cursor:`pointer`,color:n?`#08121a`:`rgba(255,255,255,0.85)`,background:n?T:`rgba(255,255,255,0.08)`,userSelect:`none`}),i.addEventListener(`pointerdown`,e=>{C(e),r()}),i};x.appendChild(S(`Copy to clipboard`,!0,()=>N())),x.appendChild(S(`Cancel`,!1,()=>I())),r.appendChild(x);function C(e){e.preventDefault?.(),e.stopPropagation?.()}function O(e){let t=e,r=n.getBoundingClientRect?.()??{left:0,top:0};return{x:(t.clientX??0)-r.left,y:(t.clientY??0)-r.top}}function k(e,t){C(e),_=O(e),g=t,t===`create`?d={x:_.x,y:_.y,w:0,h:0}:d&&(v=d),y(x,{display:`none`}),y(a,{cursor:t===`move`?`grabbing`:`crosshair`});let n=e.pointerId;n!==void 0&&r.setPointerCapture?.(n),A()}function A(){if(!d){y(a,{display:`none`}),y(o,{display:`none`}),y(i,{display:`block`});return}y(i,{display:`none`}),y(a,{display:`block`,left:`${d.x}px`,top:`${d.y}px`,width:`${d.w}px`,height:`${d.h}px`});let t=e.scale();o.textContent=`${Math.round(d.w*t)}×${Math.round(d.h*t)}`;let n=d.y-24;y(o,{display:`block`,left:`${Math.max(2,d.x)}px`,top:`${n>=2?n:d.y+4}px`})}function j(){if(!d||d.w<w||d.h<w)return;y(x,{display:`flex`});let e=b(),t=x.offsetWidth||200,n=x.offsetHeight||36,r=h(d,t,n,e.w,e.h);y(x,{left:`${r.x}px`,top:`${r.y}px`})}r.addEventListener(`pointerdown`,e=>{k(e,`create`)}),a.addEventListener(`pointerdown`,e=>{k(e,`move`)}),r.addEventListener(`pointermove`,e=>{if(g===`idle`||!d)return;let t=O(e),n=b();if(g===`create`){let e=f({x0:_.x,y0:_.y,x1:t.x,y1:t.y});d={x:Math.max(0,Math.min(e.x,n.w)),y:Math.max(0,Math.min(e.y,n.h)),w:Math.min(e.w,n.w-Math.max(0,Math.min(e.x,n.w))),h:Math.min(e.h,n.h-Math.max(0,Math.min(e.y,n.h)))}}else d=g===`move`?m(v,t.x-_.x,t.y-_.y,n.w,n.h):p(v,g,t.x-_.x,t.y-_.y,n.w,n.h);A()});let M=()=>{g!==`idle`&&(g===`create`&&d&&(d.w<w||d.h<w)&&(d=null),g=`idle`,y(a,{cursor:`grab`}),s.textContent=d?`drag the handles or the middle · Enter to copy · Esc to cancel`:`drag to select · Esc to cancel`,A(),j())};r.addEventListener(`pointerup`,M),r.addEventListener(`pointercancel`,M);function N(){let t=d;I(),t&&e.onCopy(t)}let P=e=>{let t=e.key;t===`Escape`?I():t===`Enter`&&d&&N()};t.addEventListener?.(`keydown`,P);let F=!1;function I(){F||(F=!0,t.removeEventListener?.(`keydown`,P),r.remove(),e.onClose())}return n.appendChild(r),{element:r,close:I}}var k=96,A=200,j=8192;function M(e){return Array.isArray(e)?`Array(${e.length})`:`Object(${Object.keys(e).length} keys)`}function N(e,t){if(Array.isArray(e)&&e.some(e=>typeof e==`object`&&!!e)){let t=e.slice(0,A).map(e=>JSON.stringify(e)),n=e.length-A;return t.join(`
2
2
  `)+(n>0?`\n… ${n} more`:``)}return t.length>j?`${t.slice(0,j)} … (${t.length-j} more chars)`:t}function P(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}var F=2,I=4;function L(e,t={}){let n=t.doc??(typeof document<`u`?document:null);if(!n)return null;let r=t.container??(typeof document<`u`?document.body:null);return!r||typeof r.appendChild!=`function`?null:new B(e,r,n,t.statsSource,t.actions,t.frameSource)}var R=300,z=[`log`,`info`,`warn`,`error`,`debug`],B=class{engine;container;doc;statsSource;actions;frameSource;panels=new Map;cleanups=[];menuButton;dropdown=null;dismissDropdown=null;menuPos={x:8,y:8};selected=null;collapsedFlags=new Map;statsChip=null;logRows=[];levelEnabled={debug:!0,info:!0,warn:!0,error:!0};consoleCapture=!1;consolePatched=[];frame=0;editing=0;detailOpen=new WeakMap;resumeScale=1;timeEls=null;hovering=!1;constructor(e,t,n,r,i=[],a){this.engine=e,this.container=t,this.doc=n,this.statsSource=r,this.actions=i,this.frameSource=a,this.menuButton=n.createElement(`div`),this.menuButton.textContent=`☰ debug`,y(this.menuButton,{position:`absolute`,top:`8px`,left:`8px`,padding:`4px 10px`,background:`rgba(18,20,26,0.85)`,border:`1px solid rgba(255,255,255,0.18)`,borderRadius:`6px`,color:`rgba(255,255,255,0.85)`,font:`12px ui-monospace, Menlo, monospace`,cursor:`pointer`,userSelect:`none`,zIndex:`50`,pointerEvents:`auto`}),this.bindMenuDrag(),t.style.position||(t.style.position=`relative`),t.appendChild(this.menuButton),this.cleanups.push(e.log.added.connect(e=>{this.pushLog({level:e.level,source:`engine`,text:e.parts.map(U).join(` `)})})),this.cleanups.push(e.sceneChanged.connect(()=>{this.selected=null,this.applyColliderScope(),this.renderExplorer(),this.renderInspector()})),this.cleanups.push(e.updated.connect(()=>{this.frame+=1,this.frame%30==0&&(this.renderExplorer(),this.renderStats(),this.editing===0&&!this.hovering&&this.renderInspector(),this.syncTime())}))}isOpen(e){return e===`copyScene`||e===`captureRegion`?!1:e===`colliders`?this.colliderMode!==`off`:e===`stats`?this.statsChip!==null:this.panels.has(e)}colliderMode=`off`;setColliders(e){this.colliderMode=e,this.applyColliderScope()}applyColliderScope(){let e=this.colliderMode;for(let t of n(`2d`).concat(n(`3d`)))t.debugDraw=e!==`off`,t.debugScope=e===`selected`?this.selected:null}open(e){if(e===`stats`){this.openStatsChip();return}if(this.panels.has(e)){e===`explorer`&&this.renderExplorer(),e===`inspector`&&this.renderInspector(),e===`logs`&&this.renderLogs(),e===`time`&&this.renderTime();return}let t=new C(this.doc,this.container,{explorer:`Explorer`,inspector:`Inspector`,logs:`Logs`,time:`Time`}[e],{explorer:{x:12,y:44,w:240,h:320},inspector:{x:264,y:44,w:280,h:320},logs:{x:12,y:380,w:532,h:200},time:{x:556,y:44,w:260,h:150}}[e]);t.onClose=()=>this.close(e),e===`inspector`&&(t.body.addEventListener(`pointerenter`,()=>{this.hovering=!0}),t.body.addEventListener(`pointerleave`,()=>{this.hovering=!1})),this.panels.set(e,t),e===`explorer`&&this.renderExplorer(),e===`inspector`&&this.renderInspector(),e===`logs`&&this.renderLogs(),e===`time`&&this.renderTime()}close(e){if(e===`inspector`&&(this.hovering=!1),e===`stats`){this.statsChip?.remove(),this.statsChip=null;return}let t=this.panels.get(e);t&&(t.remove(),this.panels.delete(e))}toggle(e){if(e===`copyScene`){this.copyScene();return}if(e===`captureRegion`){this.captureRegion();return}if(e===`colliders`){this.setColliders({off:`all`,all:`selected`,selected:`off`}[this.colliderMode]);return}this.isOpen(e)?this.close(e):this.open(e)}setLevelEnabled(e,t){this.levelEnabled[e]=t,this.renderLogs()}setConsoleCapture(e){if(e!==this.consoleCapture)if(this.consoleCapture=e,e){this.renderLogs();let e=console;for(let t of z){let n=e[t];e[t]=(...e)=>{n.apply(console,e);let r=t===`log`?`info`:t;this.pushLog({level:r,source:`console`,text:e.map(U).join(` `)})},this.consolePatched.push(()=>{e[t]=n})}}else{for(let e of this.consolePatched)e();this.consolePatched=[],this.renderLogs()}}dispose(){this.engine.debugSelection=null,this.setColliders(`off`),this.setConsoleCapture(!1);for(let e of this.cleanups)e();for(let e of[...this.panels.keys()])this.close(e);this.close(`stats`),this.closeDropdown(),this.menuButton.remove()}menuLabel(e,t){let n=e===`colliders`&&this.colliderMode!==`off`?` · ${this.colliderMode}`:``;return`${this.isOpen(e)?`✓ `:``}${t}${n}`}bindMenuDrag(){let e=null,t=!1;this.menuButton.addEventListener(`pointerdown`,n=>{let r=n;e={x:r.clientX??0,y:r.clientY??0,bx:this.menuPos.x,by:this.menuPos.y},t=!1,r.pointerId!==void 0&&this.menuButton.setPointerCapture?.(r.pointerId)}),this.menuButton.addEventListener(`pointermove`,n=>{if(!e)return;let r=n,i=(r.clientX??0)-e.x,a=(r.clientY??0)-e.y;!t&&Math.hypot(i,a)<I||(t||(t=!0,this.closeDropdown(),y(this.menuButton,{cursor:`grabbing`})),this.moveMenuButton(e.bx+i,e.by+a))});let n=()=>{if(!e)return;let n=t;e=null,t=!1,y(this.menuButton,{cursor:`pointer`}),n||this.toggleDropdown()};this.menuButton.addEventListener(`pointerup`,n),this.menuButton.addEventListener(`pointercancel`,n)}moveMenuButton(e,t){let n=this.menuButton,r=g(e,t,n.offsetWidth??0,n.offsetHeight??0,this.container.clientWidth||0,this.container.clientHeight||0,0,0);this.menuPos={x:r.x,y:r.y},y(this.menuButton,{left:`${r.x}px`,top:`${r.y}px`})}closeDropdown(){this.dismissDropdown?.(),this.dismissDropdown=null,this.dropdown?.remove(),this.dropdown=null}toggleDropdown(){if(this.dropdown){this.closeDropdown();return}let e=this.doc.createElement(`div`);y(e,{position:`absolute`,background:`rgba(18,20,26,0.95)`,border:`1px solid rgba(255,255,255,0.18)`,borderRadius:`6px`,font:`12px ui-monospace, Menlo, monospace`,color:`rgba(255,255,255,0.85)`,zIndex:`60`,pointerEvents:`auto`,overflow:`hidden`});for(let[t,n]of[[`explorer`,`Explorer`],[`inspector`,`Inspector`],[`logs`,`Logs`],[`time`,`Time`],[`copyScene`,`Copy scene JSON`],[`captureRegion`,`Capture region`],[`stats`,`Stats`],[`colliders`,`Colliders`]]){let r=this.doc.createElement(`div`);r.textContent=this.menuLabel(t,n),y(r,{padding:`6px 14px`,cursor:`pointer`,userSelect:`none`}),r.addEventListener(`click`,()=>{if(this.toggle(t),t===`colliders`){r.textContent=this.menuLabel(t,n);return}this.closeDropdown()}),e.appendChild(r)}for(let t of this.actions){let n=this.doc.createElement(`div`);n.textContent=t.label,y(n,{padding:`6px 14px`,cursor:`pointer`,userSelect:`none`,borderTop:`1px solid rgba(255,255,255,0.14)`,color:`rgba(158,232,220,0.95)`}),n.addEventListener(`click`,()=>{this.closeDropdown(),t.run()}),e.appendChild(n)}this.container.appendChild(e),this.dropdown=e;let t=e,n=this.menuButton,r=_({x:this.menuPos.x,y:this.menuPos.y,w:n.offsetWidth??0,h:n.offsetHeight??26},t.offsetWidth??0,t.offsetHeight??0,this.container.clientWidth||0,this.container.clientHeight||0);y(e,{left:`${r.x}px`,top:`${r.y}px`}),this.dismissDropdown=v(this.doc,[e,this.menuButton],()=>this.closeDropdown())}openStatsChip(){if(this.statsChip)return;let e=this.doc.createElement(`div`);y(e,{position:`absolute`,top:`8px`,right:`8px`,padding:`4px 10px`,background:`rgba(18,20,26,0.85)`,border:`1px solid rgba(255,255,255,0.18)`,borderRadius:`6px`,color:`rgba(255,255,255,0.85)`,font:`12px ui-monospace, Menlo, monospace`,textAlign:`right`,whiteSpace:`pre`,userSelect:`none`,pointerEvents:`none`,zIndex:`70`}),this.container.appendChild(e),this.statsChip=e,this.renderStats()}renderStats(){let e=this.statsChip;if(!e)return;let t=this.engine.stats(),n=this.statsSource?.()??{},r=[`nodes ${t.nodes}`,...n.triangles===void 0?[]:[`tris ${H(n.triangles)}`],...n.drawCalls===void 0?[]:[`calls ${n.drawCalls}`]].join(` · `);e.textContent=`${Math.round(t.fps)} fps · ${t.frameMs.toFixed(1)} ms\n${r}`}renderExplorer(){let e=this.panels.get(`explorer`);if(!e)return;let t=e.body.scrollTop;V(e.body);let n=this.engine.scene?.root;if(!n){e.body.scrollTop=t;return}let r=(e,t)=>{let n=e.constructor,i=e.children.length>0,a=this.collapsedFlags.get(e)===!0,o=this.doc.createElement(`div`);y(o,{display:`flex`,alignItems:`center`,cursor:`pointer`,padding:`1px 2px`,borderRadius:`3px`,background:e===this.selected?`rgba(110,160,255,0.25)`:`transparent`});let s=this.doc.createElement(`span`);s.textContent=i?a?`▸`:`▾`:`·`,y(s,{width:`14px`,flex:`none`,textAlign:`center`,opacity:i?`0.85`:`0.25`,userSelect:`none`}),i&&s.addEventListener(`click`,t=>{t.stopPropagation?.(),this.collapsedFlags.set(e,!a),this.renderExplorer()}),o.appendChild(s);let c=this.doc.createElement(`span`);c.textContent=e.name,y(c,{whiteSpace:`nowrap`}),o.appendChild(c);let l=this.doc.createElement(`span`);if(l.textContent=` ${n.typeName}`,y(l,{opacity:`0.45`,whiteSpace:`nowrap`,fontSize:`10px`}),o.appendChild(l),i&&a){let t=this.doc.createElement(`span`);t.textContent=` (${e.children.length})`,y(t,{opacity:`0.35`,fontSize:`10px`}),o.appendChild(t)}if(o.addEventListener(`click`,()=>{this.selected=e,this.engine.debugSelection=e,this.applyColliderScope(),this.open(`inspector`),this.renderExplorer(),this.renderInspector()}),t.appendChild(o),i&&!a){let n=this.doc.createElement(`div`);y(n,{marginLeft:`8px`,paddingLeft:`8px`,borderLeft:`1px solid rgba(255,255,255,0.14)`});for(let t of e.children)r(t,n);t.appendChild(n)}};r(n,e.body),e.body.scrollTop=t}renderInspector(){let e=this.panels.get(`inspector`);if(!e)return;let n=e.body.scrollTop;V(e.body);let r=this.selected;if(r&&r.tree===null&&(this.selected=null,this.engine.debugSelection=null,this.applyColliderScope(),r=null),!r){let t=this.doc.createElement(`div`);t.textContent=`select a node in the Explorer`,y(t,{opacity:`0.6`}),e.body.appendChild(t);return}let i=r.constructor,a=this.doc.createElement(`div`);if(a.textContent=`${r.getPath()} · ${i.typeName}${r.uid?` · ${r.uid}`:``}`,y(a,{fontWeight:`700`,marginBottom:`6px`,whiteSpace:`pre-wrap`}),e.body.appendChild(a),r.groups.size>0){let t=this.doc.createElement(`div`);t.textContent=`groups: ${[...r.groups].join(`, `)}`,y(t,{opacity:`0.7`,marginBottom:`6px`}),e.body.appendChild(t)}let o=t(i),s=r;for(let t of Object.keys(o)){let n=o[t];this.renderValueRow(e.body,r,t,{read:()=>s[t],write:e=>{s[t]=e},options:n?.options,variants:n?.variants})}e.body.scrollTop=n}renderValueRow(t,n,r,i){let a=i.read(),o=i.options,s={get[r](){return i.read()},set[r](e){i.write(e)}},c=this.doc.createElement(`div`);y(c,{display:`flex`,gap:`6px`,alignItems:`center`,margin:`2px 0`});let l=this.doc.createElement(`div`);l.textContent=r,y(l,{minWidth:`84px`,opacity:`0.75`}),c.appendChild(l);let u=(e,t)=>{let i=this.doc.createElement(`input`);return i.type=`number`,i.value=String(e),y(i,G(`70px`)),this.trackEditing(i),i.addEventListener(`change`,()=>{let e=Number(i.value);Number.isFinite(e)?t(e):i.value=String(n[r])}),i};if(typeof a==`number`)c.appendChild(u(a,e=>{s[r]=e}));else if(typeof a==`boolean`){let e=this.doc.createElement(`input`);e.type=`checkbox`,e.checked=a,this.trackEditing(e),e.addEventListener(`change`,()=>{s[r]=e.checked}),c.appendChild(e)}else if(typeof a==`string`&&o&&o.length>0){let e=this.doc.createElement(`select`);for(let t of o.includes(a)?o:[a,...o]){let n=this.doc.createElement(`option`);n.value=t,n.textContent=t,t===a&&(n.selected=!0),e.appendChild(n)}e.value=a,this.trackEditing(e),e.addEventListener(`change`,()=>{s[r]=e.value}),y(e,G(`110px`)),c.appendChild(e)}else if(typeof a==`string`){let e=this.doc.createElement(`input`);e.type=`text`,e.value=a,this.trackEditing(e),y(e,G(`140px`)),e.addEventListener(`change`,()=>{s[r]=e.value}),c.appendChild(e)}else if(Array.isArray(a)&&a.length<=8&&a.every(e=>typeof e==`number`))for(let e=0;e<a.length;e++)c.appendChild(u(a[e],t=>{let n=[...s[r]];n[e]=t,s[r]=n}));else if(P(a)){let o=this.doc.createElement(`div`);y(o,{marginLeft:`10px`,paddingLeft:`8px`,borderLeft:`1px solid rgba(255,255,255,0.14)`});let s=i.variants?.tag,l=Object.keys(a);s&&!l.includes(s)&&l.unshift(s);let u=i.path??r;for(let t of l){let r=t===s;this.renderValueRow(o,n,t,{path:`${u}.${t}`,read:()=>i.read()[t]??(r?``:null),write:n=>{if(r&&i.variants){let t=i.variants.byTag[String(n)];if(t!==void 0){i.write(e(t));return}}i.write({...i.read(),[t]:n})},options:r&&i.variants?Object.keys(i.variants.byTag):void 0})}t.appendChild(c),t.appendChild(o);return}else{let o=JSON.stringify(e(a));if(o.length<=k){let e=this.doc.createElement(`div`);e.textContent=o,y(e,{opacity:`0.65`,whiteSpace:`pre-wrap`,wordBreak:`break-all`}),c.appendChild(e)}else{let e=i.path??r,s=this.detailOpen.get(n)?.has(e)??!1,l=this.doc.createElement(`div`);if(l.textContent=`${s?`▾`:`▸`} ${M(a)}`,y(l,{opacity:`0.75`,cursor:`pointer`,userSelect:`none`}),l.addEventListener(`click`,()=>{let t=this.detailOpen.get(n);t||(t=new Set,this.detailOpen.set(n,t)),s?t.delete(e):t.add(e),this.renderInspector()}),c.appendChild(l),t.appendChild(c),s){let e=this.doc.createElement(`div`);e.textContent=N(a,o),y(e,{opacity:`0.6`,whiteSpace:`pre-wrap`,wordBreak:`break-all`,marginLeft:`10px`,paddingLeft:`8px`,borderLeft:`1px solid rgba(255,255,255,0.14)`}),t.appendChild(e)}return}}t.appendChild(c)}trackEditing(e){e.addEventListener(`focus`,()=>{this.editing+=1}),e.addEventListener(`blur`,()=>{this.editing=Math.max(0,this.editing-1)})}pushLog(e){this.logRows.push(e),this.logRows.length>R&&this.logRows.splice(0,this.logRows.length-R),this.renderLogs()}async copyScene(){let e=await o(a(this.engine.scene??null));return this.toast(e?`scene JSON copied`:`could not reach the clipboard`),e}captureRegion(){this.capture||=(this.captureScale=globalThis.devicePixelRatio??1,this.frameSource?.().then(e=>{this.captureScale=e.width/Math.max(1,this.container.clientWidth||e.width)}).catch(()=>{}),O({doc:this.doc,container:this.container,scale:()=>this.captureScale,onCopy:e=>void this.copyRegion({x0:e.x,y0:e.y,x1:e.x+e.w,y1:e.y+e.h}),onClose:()=>{this.capture=null}}))}capture=null;captureScale=1;async copyRegion(e){let t=this.frameSource;if(!t)return this.toast(`no renderer to capture from`),!1;try{let n=await t(),a=n.width/Math.max(1,this.container.clientWidth||n.width),o=r(e,n.width,n.height,a),c=i(n.pixels,n.width,n.height,o),l=await s(this.doc,c);return this.toast(l?`copied ${c.width}×${c.height}`:`could not copy the image`),l}catch(e){return this.toast(`capture failed: ${e instanceof Error?e.message:String(e)}`),!1}}toast(e){let t=this.doc.createElement(`div`);t.textContent=e,y(t,{position:`absolute`,bottom:`16px`,left:`50%`,transform:`translateX(-50%)`,padding:`6px 12px`,borderRadius:`6px`,background:`rgba(18,20,26,0.92)`,color:`rgba(255,255,255,0.9)`,font:`12px ui-monospace, Menlo, monospace`,zIndex:`90`,pointerEvents:`none`}),this.container.appendChild(t),setTimeout(()=>t.remove(),1800)}setTimeScale(e){Number.isFinite(e)&&(this.engine.timeScale=Math.max(0,e),this.syncTime())}setPaused(e){e?(this.engine.timeScale>0&&(this.resumeScale=this.engine.timeScale),this.engine.timeScale=0):this.engine.timeScale=this.resumeScale>0?this.resumeScale:1,this.syncTime()}get paused(){return this.engine.timeScale===0}nextFrame(){this.paused||this.setPaused(!0),this.engine.step(),this.syncTime()}syncTime(){let e=this.timeEls;if(!e)return;let t=this.engine.timeScale,n=String(Math.round(t*1e3)/1e3);this.editing===0&&(e.slider.value=String(Math.min(t,F)),e.box.value=n),e.readout.textContent=`timeScale ${n}${this.paused?` · paused`:``}`,e.pause.textContent=this.paused?`▶ Resume`:`⏸ Pause`}renderTime(){let e=this.panels.get(`time`);if(!e)return;V(e.body),this.timeEls=null;let t=this.doc.createElement(`div`);y(t,{marginBottom:`6px`,opacity:`0.85`}),e.body.appendChild(t);let n=this.doc.createElement(`input`);n.type=`range`,n.min=`0`,n.max=String(F),n.step=`0.05`,n.value=String(Math.min(this.engine.timeScale,F)),y(n,{width:`100%`,marginBottom:`6px`}),this.trackEditing(n),n.addEventListener(`input`,()=>this.setTimeScale(Number(n.value))),e.body.appendChild(n);let r=this.doc.createElement(`div`);y(r,{display:`flex`,gap:`6px`,alignItems:`center`,marginBottom:`8px`});let i=this.doc.createElement(`input`);i.type=`number`,i.min=`0`,i.step=`0.05`,i.value=String(this.engine.timeScale),y(i,G(`72px`)),this.trackEditing(i),i.addEventListener(`change`,()=>{let e=Number(i.value);Number.isFinite(e)&&this.setTimeScale(e),this.syncTimeAfterEdit()}),r.appendChild(i);for(let e of[.25,.5,1,2]){let t=this.doc.createElement(`div`);t.textContent=`${e}×`,y(t,{cursor:`pointer`,opacity:`0.7`,padding:`2px 4px`}),t.addEventListener(`click`,()=>this.setTimeScale(e)),r.appendChild(t)}e.body.appendChild(r);let a=this.doc.createElement(`div`);y(a,{display:`flex`,gap:`8px`});let o=this.doc.createElement(`div`);y(o,W()),o.addEventListener(`click`,()=>this.setPaused(!this.paused)),a.appendChild(o);let s=this.doc.createElement(`div`);s.textContent=`⏭ Next frame`,s.title=`Pauses, then advances one fixed step`,y(s,W()),s.addEventListener(`click`,()=>this.nextFrame()),a.appendChild(s),e.body.appendChild(a),this.timeEls={slider:n,box:i,readout:t,pause:o},this.syncTime()}syncTimeAfterEdit(){let e=this.timeEls;if(!e)return;let t=this.engine.timeScale;e.slider.value=String(Math.min(t,F)),e.box.value=String(Math.round(t*1e3)/1e3)}renderLogs(){let e=this.panels.get(`logs`);if(!e)return;V(e.body);let t=this.doc.createElement(`div`);y(t,{display:`flex`,gap:`8px`,marginBottom:`4px`,flexWrap:`wrap`});for(let e of[`debug`,`info`,`warn`,`error`]){let n=this.doc.createElement(`div`);n.textContent=`${this.levelEnabled[e]?`✓`:`·`}${e}`,y(n,{cursor:`pointer`,opacity:this.levelEnabled[e]?`1`:`0.45`}),n.addEventListener(`click`,()=>this.setLevelEnabled(e,!this.levelEnabled[e])),t.appendChild(n)}let n=this.doc.createElement(`div`);n.textContent=`${this.consoleCapture?`✓`:`·`}console`,y(n,{cursor:`pointer`,marginLeft:`auto`}),n.addEventListener(`click`,()=>this.setConsoleCapture(!this.consoleCapture)),t.appendChild(n),e.body.appendChild(t);let r={debug:`rgba(255,255,255,0.5)`,info:`rgba(255,255,255,0.85)`,warn:`#ffc861`,error:`#ff6b6b`};for(let t of this.logRows){if(!this.levelEnabled[t.level])continue;let n=this.doc.createElement(`div`);n.textContent=`[${t.source===`console`?`console`:t.level}] ${t.text}`,y(n,{color:r[t.level],whiteSpace:`pre-wrap`}),e.body.appendChild(n)}}};function V(e){for(let t of[...e.children])t.remove()}function H(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}function U(e){if(typeof e==`string`)return e;if(e instanceof Error){let t=e.stack?.split(`
3
3
  `)[1]?.trim();return`${e.name}: ${e.message}${t?` (${t})`:``}`}try{let t=JSON.stringify(e);return t===`{}`||t===void 0?String(e):t}catch{return String(e)}}function W(){return{background:`rgba(255,255,255,0.08)`,border:`1px solid rgba(255,255,255,0.2)`,borderRadius:`4px`,padding:`4px 8px`,cursor:`pointer`,userSelect:`none`}}function G(e){return{width:e,background:`rgba(255,255,255,0.08)`,border:`1px solid rgba(255,255,255,0.2)`,borderRadius:`4px`,color:`inherit`,font:`inherit`,padding:`2px 4px`}}export{L as attachDebugOverlay};