incanto 0.28.0 → 0.29.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 +118 -1
- package/dist/2d.d.ts +38 -5
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +319 -15
- package/dist/3d.js +5 -5
- package/dist/{behavior-CPibUfnH.d.ts → behavior-BAc0erXF.d.ts} +21 -0
- package/dist/{create-game-BRgWpNsa.js → create-game-CNKXGfpr.js} +270 -47
- package/dist/{create-game-71TJjW1T.js → create-game-CbuLWorm.js} +53 -14
- 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-switch-CAKlJMIY.js +161 -0
- package/dist/editor.d.ts +49 -0
- package/dist/editor.js +8439 -0
- package/dist/{gameplay-DEG-TP7D.js → gameplay-L05WgWd1.js} +207 -37
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +22 -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-CCVTrKOd.js} +62 -2
- package/dist/{physics-3d--y5clE2j.js → physics-3d-BZZLtwJu.js} +439 -8
- 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-C44aSduO.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/test.d.ts +35 -3
- package/dist/test.js +104 -10
- package/dist/vite.d.ts +53 -1
- package/dist/vite.js +94 -2
- package/editor/assets/{agent8-CojUfCXN.js → agent8-DEVkEa3d.js} +1 -1
- package/editor/assets/debug-zGAtpDF0.js +2 -0
- package/editor/assets/index-Bx4UtWYY.js +10586 -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 +151 -0
- package/skills/incanto-environment.md +65 -1
- package/skills/incanto-node-reference.md +33 -1
- package/skills/incanto-physics-and-input.md +45 -6
- package/skills/incanto-verifying-your-game.md +51 -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/tps-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/editor/assets/index-D6RQgROR.js +0 -8330
- package/editor/assets/index-D8QvwvOm.css +0 -1
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-CZmOYjL2.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 };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//#region src/core/editor-switch.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cover the page while it changes what it is.
|
|
4
|
+
*
|
|
5
|
+
* A mode switch tears down a whole game and builds a whole IDE in its place;
|
|
6
|
+
* between those two the window shows a dead canvas, a flash of white body, and
|
|
7
|
+
* then a fully-formed editor. Every one of those frames is honest and none of
|
|
8
|
+
* them is pleasant. A veil makes the crossing read as one movement: fade to the
|
|
9
|
+
* editor's own background, do the work behind it, fade back.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately CSS-only and dependency-free — it has to work in a game page
|
|
12
|
+
* whose styles we know nothing about, so it sits at the top of the stacking
|
|
13
|
+
* order, eats pointer events (nobody clicks a UI that is being replaced), and
|
|
14
|
+
* removes itself completely. In a headless boot there is no document and this
|
|
15
|
+
* is a pair of no-ops.
|
|
16
|
+
*/
|
|
17
|
+
const VEIL_MS = 160;
|
|
18
|
+
function raiseVeil(label = "", background = "#0b0d14") {
|
|
19
|
+
if (typeof document === "undefined") return { lift: () => {} };
|
|
20
|
+
const el = document.createElement("div");
|
|
21
|
+
el.dataset.incantoSwitch = "";
|
|
22
|
+
el.style.cssText = `position:fixed;inset:0;z-index:2147483646;background:${background};display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity ${VEIL_MS}ms ease;pointer-events:auto;`;
|
|
23
|
+
if (label) {
|
|
24
|
+
const text = document.createElement("div");
|
|
25
|
+
text.textContent = label;
|
|
26
|
+
text.style.cssText = "font:13px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#5d6680;letter-spacing:0.04em;";
|
|
27
|
+
el.appendChild(text);
|
|
28
|
+
}
|
|
29
|
+
document.body.appendChild(el);
|
|
30
|
+
requestAnimationFrame(() => {
|
|
31
|
+
el.style.opacity = "1";
|
|
32
|
+
});
|
|
33
|
+
let lifted = false;
|
|
34
|
+
return { lift() {
|
|
35
|
+
if (lifted) return;
|
|
36
|
+
lifted = true;
|
|
37
|
+
requestAnimationFrame(() => {
|
|
38
|
+
el.style.opacity = "0";
|
|
39
|
+
setTimeout(() => el.remove(), 200);
|
|
40
|
+
});
|
|
41
|
+
} };
|
|
42
|
+
}
|
|
43
|
+
/** Hold the veil up for at least one paint so the fade is visible at all. */
|
|
44
|
+
async function settle(ms = VEIL_MS) {
|
|
45
|
+
if (typeof requestAnimationFrame !== "function") return;
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The `/api/library` route `incanto/vite`'s `incantoLibrary()` (and the
|
|
50
|
+
* `incanto-editor` server) serve. Used when a game passes `library: true`.
|
|
51
|
+
*/
|
|
52
|
+
function devServerLibrary(token) {
|
|
53
|
+
return async (query) => {
|
|
54
|
+
const params = new URLSearchParams({
|
|
55
|
+
tags: query.tags.join(","),
|
|
56
|
+
page: String(query.page ?? 1),
|
|
57
|
+
limit: String(query.limit ?? 24)
|
|
58
|
+
});
|
|
59
|
+
if (query.keyword?.trim()) params.set("keyword", query.keyword.trim());
|
|
60
|
+
const res = await fetch(`/api/library?${params}`, { headers: token ? { "x-incanto-v8-token": token } : {} });
|
|
61
|
+
if (res.status === 401) {
|
|
62
|
+
const body = await res.json().catch(() => ({}));
|
|
63
|
+
const error = new Error(body.error ?? "the asset library needs a Verse8 access token");
|
|
64
|
+
error.needsToken = true;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok) throw new Error(`asset library: HTTP ${res.status}`);
|
|
68
|
+
return await res.json();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The default opener: pull `incanto/editor` in on demand and mount it.
|
|
73
|
+
*
|
|
74
|
+
* It is a dynamic import so the editor — an IDE's worth of UI — never enters a
|
|
75
|
+
* production bundle. A build that cannot resolve it fails HERE, with the two
|
|
76
|
+
* ways to fix it, rather than silently offering a menu item that does nothing.
|
|
77
|
+
*/
|
|
78
|
+
async function openBundledEditor(opts) {
|
|
79
|
+
let mod;
|
|
80
|
+
try {
|
|
81
|
+
mod = await import("incanto/editor");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new Error(`createGame3D editor: could not load 'incanto/editor'. If this app aliases incanto to the engine SOURCE (the examples and playground do), add an alias for 'incanto/editor' → packages/editor/src/main.ts — or pass editor: { open } and load it yourself. (${error instanceof Error ? error.message : String(error)})`);
|
|
84
|
+
}
|
|
85
|
+
let handle = null;
|
|
86
|
+
handle = await mod.mountEditor({
|
|
87
|
+
...opts.camera ? { camera: opts.camera } : {},
|
|
88
|
+
api: {
|
|
89
|
+
meta: async () => ({
|
|
90
|
+
version: "live",
|
|
91
|
+
mode: "single",
|
|
92
|
+
root: "",
|
|
93
|
+
input: null,
|
|
94
|
+
output: null
|
|
95
|
+
}),
|
|
96
|
+
loadScene: async () => opts.scene,
|
|
97
|
+
...opts.save ? { saveScene: async (json) => void opts.save?.(json) } : {},
|
|
98
|
+
...opts.library ? { library: opts.library } : {}
|
|
99
|
+
},
|
|
100
|
+
onPlay: (choice, working) => {
|
|
101
|
+
if (choice !== "debug") return void 0;
|
|
102
|
+
const camera = handle?.camera();
|
|
103
|
+
return crossFade(async () => {
|
|
104
|
+
handle?.dispose();
|
|
105
|
+
handle = null;
|
|
106
|
+
await opts.play(working, camera);
|
|
107
|
+
}, "starting the game…");
|
|
108
|
+
},
|
|
109
|
+
...opts.exit ? { onExit: () => crossFade(async () => {
|
|
110
|
+
handle?.dispose();
|
|
111
|
+
handle = null;
|
|
112
|
+
await opts.exit?.();
|
|
113
|
+
}, "back to the game…") } : {}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Run a mode change behind the veil: cover, swap, reveal.
|
|
118
|
+
*
|
|
119
|
+
* The reveal waits for the new page to have PAINTED (two frames — one for the
|
|
120
|
+
* layout the swap just produced, one for it to reach the screen), otherwise the
|
|
121
|
+
* fade-out races the boot and shows the very flash it exists to hide.
|
|
122
|
+
*/
|
|
123
|
+
async function crossFade(swap, label = "") {
|
|
124
|
+
const veil = raiseVeil(label);
|
|
125
|
+
await settle();
|
|
126
|
+
try {
|
|
127
|
+
await swap();
|
|
128
|
+
} finally {
|
|
129
|
+
await settle(0);
|
|
130
|
+
veil.lift();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The camera the player is actually looking through, as a pose.
|
|
135
|
+
*
|
|
136
|
+
* Reads the renderer's LAST rendered camera rather than the scene's Camera3D:
|
|
137
|
+
* a third-person rig, a cutscene camera and a debug free-fly all end up in the
|
|
138
|
+
* same place, and it is the view on screen — the thing continuity is about.
|
|
139
|
+
*/
|
|
140
|
+
function poseFromRenderer(renderer, fallbackDistance = 8) {
|
|
141
|
+
const basis = renderer.cameraBasis;
|
|
142
|
+
const posOf = renderer.lastCameraPosition;
|
|
143
|
+
if (typeof basis !== "function" || typeof posOf !== "function") return void 0;
|
|
144
|
+
const eye = posOf.call(renderer);
|
|
145
|
+
if (!eye) return void 0;
|
|
146
|
+
const { forward } = basis.call(renderer);
|
|
147
|
+
return {
|
|
148
|
+
position: [
|
|
149
|
+
eye.x,
|
|
150
|
+
eye.y,
|
|
151
|
+
eye.z
|
|
152
|
+
],
|
|
153
|
+
target: [
|
|
154
|
+
eye.x + forward.x * fallbackDistance,
|
|
155
|
+
eye.y + forward.y * fallbackDistance,
|
|
156
|
+
eye.z + forward.z * fallbackDistance
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { poseFromRenderer as i, devServerLibrary as n, openBundledEditor as r, crossFade 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;
|