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.
- package/README.md +44 -0
- package/bin/incanto-editor.mjs +219 -29
- package/dist/2d.d.ts +54 -6
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +353 -16
- package/dist/3d.js +5 -5
- package/dist/{behavior-CPibUfnH.d.ts → behavior-BAc0erXF.d.ts} +21 -0
- package/dist/{create-game-71TJjW1T.js → create-game-5z_QVtLx.js} +59 -16
- package/dist/{create-game-BRgWpNsa.js → create-game-DuBTv2zI.js} +276 -49
- package/dist/debug-draw-BM3DsvtT.js +18 -0
- package/dist/debug.d.ts +51 -6
- package/dist/debug.js +242 -19
- package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
- package/dist/editor.d.ts +49 -0
- package/dist/editor.js +8554 -0
- package/dist/{gameplay-DEG-TP7D.js → gameplay-Cfr6aFZ1.js} +219 -40
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +31 -3
- package/dist/index.js +2 -2
- package/dist/{loader-DILt9PGC.d.ts → loader-B242FF6N.d.ts} +1 -1
- package/dist/net.d.ts +1 -1
- package/dist/net.js +2 -2
- package/dist/{pathfinding-RWYkNKx9.d.ts → pathfinding-BwD974Ss.d.ts} +1 -1
- package/dist/{physics-2d-DiVFFlH3.js → physics-2d-3kOQCtgd.js} +88 -3
- package/dist/{physics-3d--y5clE2j.js → physics-3d-CeRH-Ff_.js} +475 -9
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-nObreUQR.js → register-BTg0EM7s.js} +35 -3
- package/dist/{register-6R75AC7-.js → register-DJ0SByQg.js} +5550 -5073
- package/dist/{register-CvpSUU3O.js → register-DWcWq4QG.js} +22 -2
- package/dist/{register-BFFE1Mh1.js → register-MelqEdza.js} +1 -1
- package/dist/teardown-ByzfDPyu.js +240 -0
- package/dist/test.d.ts +35 -3
- package/dist/test.js +104 -10
- package/dist/vite.d.ts +139 -1
- package/dist/vite.js +355 -4
- package/editor/assets/{agent8-CojUfCXN.js → agent8-CAp0i5qn.js} +1 -1
- package/editor/assets/debug-BoEYfbqK.js +2 -0
- package/editor/assets/index-BO6WU8by.js +10696 -0
- package/editor/index.html +3 -157
- package/package.json +3 -2
- package/schemas/scene.schema.json +102 -4
- package/skills/incanto-3d-models.md +38 -0
- package/skills/incanto-assets.md +13 -0
- package/skills/incanto-building-3d-games.md +83 -6
- package/skills/incanto-editor.md +212 -8
- package/skills/incanto-environment.md +65 -1
- package/skills/incanto-node-reference.md +33 -1
- package/skills/incanto-physics-and-input.md +60 -6
- package/skills/incanto-verifying-your-game.md +57 -2
- package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
- package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/vite.config.ts +7 -0
- package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/vite.config.ts +7 -0
- package/dist/debug-draw-CZmOYjL2.js +0 -13
- package/editor/assets/index-D6RQgROR.js +0 -8330
- 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-
|
|
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
|
-
|
|
45
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/editor.d.ts
ADDED
|
@@ -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;
|