incanto 0.28.0 → 0.30.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 (63) hide show
  1. package/README.md +44 -0
  2. package/bin/incanto-editor.mjs +219 -29
  3. package/dist/2d.d.ts +54 -6
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +353 -16
  6. package/dist/3d.js +5 -5
  7. package/dist/{behavior-CPibUfnH.d.ts → behavior-BAc0erXF.d.ts} +21 -0
  8. package/dist/{create-game-71TJjW1T.js → create-game-5z_QVtLx.js} +59 -16
  9. package/dist/{create-game-BRgWpNsa.js → create-game-DuBTv2zI.js} +276 -49
  10. package/dist/debug-draw-BM3DsvtT.js +18 -0
  11. package/dist/debug.d.ts +51 -6
  12. package/dist/debug.js +242 -19
  13. package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
  14. package/dist/editor.d.ts +49 -0
  15. package/dist/editor.js +8554 -0
  16. package/dist/{gameplay-DEG-TP7D.js → gameplay-Cfr6aFZ1.js} +219 -40
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +31 -3
  20. package/dist/index.js +2 -2
  21. package/dist/{loader-DILt9PGC.d.ts → loader-B242FF6N.d.ts} +1 -1
  22. package/dist/net.d.ts +1 -1
  23. package/dist/net.js +2 -2
  24. package/dist/{pathfinding-RWYkNKx9.d.ts → pathfinding-BwD974Ss.d.ts} +1 -1
  25. package/dist/{physics-2d-DiVFFlH3.js → physics-2d-3kOQCtgd.js} +88 -3
  26. package/dist/{physics-3d--y5clE2j.js → physics-3d-CeRH-Ff_.js} +475 -9
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-nObreUQR.js → register-BTg0EM7s.js} +35 -3
  30. package/dist/{register-6R75AC7-.js → register-DJ0SByQg.js} +5550 -5073
  31. package/dist/{register-CvpSUU3O.js → register-DWcWq4QG.js} +22 -2
  32. package/dist/{register-BFFE1Mh1.js → register-MelqEdza.js} +1 -1
  33. package/dist/teardown-ByzfDPyu.js +240 -0
  34. package/dist/test.d.ts +35 -3
  35. package/dist/test.js +104 -10
  36. package/dist/vite.d.ts +139 -1
  37. package/dist/vite.js +355 -4
  38. package/editor/assets/{agent8-CojUfCXN.js → agent8-CAp0i5qn.js} +1 -1
  39. package/editor/assets/debug-BoEYfbqK.js +2 -0
  40. package/editor/assets/index-BO6WU8by.js +10696 -0
  41. package/editor/index.html +3 -157
  42. package/package.json +3 -2
  43. package/schemas/scene.schema.json +102 -4
  44. package/skills/incanto-3d-models.md +38 -0
  45. package/skills/incanto-assets.md +13 -0
  46. package/skills/incanto-building-3d-games.md +83 -6
  47. package/skills/incanto-editor.md +212 -8
  48. package/skills/incanto-environment.md +65 -1
  49. package/skills/incanto-node-reference.md +33 -1
  50. package/skills/incanto-physics-and-input.md +60 -6
  51. package/skills/incanto-verifying-your-game.md +57 -2
  52. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
  53. package/templates-app/beacon-isle-3d/package.json +1 -1
  54. package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
  55. package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
  56. package/templates-app/tps-3d/package.json +1 -1
  57. package/templates-app/tps-3d/vite.config.ts +7 -0
  58. package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
  59. package/templates-app/village-quest-3d/package.json +1 -1
  60. package/templates-app/village-quest-3d/vite.config.ts +7 -0
  61. package/dist/debug-draw-CZmOYjL2.js +0 -13
  62. package/editor/assets/index-D6RQgROR.js +0 -8330
  63. package/editor/assets/index-D8QvwvOm.css +0 -1
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as RendererStats, E as LogLevel, v as Engine } from "./behavior-CPibUfnH.js";
1
+ import { C as RendererStats, E as LogLevel, v as Engine } from "./behavior-BAc0erXF.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
@@ -8,10 +8,30 @@ interface DocumentLike {
8
8
  //#endregion
9
9
  //#region src/debug/index.d.ts
10
10
  type DebugPanelId = "explorer" | "inspector" | "logs" | "stats";
11
+ /** Menu entries that are switches rather than panels. */
12
+ type DebugSwitchId = "colliders";
13
+ /** `all` is every collider in the world; `selected` follows the Explorer. */
14
+ type ColliderMode = "off" | "all" | "selected";
15
+ type DebugMenuId = DebugPanelId | DebugSwitchId;
16
+ /** An extra ☰ menu entry the host app supplies (e.g. "edit this scene"). */
17
+ interface DebugAction {
18
+ /** Menu label. Keep it a verb — the menu is a list of things you can do. */
19
+ label: string;
20
+ /** Runs on click. The menu closes first, so a slow action can await freely. */
21
+ run(): void | Promise<void>;
22
+ }
11
23
  interface DebugOverlayOptions {
12
24
  /** Overlay host (default: document.body). Give it position: relative. */
13
25
  container?: HTMLElement;
14
26
  /**
27
+ * Extra menu entries, listed under the panels with a separator.
28
+ *
29
+ * This is how a page hangs its own tooling off the overlay without the
30
+ * overlay having to know what tooling exists — `createGame3D`'s live editor
31
+ * switch arrives through here.
32
+ */
33
+ actions?: DebugAction[];
34
+ /**
15
35
  * Renderer counters for the Stats chip — the overlay only holds the engine,
16
36
  * so createGame wires `() => renderer.stats()` here. Omitted (headless/stub
17
37
  * renderers): the chip shows engine stats only.
@@ -27,6 +47,7 @@ declare class DebugOverlay {
27
47
  private readonly container;
28
48
  private readonly doc;
29
49
  private readonly statsSource?;
50
+ private readonly actions;
30
51
  private readonly panels;
31
52
  private readonly cleanups;
32
53
  private readonly menuButton;
@@ -41,11 +62,27 @@ declare class DebugOverlay {
41
62
  private consolePatched;
42
63
  private frame;
43
64
  private editing;
44
- constructor(engine: Engine, container: HTMLElement, doc: DocumentLike, statsSource?: (() => Partial<RendererStats>) | undefined);
45
- isOpen(id: DebugPanelId): boolean;
65
+ /** Which bulky values are unfolded, per node ('transforms', 'collider.vertices'). */
66
+ private readonly detailOpen;
67
+ /** Pointer inside the inspector — it stops refreshing under your hand. */
68
+ private hovering;
69
+ constructor(engine: Engine, container: HTMLElement, doc: DocumentLike, statsSource?: (() => Partial<RendererStats>) | undefined, actions?: DebugAction[]);
70
+ isOpen(id: DebugMenuId): boolean;
71
+ /** How much of the physics world the wireframes show. */
72
+ colliderMode: ColliderMode;
73
+ /**
74
+ * Physics wireframes, from the menu instead of from the console. What you see
75
+ * is the shape data the solver holds — the fastest way to catch a collider
76
+ * that disagrees with the art it belongs to.
77
+ *
78
+ * `'selected'` narrows it to the Explorer's selection, because `'all'` on a
79
+ * real map is a terrain grid burying the one shape you came to look at.
80
+ */
81
+ setColliders(mode: ColliderMode): void;
82
+ private applyColliderScope;
46
83
  open(id: DebugPanelId): void;
47
84
  close(id: DebugPanelId): void;
48
- toggle(id: DebugPanelId): void;
85
+ toggle(id: DebugMenuId): void;
49
86
  setLevelEnabled(level: LogLevel, on: boolean): void;
50
87
  /**
51
88
  * Patch console.* into the Logs panel (restored on disable/dispose).
@@ -54,15 +91,23 @@ declare class DebugOverlay {
54
91
  */
55
92
  setConsoleCapture(on: boolean): void;
56
93
  dispose(): void;
94
+ /** `✓ Colliders · selected` — check for on, suffix for the collider mode. */
95
+ private menuLabel;
57
96
  private toggleDropdown;
58
97
  private openStatsChip;
59
98
  private renderStats;
60
99
  private renderExplorer;
61
100
  private renderInspector;
62
- private renderPropRow;
101
+ /**
102
+ * One editable row. `read`/`write` are closures rather than a (node, key)
103
+ * pair so the SAME renderer serves a node prop and a key INSIDE an object
104
+ * prop — `collider.radius` gets a real number field instead of being a
105
+ * character in an ellipsised JSON dump.
106
+ */
107
+ private renderValueRow;
63
108
  private trackEditing;
64
109
  private pushLog;
65
110
  private renderLogs;
66
111
  }
67
112
  //#endregion
68
- export { DebugOverlay, DebugOverlayOptions, DebugPanelId, attachDebugOverlay };
113
+ export { ColliderMode, DebugAction, DebugMenuId, DebugOverlay, DebugOverlayOptions, DebugPanelId, DebugSwitchId, attachDebugOverlay };
package/dist/debug.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
2
2
  import { s as mergeStaticProps } from "./registry-BVJ2HbCn.js";
3
+ import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
3
4
  //#region src/debug/panel.ts
4
5
  /** Keep a panel rect on screen and above the minimum usable size. */
5
6
  function clampPanelRect(x, y, w, h, viewW, viewH, minW = 180, minH = 120) {
@@ -152,13 +153,52 @@ var FloatingPanel = class {
152
153
  };
153
154
  //#endregion
154
155
  //#region src/debug/index.ts
156
+ /**
157
+ * incanto/debug — the runtime debug overlay: play the game WITH X-ray vision.
158
+ *
159
+ * A ☰ debug menu (top-left) toggles three floating, draggable, resizable
160
+ * panels over the running game:
161
+ * - Explorer — the live scene tree; click a node to inspect it
162
+ * - Inspector — the selected node's current state; simple values editable
163
+ * - Logs — engine.log tail + optional browser-console capture, level filters
164
+ * plus a Stats chip — a fixed always-on-top perf readout pinned top-right
165
+ * (fps · frame ms / nodes · tris · draw calls) — and a Colliders switch that
166
+ * draws every physics shape as wireframe over the live game.
167
+ *
168
+ * Enable via `createGame({ debug: true })`, gated to dev (e.g.
169
+ * `debug: import.meta.env.VITE_INCANTO_DEBUG === '1'`). There is NO URL/query
170
+ * toggle — a deployed build must not be switchable on by end users.
171
+ */
172
+ /** Longest serialized value the inspector shows inline, un-folded. */
173
+ const DETAIL_INLINE_MAX = 96;
174
+ /** Expanded detail is capped — a 100k-float vertices array must not be the DOM. */
175
+ const DETAIL_MAX_ROWS = 200;
176
+ const DETAIL_MAX_CHARS = 8192;
177
+ /** One line saying what the folded value is and how much of it there is. */
178
+ function summarizeValue(value) {
179
+ if (Array.isArray(value)) return `Array(${value.length})`;
180
+ return `Object(${Object.keys(value).length} keys)`;
181
+ }
182
+ /** The unfolded body: one row per element where that reads better than a blob. */
183
+ function detailText(value, json) {
184
+ if (Array.isArray(value) && value.some((v) => typeof v === "object" && v !== null)) {
185
+ const rows = value.slice(0, DETAIL_MAX_ROWS).map((v) => JSON.stringify(v));
186
+ const extra = value.length - DETAIL_MAX_ROWS;
187
+ return rows.join("\n") + (extra > 0 ? `\n… ${extra} more` : "");
188
+ }
189
+ return json.length > DETAIL_MAX_CHARS ? `${json.slice(0, DETAIL_MAX_CHARS)} … (${json.length - DETAIL_MAX_CHARS} more chars)` : json;
190
+ }
191
+ /** A JSON object (not an array, not null) — the shape that becomes a block. */
192
+ function isPlainObject(value) {
193
+ return typeof value === "object" && value !== null && !Array.isArray(value);
194
+ }
155
195
  /** Attach the overlay; null when no DOM is available (headless). */
156
196
  function attachDebugOverlay(engine, opts = {}) {
157
197
  const doc = opts.doc ?? (typeof document !== "undefined" ? document : null);
158
198
  if (!doc) return null;
159
199
  const container = opts.container ?? (typeof document !== "undefined" ? document.body : null);
160
200
  if (!container || typeof container.appendChild !== "function") return null;
161
- return new DebugOverlay(engine, container, doc, opts.statsSource);
201
+ return new DebugOverlay(engine, container, doc, opts.statsSource, opts.actions);
162
202
  }
163
203
  const MAX_LOG_ROWS = 300;
164
204
  const CONSOLE_LEVELS = [
@@ -173,6 +213,7 @@ var DebugOverlay = class {
173
213
  container;
174
214
  doc;
175
215
  statsSource;
216
+ actions;
176
217
  panels = /* @__PURE__ */ new Map();
177
218
  cleanups = [];
178
219
  menuButton;
@@ -192,11 +233,16 @@ var DebugOverlay = class {
192
233
  consolePatched = [];
193
234
  frame = 0;
194
235
  editing = 0;
195
- constructor(engine, container, doc, statsSource) {
236
+ /** Which bulky values are unfolded, per node ('transforms', 'collider.vertices'). */
237
+ detailOpen = /* @__PURE__ */ new WeakMap();
238
+ /** Pointer inside the inspector — it stops refreshing under your hand. */
239
+ hovering = false;
240
+ constructor(engine, container, doc, statsSource, actions = []) {
196
241
  this.engine = engine;
197
242
  this.container = container;
198
243
  this.doc = doc;
199
244
  this.statsSource = statsSource;
245
+ this.actions = actions;
200
246
  this.menuButton = doc.createElement("div");
201
247
  this.menuButton.textContent = "☰ debug";
202
248
  applyStyle(this.menuButton, {
@@ -226,6 +272,7 @@ var DebugOverlay = class {
226
272
  }));
227
273
  this.cleanups.push(engine.sceneChanged.connect(() => {
228
274
  this.selected = null;
275
+ this.applyColliderScope();
229
276
  this.renderExplorer();
230
277
  this.renderInspector();
231
278
  }));
@@ -234,12 +281,34 @@ var DebugOverlay = class {
234
281
  if (this.frame % 30 !== 0) return;
235
282
  this.renderExplorer();
236
283
  this.renderStats();
237
- if (this.editing === 0) this.renderInspector();
284
+ if (this.editing === 0 && !this.hovering) this.renderInspector();
238
285
  }));
239
286
  }
240
287
  isOpen(id) {
288
+ if (id === "colliders") return this.colliderMode !== "off";
241
289
  return id === "stats" ? this.statsChip !== null : this.panels.has(id);
242
290
  }
291
+ /** How much of the physics world the wireframes show. */
292
+ colliderMode = "off";
293
+ /**
294
+ * Physics wireframes, from the menu instead of from the console. What you see
295
+ * is the shape data the solver holds — the fastest way to catch a collider
296
+ * that disagrees with the art it belongs to.
297
+ *
298
+ * `'selected'` narrows it to the Explorer's selection, because `'all'` on a
299
+ * real map is a terrain grid burying the one shape you came to look at.
300
+ */
301
+ setColliders(mode) {
302
+ this.colliderMode = mode;
303
+ this.applyColliderScope();
304
+ }
305
+ applyColliderScope() {
306
+ const mode = this.colliderMode;
307
+ for (const source of debugSources("2d").concat(debugSources("3d"))) {
308
+ source.debugDraw = mode !== "off";
309
+ source.debugScope = mode === "selected" ? this.selected : null;
310
+ }
311
+ }
243
312
  open(id) {
244
313
  if (id === "stats") {
245
314
  this.openStatsChip();
@@ -271,12 +340,21 @@ var DebugOverlay = class {
271
340
  }
272
341
  }[id]);
273
342
  panel.onClose = () => this.close(id);
343
+ if (id === "inspector") {
344
+ panel.body.addEventListener("pointerenter", () => {
345
+ this.hovering = true;
346
+ });
347
+ panel.body.addEventListener("pointerleave", () => {
348
+ this.hovering = false;
349
+ });
350
+ }
274
351
  this.panels.set(id, panel);
275
352
  if (id === "explorer") this.renderExplorer();
276
353
  if (id === "inspector") this.renderInspector();
277
354
  if (id === "logs") this.renderLogs();
278
355
  }
279
356
  close(id) {
357
+ if (id === "inspector") this.hovering = false;
280
358
  if (id === "stats") {
281
359
  this.statsChip?.remove();
282
360
  this.statsChip = null;
@@ -288,6 +366,14 @@ var DebugOverlay = class {
288
366
  this.panels.delete(id);
289
367
  }
290
368
  toggle(id) {
369
+ if (id === "colliders") {
370
+ this.setColliders({
371
+ off: "all",
372
+ all: "selected",
373
+ selected: "off"
374
+ }[this.colliderMode]);
375
+ return;
376
+ }
291
377
  if (this.isOpen(id)) this.close(id);
292
378
  else this.open(id);
293
379
  }
@@ -329,6 +415,7 @@ var DebugOverlay = class {
329
415
  }
330
416
  dispose() {
331
417
  this.engine.debugSelection = null;
418
+ this.setColliders("off");
332
419
  this.setConsoleCapture(false);
333
420
  for (const cleanup of this.cleanups) cleanup();
334
421
  for (const id of [...this.panels.keys()]) this.close(id);
@@ -337,6 +424,11 @@ var DebugOverlay = class {
337
424
  this.dropdown = null;
338
425
  this.menuButton.remove();
339
426
  }
427
+ /** `✓ Colliders · selected` — check for on, suffix for the collider mode. */
428
+ menuLabel(id, label) {
429
+ const suffix = id === "colliders" && this.colliderMode !== "off" ? ` · ${this.colliderMode}` : "";
430
+ return `${this.isOpen(id) ? "✓ " : ""}${label}${suffix}`;
431
+ }
340
432
  toggleDropdown() {
341
433
  if (this.dropdown) {
342
434
  this.dropdown.remove();
@@ -361,10 +453,11 @@ var DebugOverlay = class {
361
453
  ["explorer", "Explorer"],
362
454
  ["inspector", "Inspector"],
363
455
  ["logs", "Logs"],
364
- ["stats", "Stats"]
456
+ ["stats", "Stats"],
457
+ ["colliders", "Colliders"]
365
458
  ]) {
366
459
  const item = this.doc.createElement("div");
367
- item.textContent = `${this.isOpen(id) ? "✓ " : ""}${label}`;
460
+ item.textContent = this.menuLabel(id, label);
368
461
  applyStyle(item, {
369
462
  padding: "6px 14px",
370
463
  cursor: "pointer",
@@ -372,11 +465,32 @@ var DebugOverlay = class {
372
465
  });
373
466
  item.addEventListener("click", () => {
374
467
  this.toggle(id);
468
+ if (id === "colliders") {
469
+ item.textContent = this.menuLabel(id, label);
470
+ return;
471
+ }
375
472
  this.dropdown?.remove();
376
473
  this.dropdown = null;
377
474
  });
378
475
  menu.appendChild(item);
379
476
  }
477
+ for (const action of this.actions) {
478
+ const item = this.doc.createElement("div");
479
+ item.textContent = action.label;
480
+ applyStyle(item, {
481
+ padding: "6px 14px",
482
+ cursor: "pointer",
483
+ userSelect: "none",
484
+ borderTop: "1px solid rgba(255,255,255,0.14)",
485
+ color: "rgba(158,232,220,0.95)"
486
+ });
487
+ item.addEventListener("click", () => {
488
+ this.dropdown?.remove();
489
+ this.dropdown = null;
490
+ action.run();
491
+ });
492
+ menu.appendChild(item);
493
+ }
380
494
  this.container.appendChild(menu);
381
495
  this.dropdown = menu;
382
496
  }
@@ -418,9 +532,13 @@ var DebugOverlay = class {
418
532
  renderExplorer() {
419
533
  const panel = this.panels.get("explorer");
420
534
  if (!panel) return;
535
+ const scroll = panel.body.scrollTop;
421
536
  clear(panel.body);
422
537
  const root = this.engine.scene?.root;
423
- if (!root) return;
538
+ if (!root) {
539
+ panel.body.scrollTop = scroll;
540
+ return;
541
+ }
424
542
  const renderNode = (node, container) => {
425
543
  const ctor = node.constructor;
426
544
  const hasKids = node.children.length > 0;
@@ -473,6 +591,7 @@ var DebugOverlay = class {
473
591
  row.addEventListener("click", () => {
474
592
  this.selected = node;
475
593
  this.engine.debugSelection = node;
594
+ this.applyColliderScope();
476
595
  this.open("inspector");
477
596
  this.renderExplorer();
478
597
  this.renderInspector();
@@ -490,15 +609,18 @@ var DebugOverlay = class {
490
609
  }
491
610
  };
492
611
  renderNode(root, panel.body);
612
+ panel.body.scrollTop = scroll;
493
613
  }
494
614
  renderInspector() {
495
615
  const panel = this.panels.get("inspector");
496
616
  if (!panel) return;
617
+ const scroll = panel.body.scrollTop;
497
618
  clear(panel.body);
498
619
  let node = this.selected;
499
620
  if (node && node.tree === null) {
500
621
  this.selected = null;
501
622
  this.engine.debugSelection = null;
623
+ this.applyColliderScope();
502
624
  node = null;
503
625
  }
504
626
  if (!node) {
@@ -527,11 +649,37 @@ var DebugOverlay = class {
527
649
  panel.body.appendChild(groups);
528
650
  }
529
651
  const schema = mergeStaticProps(ctor);
530
- for (const key of Object.keys(schema)) this.renderPropRow(panel.body, node, key, schema[key]?.options);
531
- }
532
- renderPropRow(body, node, key, schemaOptions) {
533
652
  const record = node;
534
- const value = record[key];
653
+ for (const key of Object.keys(schema)) {
654
+ const def = schema[key];
655
+ this.renderValueRow(panel.body, node, key, {
656
+ read: () => record[key],
657
+ write: (next) => {
658
+ record[key] = next;
659
+ },
660
+ options: def?.options,
661
+ variants: def?.variants
662
+ });
663
+ }
664
+ panel.body.scrollTop = scroll;
665
+ }
666
+ /**
667
+ * One editable row. `read`/`write` are closures rather than a (node, key)
668
+ * pair so the SAME renderer serves a node prop and a key INSIDE an object
669
+ * prop — `collider.radius` gets a real number field instead of being a
670
+ * character in an ellipsised JSON dump.
671
+ */
672
+ renderValueRow(body, node, key, access) {
673
+ const value = access.read();
674
+ const schemaOptions = access.options;
675
+ const record = {
676
+ get [key]() {
677
+ return access.read();
678
+ },
679
+ set [key](next) {
680
+ access.write(next);
681
+ }
682
+ };
535
683
  const row = this.doc.createElement("div");
536
684
  applyStyle(row, {
537
685
  display: "flex",
@@ -551,6 +699,7 @@ var DebugOverlay = class {
551
699
  input.type = "number";
552
700
  input.value = String(current);
553
701
  applyStyle(input, inputStyle("70px"));
702
+ this.trackEditing(input);
554
703
  input.addEventListener("change", () => {
555
704
  const next = Number(input.value);
556
705
  if (Number.isFinite(next)) commit(next);
@@ -596,20 +745,94 @@ var DebugOverlay = class {
596
745
  record[key] = input.value;
597
746
  });
598
747
  row.appendChild(input);
599
- } else if (Array.isArray(value) && value.every((v) => typeof v === "number")) for (let i = 0; i < value.length; i++) row.appendChild(numberInput(value[i], (next) => {
748
+ } else if (Array.isArray(value) && value.length <= 8 && value.every((v) => typeof v === "number")) for (let i = 0; i < value.length; i++) row.appendChild(numberInput(value[i], (next) => {
600
749
  const arr = [...record[key]];
601
750
  arr[i] = next;
602
751
  record[key] = arr;
603
752
  }));
604
- else {
605
- const readout = this.doc.createElement("div");
606
- readout.textContent = JSON.stringify(jsonClone(value));
607
- applyStyle(readout, {
608
- opacity: "0.65",
609
- overflow: "hidden",
610
- textOverflow: "ellipsis"
753
+ else if (isPlainObject(value)) {
754
+ const nest = this.doc.createElement("div");
755
+ applyStyle(nest, {
756
+ marginLeft: "10px",
757
+ paddingLeft: "8px",
758
+ borderLeft: "1px solid rgba(255,255,255,0.14)"
611
759
  });
612
- row.appendChild(readout);
760
+ const tag = access.variants?.tag;
761
+ const keys = Object.keys(value);
762
+ if (tag && !keys.includes(tag)) keys.unshift(tag);
763
+ const basePath = access.path ?? key;
764
+ for (const k of keys) {
765
+ const isTag = k === tag;
766
+ this.renderValueRow(nest, node, k, {
767
+ path: `${basePath}.${k}`,
768
+ read: () => access.read()[k] ?? (isTag ? "" : null),
769
+ write: (next) => {
770
+ if (isTag && access.variants) {
771
+ const preset = access.variants.byTag[String(next)];
772
+ if (preset !== void 0) {
773
+ access.write(jsonClone(preset));
774
+ return;
775
+ }
776
+ }
777
+ access.write({
778
+ ...access.read(),
779
+ [k]: next
780
+ });
781
+ },
782
+ options: isTag && access.variants ? Object.keys(access.variants.byTag) : void 0
783
+ });
784
+ }
785
+ body.appendChild(row);
786
+ body.appendChild(nest);
787
+ return;
788
+ } else {
789
+ const json = JSON.stringify(jsonClone(value));
790
+ if (json.length <= DETAIL_INLINE_MAX) {
791
+ const readout = this.doc.createElement("div");
792
+ readout.textContent = json;
793
+ applyStyle(readout, {
794
+ opacity: "0.65",
795
+ whiteSpace: "pre-wrap",
796
+ wordBreak: "break-all"
797
+ });
798
+ row.appendChild(readout);
799
+ } else {
800
+ const path = access.path ?? key;
801
+ const open = this.detailOpen.get(node)?.has(path) ?? false;
802
+ const toggle = this.doc.createElement("div");
803
+ toggle.textContent = `${open ? "▾" : "▸"} ${summarizeValue(value)}`;
804
+ applyStyle(toggle, {
805
+ opacity: "0.75",
806
+ cursor: "pointer",
807
+ userSelect: "none"
808
+ });
809
+ toggle.addEventListener("click", () => {
810
+ let set = this.detailOpen.get(node);
811
+ if (!set) {
812
+ set = /* @__PURE__ */ new Set();
813
+ this.detailOpen.set(node, set);
814
+ }
815
+ if (open) set.delete(path);
816
+ else set.add(path);
817
+ this.renderInspector();
818
+ });
819
+ row.appendChild(toggle);
820
+ body.appendChild(row);
821
+ if (open) {
822
+ const detail = this.doc.createElement("div");
823
+ detail.textContent = detailText(value, json);
824
+ applyStyle(detail, {
825
+ opacity: "0.6",
826
+ whiteSpace: "pre-wrap",
827
+ wordBreak: "break-all",
828
+ marginLeft: "10px",
829
+ paddingLeft: "8px",
830
+ borderLeft: "1px solid rgba(255,255,255,0.14)"
831
+ });
832
+ body.appendChild(detail);
833
+ }
834
+ return;
835
+ }
613
836
  }
614
837
  body.appendChild(row);
615
838
  }
@@ -0,0 +1,59 @@
1
+ import { s as JsonObject } from "./schema-CcoWb32N.js";
2
+
3
+ //#region src/core/editor-switch.d.ts
4
+ /** Where a camera is and what it looks at, in world space. */
5
+ interface CameraPose {
6
+ position: [number, number, number];
7
+ target: [number, number, number];
8
+ }
9
+ /** What the editor module needs to know to take over the page. */
10
+ interface EditorOpenOptions {
11
+ /** The authored scene to edit. */
12
+ scene: JsonObject;
13
+ /** Start the edit view here (continuity out of the running game). */
14
+ camera?: CameraPose;
15
+ /** Called when the editor hands the page back — boot the game from `working`. */
16
+ play(working: JsonObject, camera?: CameraPose): void | Promise<void>;
17
+ /** Called when the user leaves the editor without playing. */
18
+ exit?(): void | Promise<void>;
19
+ /** Persist edits (from `editor.save`). Omitted: no save button. */
20
+ save?(working: JsonObject): void | Promise<void>;
21
+ /** Asset-library search (from `editor.library`). Omitted: no 📚 buttons. */
22
+ library?: LibrarySearch;
23
+ }
24
+ /**
25
+ * How a page opens its editor.
26
+ *
27
+ * Default: `import('incanto/editor')`, so `editor: true` is all a game needs.
28
+ * Apps that alias the engine to source (the examples, the playground) map that
29
+ * specifier themselves, or pass `open` and skip the resolution entirely.
30
+ */
31
+ interface EditorSwitchOptions {
32
+ open?(opts: EditorOpenOptions): void | Promise<void>;
33
+ /** Persist edits. Omitted: the editor's save button is hidden. */
34
+ save?(working: JsonObject): void | Promise<void>;
35
+ /**
36
+ * Search the agent8 asset library from inside the game's editor.
37
+ *
38
+ * `true` means "my dev server serves `/api/library`" — add
39
+ * `incantoLibrary()` from `incanto/vite` and this is the whole opt-in. A
40
+ * function is your own transport. Omitted: the 📚 buttons are not rendered,
41
+ * because a page with no catalog should show no button rather than one that
42
+ * fails when pressed.
43
+ */
44
+ library?: true | LibrarySearch;
45
+ }
46
+ /** Search the asset catalog: the shape the editor's browser consumes. */
47
+ type LibrarySearch = (query: {
48
+ tags: string[];
49
+ keyword?: string;
50
+ page?: number;
51
+ limit?: number;
52
+ }) => Promise<{
53
+ items: unknown[];
54
+ page: number;
55
+ hasNext: boolean;
56
+ total?: number;
57
+ }>;
58
+ //#endregion
59
+ export { EditorSwitchOptions as t };
@@ -0,0 +1,49 @@
1
+ // Generated by packages/editor/scripts/emit-types.mjs — do not edit.
2
+ type Json = Record<string, unknown>;
3
+
4
+ export interface EditorMeta {
5
+ version: string;
6
+ mode: 'single' | 'project';
7
+ root: string;
8
+ input: string | null;
9
+ output: string | null;
10
+ }
11
+
12
+ export interface SceneEntry {
13
+ rel: string;
14
+ abs: string;
15
+ }
16
+
17
+ /** Where a mounted editor reads and writes scenes. */
18
+ export interface EditorApi {
19
+ meta(): Promise<EditorMeta>;
20
+ loadScene(file?: string): Promise<Json>;
21
+ saveScene?(json: Json, file?: string): Promise<void>;
22
+ scenes?(): Promise<SceneEntry[]>;
23
+ createScene?(path: string): Promise<SceneEntry>;
24
+ }
25
+
26
+ export type PlayChoice = 'preview' | 'debug';
27
+
28
+ export interface MountEditorOptions {
29
+ host?: HTMLElement;
30
+ api?: EditorApi;
31
+ camera?: { position: readonly number[]; target: readonly number[] };
32
+ onPlay?(choice: PlayChoice, working: Json): void | Promise<void>;
33
+ onExit?(): void | Promise<void>;
34
+ }
35
+
36
+ export interface EditorHandle {
37
+ dispose(): void;
38
+ working(): Json;
39
+ camera(): { position: [number, number, number]; target: [number, number, number] };
40
+ }
41
+
42
+ /**
43
+ * Mount the scene editor over the page. It OWNS the window while it is up —
44
+ * dispose() puts the page back exactly as it was.
45
+ */
46
+ export declare function mountEditor(opts?: MountEditorOptions): Promise<EditorHandle>;
47
+
48
+ /** The `incanto-editor` server's /api routes, as an EditorApi. */
49
+ export declare const httpEditorApi: EditorApi;