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
@@ -1,12 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { _ as registerBehavior, n as loadScene, o as computeViewport, s as resolveViewport } from "./loader-BZqOKfI2.js";
3
- import { l as AudioPlayer, u as Engine } from "./register-nObreUQR.js";
3
+ import { l as AudioPlayer, u as Engine } from "./register-BTg0EM7s.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
6
- import { n as registerGameplayBehaviors } from "./gameplay-DEG-TP7D.js";
7
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-CvpSUU3O.js";
8
- import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
9
- import { n as enablePhysics2D } from "./physics-2d-DiVFFlH3.js";
6
+ import { i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-ByzfDPyu.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-Cfr6aFZ1.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DWcWq4QG.js";
9
+ import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-3kOQCtgd.js";
10
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
11
12
  //#region src/2d/assets.ts
12
13
  /**
@@ -266,6 +267,7 @@ var Renderer2D = class {
266
267
  canvas: opts.canvas,
267
268
  antialias: rendering.antialias
268
269
  });
270
+ this.webgl.info.autoReset = false;
269
271
  this.webgl.setPixelRatio(rendering.pixelRatio);
270
272
  this.webgl.info.autoReset = false;
271
273
  this.debugLines = new LineSegments(new BufferGeometry(), new LineBasicMaterial({
@@ -308,7 +310,7 @@ var Renderer2D = class {
308
310
  }
309
311
  syncDebugLines() {
310
312
  let vertices = null;
311
- for (const source of debugSources("2d")) {
313
+ for (const source of debugSources("2d", this.engine)) {
312
314
  vertices = source.debugLines();
313
315
  if (vertices) break;
314
316
  }
@@ -386,6 +388,7 @@ var Renderer2D = class {
386
388
  }
387
389
  frame(this.worldCam, center.x, center.y, uiW / zoom, uiH / zoom);
388
390
  frame(this.uiCam, uiW / 2, uiH / 2, uiW, uiH);
391
+ this.webgl.info.reset();
389
392
  this.webgl.autoClear = true;
390
393
  this.webgl.render(this.worldScene, this.worldCam);
391
394
  this.webgl.autoClear = false;
@@ -405,8 +408,8 @@ var Renderer2D = class {
405
408
  return { ...this.lastView };
406
409
  }
407
410
  /**
408
- * GPU counters for the LAST rendered frame (world + UI passes combined),
409
- * straight from three's per-frame-maintained `renderer.info`.
411
+ * GPU counters for the LAST rendered frame world + UI passes combined (see
412
+ * the `info.reset()` in the render pass), plus GPU memory.
410
413
  */
411
414
  stats() {
412
415
  const info = this.webgl.info;
@@ -517,7 +520,8 @@ async function createGame2D(opts) {
517
520
  fixedHz: opts.fixedHz,
518
521
  ...opts._scheduler ? { scheduler: opts._scheduler } : {}
519
522
  });
520
- const scene = loadScene(cloneSceneJson(opts.scene), {
523
+ const sourceJson = cloneSceneJson(opts.scene);
524
+ const scene = loadScene(cloneSceneJson(sourceJson), {
521
525
  resolveScene: opts.resolveScene,
522
526
  engine
523
527
  });
@@ -553,22 +557,65 @@ async function createGame2D(opts) {
553
557
  ...opts.pixelRatio !== void 0 ? { pixelRatio: opts.pixelRatio } : {},
554
558
  ...opts.antialias !== void 0 ? { antialias: opts.antialias } : {}
555
559
  });
556
- if (opts.debug ?? false) {
560
+ const wantDebug = opts.debug ?? false;
561
+ const wantEditor = opts.editor ?? wantDebug;
562
+ const editorSwitch = wantEditor === true ? {} : wantEditor ? wantEditor : null;
563
+ const editorAction = {
564
+ label: "✎ edit this scene",
565
+ run: async () => {
566
+ if (!editorSwitch) return;
567
+ const open = editorSwitch.open ?? openBundledEditor;
568
+ const reboot = async (working) => {
569
+ await createGame2D({
570
+ ...opts,
571
+ scene: working
572
+ });
573
+ };
574
+ await crossFade(async () => {
575
+ disposeGame();
576
+ try {
577
+ await open({
578
+ scene: sourceJson,
579
+ play: reboot,
580
+ exit: () => reboot(sourceJson),
581
+ ...editorSwitch.save ? { save: editorSwitch.save } : {},
582
+ ...editorSwitch.library ? { library: editorSwitch.library === true ? devServerLibrary() : editorSwitch.library } : {}
583
+ });
584
+ } catch (error) {
585
+ console.error("[incanto] could not open the editor:", error);
586
+ await reboot(sourceJson);
587
+ }
588
+ }, "opening the editor…");
589
+ }
590
+ };
591
+ if (wantDebug) {
557
592
  const host = opts.touchContainer ?? opts.canvas.parentElement ?? (typeof document !== "undefined" ? document.body : null);
558
593
  if (host) {
559
594
  const { attachDebugOverlay } = await import("./debug.js");
560
595
  const overlay = attachDebugOverlay(engine, {
561
596
  container: host,
562
597
  statsSource: () => renderer.stats?.() ?? {},
598
+ ...editorSwitch ? { actions: [editorAction] } : {},
563
599
  ...opts._debugDoc ? { doc: opts._debugDoc } : {}
564
600
  });
565
601
  if (overlay) cleanups.push(() => overlay.dispose());
566
602
  }
567
603
  }
568
604
  engine.start();
605
+ function disposeGame() {
606
+ teardown([
607
+ physics ? ["physics", () => physics?.dispose()] : null,
608
+ ["app cleanups", () => {
609
+ for (const cleanup of cleanups) cleanup();
610
+ }],
611
+ ["engine", () => engine.dispose()],
612
+ ["renderer", () => renderer.dispose()]
613
+ ]);
614
+ }
569
615
  return {
570
616
  engine,
571
617
  scene,
618
+ sourceJson,
572
619
  renderer,
573
620
  physics,
574
621
  stats() {
@@ -581,12 +628,8 @@ async function createGame2D(opts) {
581
628
  ...renderer.stats?.()
582
629
  };
583
630
  },
584
- dispose() {
585
- renderer.dispose();
586
- physics?.dispose();
587
- for (const cleanup of cleanups) cleanup();
588
- engine.dispose();
589
- }
631
+ openEditor: editorSwitch ? editorAction.run : null,
632
+ dispose: disposeGame
590
633
  };
591
634
  }
592
635
  function cloneSceneJson(json) {
@@ -1,12 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { _ as registerBehavior, n as loadScene } from "./loader-BZqOKfI2.js";
3
- import { l as AudioPlayer, u as Engine } from "./register-nObreUQR.js";
3
+ import { l as AudioPlayer, u as Engine } from "./register-BTg0EM7s.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
6
- import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-DEG-TP7D.js";
7
- import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
8
- import { S as Camera3D, f as DirectionalLight3D, t as registerNodes3D, u as ModelInstance3D } from "./register-6R75AC7-.js";
9
- import { n as enablePhysics3D } from "./physics-3d--y5clE2j.js";
6
+ import { a as poseFromRenderer, i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-ByzfDPyu.js";
7
+ import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-Cfr6aFZ1.js";
8
+ import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
9
+ import { T as Camera3D, _ as DirectionalLight3D, h as ModelInstance3D, t as registerNodes3D } from "./register-DJ0SByQg.js";
10
+ import { n as enablePhysics3D } from "./physics-3d-CeRH-Ff_.js";
10
11
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
11
12
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
12
13
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -1163,49 +1164,132 @@ function parseIblIntensity(value) {
1163
1164
  //#endregion
1164
1165
  //#region src/3d/selection-outline.ts
1165
1166
  /**
1166
- * Where IS the node the dev-overlay selected? — world-space outline segments
1167
- * for the renderer's highlight pass. A renderable node gets its actual
1168
- * bounding box (children included); a node with no visual extent gets a
1169
- * small marker cube at its world position so it is still findable.
1167
+ * Where IS the node the dev-overlay selected? — outline segments for the
1168
+ * renderer's highlight pass. A renderable node gets its actual bounding box
1169
+ * (children included); a node with no visual extent gets a small marker cube at
1170
+ * its world position so it is still findable.
1171
+ *
1172
+ * The box is ORIENTED — measured in the node's own frame, then turned by its
1173
+ * world matrix. A world-axis-aligned box lies about anything rotated: a 14 m
1174
+ * footbridge deck turned 143° would be wrapped in a 12.5 × 10.2 m rectangle,
1175
+ * three times its real footprint, and the outline would sit nowhere near the
1176
+ * plank it is supposed to be pointing at.
1170
1177
  */
1171
1178
  const box = new Box3();
1172
- const size = new Vector3();
1179
+ const childBox = new Box3();
1180
+ const toLocal = new Matrix4();
1181
+ const childMat = new Matrix4();
1182
+ const corner = new Vector3();
1183
+ /** Reused every frame — the outline refreshes while a node stays selected. */
1184
+ const points = Array.from({ length: 8 }, () => new Vector3());
1173
1185
  const MARKER_HALF = .4;
1174
1186
  /** 12 box edges = 24 points × xyz. */
1175
1187
  const OUT = new Float32Array(72);
1188
+ /** The eight corners in the order the edge list below indexes them. */
1189
+ const CORNERS = [
1190
+ [
1191
+ 0,
1192
+ 0,
1193
+ 0
1194
+ ],
1195
+ [
1196
+ 1,
1197
+ 0,
1198
+ 0
1199
+ ],
1200
+ [
1201
+ 1,
1202
+ 0,
1203
+ 1
1204
+ ],
1205
+ [
1206
+ 0,
1207
+ 0,
1208
+ 1
1209
+ ],
1210
+ [
1211
+ 0,
1212
+ 1,
1213
+ 0
1214
+ ],
1215
+ [
1216
+ 1,
1217
+ 1,
1218
+ 0
1219
+ ],
1220
+ [
1221
+ 1,
1222
+ 1,
1223
+ 1
1224
+ ],
1225
+ [
1226
+ 0,
1227
+ 1,
1228
+ 1
1229
+ ]
1230
+ ];
1231
+ const EDGES = [
1232
+ [0, 1],
1233
+ [1, 2],
1234
+ [2, 3],
1235
+ [3, 0],
1236
+ [4, 5],
1237
+ [5, 6],
1238
+ [6, 7],
1239
+ [7, 4],
1240
+ [0, 4],
1241
+ [1, 5],
1242
+ [2, 6],
1243
+ [3, 7]
1244
+ ];
1245
+ /** The subtree's extent in `root`'s own frame — no ancestor rotation baked in. */
1246
+ function localBox(root, out) {
1247
+ out.makeEmpty();
1248
+ toLocal.copy(root.matrixWorld).invert();
1249
+ root.traverse((child) => {
1250
+ const mesh = child;
1251
+ let source;
1252
+ if (mesh.isInstancedMesh) {
1253
+ if (!mesh.boundingBox) mesh.computeBoundingBox?.();
1254
+ source = mesh.boundingBox;
1255
+ } else if (mesh.geometry) {
1256
+ if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
1257
+ source = mesh.geometry.boundingBox;
1258
+ }
1259
+ if (!source) return;
1260
+ childBox.copy(source).applyMatrix4(childMat.multiplyMatrices(toLocal, child.matrixWorld));
1261
+ out.union(childBox);
1262
+ });
1263
+ }
1176
1264
  function selectionOutline3D(node) {
1177
1265
  if (!(node instanceof Node3D)) return null;
1178
1266
  const obj = node._ensureObject3D();
1179
1267
  obj.updateWorldMatrix(true, true);
1180
- box.setFromObject(obj);
1181
- box.getSize(size);
1182
- if (!Number.isFinite(size.x) || size.x === 0 && size.y === 0 && size.z === 0) {
1183
- const p = new Vector3().setFromMatrixPosition(obj.matrixWorld);
1184
- box.min.set(p.x - MARKER_HALF, p.y - MARKER_HALF, p.z - MARKER_HALF);
1185
- box.max.set(p.x + MARKER_HALF, p.y + MARKER_HALF, p.z + MARKER_HALF);
1268
+ localBox(obj, box);
1269
+ let oriented = true;
1270
+ if (box.isEmpty()) {
1271
+ oriented = false;
1272
+ corner.setFromMatrixPosition(obj.matrixWorld);
1273
+ box.min.set(corner.x - MARKER_HALF, corner.y - MARKER_HALF, corner.z - MARKER_HALF);
1274
+ box.max.set(corner.x + MARKER_HALF, corner.y + MARKER_HALF, corner.z + MARKER_HALF);
1186
1275
  } else box.expandByScalar(.02);
1187
1276
  const { min, max } = box;
1277
+ for (let c = 0; c < 8; c++) {
1278
+ const [cx, cy, cz] = CORNERS[c];
1279
+ const p = points[c].set(cx ? max.x : min.x, cy ? max.y : min.y, cz ? max.z : min.z);
1280
+ if (oriented) p.applyMatrix4(obj.matrixWorld);
1281
+ }
1188
1282
  let i = 0;
1189
- const seg = (x1, y1, z1, x2, y2, z2) => {
1190
- OUT[i++] = x1;
1191
- OUT[i++] = y1;
1192
- OUT[i++] = z1;
1193
- OUT[i++] = x2;
1194
- OUT[i++] = y2;
1195
- OUT[i++] = z2;
1196
- };
1197
- seg(min.x, min.y, min.z, max.x, min.y, min.z);
1198
- seg(max.x, min.y, min.z, max.x, min.y, max.z);
1199
- seg(max.x, min.y, max.z, min.x, min.y, max.z);
1200
- seg(min.x, min.y, max.z, min.x, min.y, min.z);
1201
- seg(min.x, max.y, min.z, max.x, max.y, min.z);
1202
- seg(max.x, max.y, min.z, max.x, max.y, max.z);
1203
- seg(max.x, max.y, max.z, min.x, max.y, max.z);
1204
- seg(min.x, max.y, max.z, min.x, max.y, min.z);
1205
- seg(min.x, min.y, min.z, min.x, max.y, min.z);
1206
- seg(max.x, min.y, min.z, max.x, max.y, min.z);
1207
- seg(max.x, min.y, max.z, max.x, max.y, max.z);
1208
- seg(min.x, min.y, max.z, min.x, max.y, max.z);
1283
+ for (const [a, b] of EDGES) {
1284
+ const p = points[a];
1285
+ const q = points[b];
1286
+ OUT[i++] = p.x;
1287
+ OUT[i++] = p.y;
1288
+ OUT[i++] = p.z;
1289
+ OUT[i++] = q.x;
1290
+ OUT[i++] = q.y;
1291
+ OUT[i++] = q.z;
1292
+ }
1209
1293
  return OUT;
1210
1294
  }
1211
1295
  //#endregion
@@ -1420,6 +1504,11 @@ var Renderer3D = class {
1420
1504
  loadedAssetScenes = /* @__PURE__ */ new WeakSet();
1421
1505
  /** The scene whose GPU programs have been pre-compiled (warm-up, see render). */
1422
1506
  compiledScene = null;
1507
+ /** The camera the last frame drew with — what warmUp compiles against. */
1508
+ warmCamera = null;
1509
+ /** Set while a caller intends to run {@link warmUp} instead: the first frame
1510
+ * then compiles only what it actually draws, and the rest goes async. */
1511
+ deferCompile = false;
1423
1512
  /** Reused per-frame walk scratch (instance-scoped — never a module global). */
1424
1513
  syncScratch = createSyncScratch();
1425
1514
  /** Editors set this: keep syncing `static: true` subtrees every frame. */
@@ -1465,6 +1554,7 @@ var Renderer3D = class {
1465
1554
  canvas: opts.canvas,
1466
1555
  antialias: rendering.antialias
1467
1556
  });
1557
+ this.webgl.info.autoReset = false;
1468
1558
  this.basePixelRatio = rendering.pixelRatio;
1469
1559
  this.webgl.setPixelRatio(rendering.pixelRatio);
1470
1560
  this.adaptive = opts.adaptiveResolution === false ? null : new AdaptiveResolution();
@@ -1511,7 +1601,7 @@ var Renderer3D = class {
1511
1601
  }
1512
1602
  syncDebugLines() {
1513
1603
  let vertices = null;
1514
- for (const source of debugSources("3d")) {
1604
+ for (const source of debugSources("3d", this.engine)) {
1515
1605
  vertices = source.debugLines();
1516
1606
  if (vertices) break;
1517
1607
  }
@@ -1543,9 +1633,56 @@ var Renderer3D = class {
1543
1633
  const size = this.lastSize;
1544
1634
  if (size.w > 0 && size.h > 0) this.webgl.setSize(size.w, size.h, false);
1545
1635
  }
1636
+ /**
1637
+ * Compile the scene's GPU programs off the critical path.
1638
+ *
1639
+ * The compile itself is unavoidable — a dressed outdoor scene is 40+ programs
1640
+ * and a driver takes hundreds of milliseconds each (measured: SEVEN SECONDS
1641
+ * for this engine's creek template on macOS/Metal). What is avoidable is
1642
+ * doing it in ONE synchronous block, which freezes the tab and leaves a
1643
+ * loading bar with nothing to say.
1644
+ *
1645
+ * Called by `createGame3D` behind the app's loading screen.
1646
+ */
1647
+ /** @internal Tell the next frame to leave the bulk compile to {@link warmUp}. */
1648
+ deferFirstCompile() {
1649
+ this.deferCompile = true;
1650
+ }
1651
+ async warmUp(onProgress) {
1652
+ this.deferCompile = false;
1653
+ const camera = this.warmCamera;
1654
+ if (!camera) return;
1655
+ const drawn = [];
1656
+ this.threeScene.traverse((obj) => {
1657
+ if (obj.isMesh) drawn.push(obj);
1658
+ else if (obj.isPoints) drawn.push(obj);
1659
+ });
1660
+ const hidden = drawn.filter((o) => o.visible);
1661
+ if (hidden.length === 0) {
1662
+ this.webgl.compile(this.threeScene, camera);
1663
+ onProgress?.(1);
1664
+ return;
1665
+ }
1666
+ for (const obj of hidden) obj.visible = false;
1667
+ const slices = Math.min(WARMUP_SLICES, hidden.length);
1668
+ const per = Math.ceil(hidden.length / slices);
1669
+ try {
1670
+ for (let i = 0; i < slices; i++) {
1671
+ for (let k = i * per; k < Math.min((i + 1) * per, hidden.length); k++) hidden[k].visible = true;
1672
+ this.webgl.compile(this.threeScene, camera);
1673
+ onProgress?.((i + 1) / slices);
1674
+ if (typeof requestAnimationFrame === "function") await new Promise((resolve) => {
1675
+ requestAnimationFrame(() => setTimeout(resolve, 0));
1676
+ });
1677
+ }
1678
+ } finally {
1679
+ for (const obj of hidden) obj.visible = true;
1680
+ }
1681
+ }
1546
1682
  render() {
1547
1683
  const scene = this.engine.scene;
1548
1684
  if (!scene) return;
1685
+ this.webgl.info.reset();
1549
1686
  this.governResolution();
1550
1687
  this.syncDebugLines();
1551
1688
  this.syncSelectionOutline();
@@ -1574,10 +1711,11 @@ var Renderer3D = class {
1574
1711
  const threeLight = sunLight ? sunLight._ensureObject3D() : null;
1575
1712
  const followsCamera = !!threeLight && sunLight.shadowFollowsCamera === true;
1576
1713
  this.environment.applySunLight(threeLight, followsCamera ? camWorld : null);
1577
- if (this.compiledScene !== scene) {
1714
+ if (this.compiledScene !== scene && !this.deferCompile) {
1578
1715
  this.compiledScene = scene;
1579
1716
  this.webgl.compile(this.threeScene, activeCamera);
1580
1717
  }
1718
+ this.warmCamera = activeCamera;
1581
1719
  const w = this.canvas.clientWidth || this.canvas.width;
1582
1720
  const h = this.canvas.clientHeight || this.canvas.height;
1583
1721
  const size = this.webgl.getSize(sizeScratch);
@@ -1645,7 +1783,10 @@ var Renderer3D = class {
1645
1783
  this.bloomBlurTarget = null;
1646
1784
  }
1647
1785
  if (!this.bloomTarget) {
1648
- this.bloomTarget = new WebGLRenderTarget(w, h, { type: HalfFloatType });
1786
+ this.bloomTarget = new WebGLRenderTarget(w, h, {
1787
+ type: HalfFloatType,
1788
+ samples: 4
1789
+ });
1649
1790
  this.bloomBrightTarget = new WebGLRenderTarget(hw, hh, { type: HalfFloatType });
1650
1791
  this.bloomBlurTarget = new WebGLRenderTarget(hw, hh, { type: HalfFloatType });
1651
1792
  }
@@ -1882,8 +2023,8 @@ var Renderer3D = class {
1882
2023
  return null;
1883
2024
  }
1884
2025
  /**
1885
- * GPU counters for the LAST rendered frame, straight from three's
1886
- * per-frame-maintained `renderer.info` (render pass + GPU memory).
2026
+ * GPU counters for the last rendered frame — EVERY pass of it, summed (see
2027
+ * the `info.reset()` in `render`), plus GPU memory.
1887
2028
  */
1888
2029
  stats() {
1889
2030
  const info = this.webgl.info;
@@ -1894,6 +2035,24 @@ var Renderer3D = class {
1894
2035
  textures: info.memory.textures
1895
2036
  };
1896
2037
  }
2038
+ /**
2039
+ * Where the last rendered frame was seen FROM.
2040
+ *
2041
+ * Paired with {@link cameraBasis} this is the view on screen — a third-person
2042
+ * rig, a cutscene camera and the editor's free-fly all answer here, which is
2043
+ * what a mode switch needs to carry across so the cut does not read as a
2044
+ * page load.
2045
+ */
2046
+ lastCameraPosition() {
2047
+ const cam = this.lastCamera;
2048
+ if (!cam) return null;
2049
+ cam.getWorldPosition(basisPos);
2050
+ return {
2051
+ x: basisPos.x,
2052
+ y: basisPos.y,
2053
+ z: basisPos.z
2054
+ };
2055
+ }
1897
2056
  /** The last rendered camera's world-space basis (for screen-plane dragging). */
1898
2057
  cameraBasis() {
1899
2058
  const cam = this.lastCamera;
@@ -1949,12 +2108,16 @@ var Renderer3D = class {
1949
2108
  this.webgl.dispose();
1950
2109
  }
1951
2110
  };
2111
+ /** Slices the warm-up compile is broken into — enough for a bar to move, few
2112
+ * enough that walking the scene per slice stays negligible. */
2113
+ const WARMUP_SLICES = 12;
1952
2114
  const sizeScratch = new Vector2();
1953
2115
  const lookScratch = new Vector3();
1954
2116
  const projScratch = new Vector3();
1955
2117
  const sunDirScratch = new Vector3();
1956
2118
  const sunTargetScratch = new Vector3();
1957
2119
  const basisQuat = new Quaternion();
2120
+ const basisPos = new Vector3();
1958
2121
  /** Reused camera world-position scratch (shadow-follow focus + underwater test). */
1959
2122
  const camPosScratch = new Vector3();
1960
2123
  /** Reused click-pick scratches (no per-pick allocation). */
@@ -1968,6 +2131,14 @@ var create_game_exports = /* @__PURE__ */ __exportAll({ createGame3D: () => crea
1968
2131
  * (engine attached for onReady) → physics (auto) → input → renderer → start.
1969
2132
  */
1970
2133
  async function createGame3D(opts) {
2134
+ const report = async (fraction, label) => {
2135
+ if (!opts.onProgress) return;
2136
+ opts.onProgress(fraction, label);
2137
+ if (typeof requestAnimationFrame !== "function") return;
2138
+ await new Promise((resolve) => {
2139
+ requestAnimationFrame(() => setTimeout(resolve, 0));
2140
+ });
2141
+ };
1971
2142
  registerNodes3D();
1972
2143
  if (opts.gameplay ?? true) registerGameplayBehaviors();
1973
2144
  for (const [name, ctor] of Object.entries(opts.behaviors ?? {})) registerBehavior(name, ctor, { replace: true });
@@ -1976,13 +2147,16 @@ async function createGame3D(opts) {
1976
2147
  fixedHz: opts.fixedHz,
1977
2148
  ...opts._scheduler ? { scheduler: opts._scheduler } : {}
1978
2149
  });
1979
- const scene = loadScene(cloneSceneJson(opts.scene), {
2150
+ const sourceJson = cloneSceneJson(opts.scene);
2151
+ const scene = loadScene(cloneSceneJson(sourceJson), {
1980
2152
  resolveScene: opts.resolveScene,
1981
2153
  engine
1982
2154
  });
1983
2155
  engine.setScene(scene);
2156
+ await report(.06, "scene");
1984
2157
  let physics = null;
1985
2158
  if (opts.physics === true || (opts.physics ?? "auto") === "auto" && hasBody(scene.root)) physics = await enablePhysics3D(engine);
2159
+ await report(.1, "physics");
1986
2160
  const cleanups = [];
1987
2161
  const keyboard = opts.keyboard ?? (typeof window !== "undefined" ? window : false);
1988
2162
  if (keyboard) engine.input.attachKeyboard(keyboard);
@@ -2016,22 +2190,79 @@ async function createGame3D(opts) {
2016
2190
  ...opts.pixelRatio !== void 0 ? { pixelRatio: opts.pixelRatio } : {},
2017
2191
  ...opts.adaptiveResolution !== void 0 ? { adaptiveResolution: opts.adaptiveResolution } : {}
2018
2192
  });
2019
- if (opts.debug ?? false) {
2193
+ await report(.14, "renderer");
2194
+ const wantDebug = opts.debug ?? false;
2195
+ const wantEditor = opts.editor ?? wantDebug;
2196
+ const editorSwitch = wantEditor === true ? {} : wantEditor ? wantEditor : null;
2197
+ const editorAction = {
2198
+ label: "✎ edit this scene",
2199
+ run: async () => {
2200
+ if (!editorSwitch) return;
2201
+ const camera = poseFromRenderer(renderer);
2202
+ const open = editorSwitch.open ?? openBundledEditor;
2203
+ const reboot = async (working) => {
2204
+ await createGame3D({
2205
+ ...opts,
2206
+ scene: working
2207
+ });
2208
+ };
2209
+ await crossFade(async () => {
2210
+ disposeGame();
2211
+ try {
2212
+ await open({
2213
+ scene: sourceJson,
2214
+ ...camera ? { camera } : {},
2215
+ play: reboot,
2216
+ exit: () => reboot(sourceJson),
2217
+ ...editorSwitch.save ? { save: editorSwitch.save } : {},
2218
+ ...editorSwitch.library ? { library: editorSwitch.library === true ? devServerLibrary() : editorSwitch.library } : {}
2219
+ });
2220
+ } catch (error) {
2221
+ console.error("[incanto] could not open the editor:", error);
2222
+ await reboot(sourceJson);
2223
+ }
2224
+ }, "opening the editor…");
2225
+ }
2226
+ };
2227
+ if (wantDebug) {
2020
2228
  const host = opts.touchContainer ?? opts.canvas.parentElement ?? (typeof document !== "undefined" ? document.body : null);
2021
2229
  if (host) {
2022
2230
  const { attachDebugOverlay } = await import("./debug.js");
2023
2231
  const overlay = attachDebugOverlay(engine, {
2024
2232
  container: host,
2025
2233
  statsSource: () => renderer.stats?.() ?? {},
2234
+ ...editorSwitch ? { actions: [editorAction] } : {},
2026
2235
  ...opts._debugDoc ? { doc: opts._debugDoc } : {}
2027
2236
  });
2028
2237
  if (overlay) cleanups.push(() => overlay.dispose());
2029
2238
  }
2030
2239
  }
2031
2240
  engine.start();
2241
+ await report(.16, "shaders");
2242
+ try {
2243
+ renderer.deferFirstCompile?.();
2244
+ const nowMs = globalThis.performance?.now?.() ?? 0;
2245
+ engine.tick(nowMs);
2246
+ engine.tick(nowMs + 16);
2247
+ await renderer.warmUp?.((f) => {
2248
+ opts.onProgress?.(.16 + f * .82, "shaders");
2249
+ });
2250
+ } catch {}
2251
+ await report(1, "ready");
2252
+ function disposeGame() {
2253
+ teardown([
2254
+ physics ? ["physics", () => physics?.dispose()] : null,
2255
+ ["app cleanups", () => {
2256
+ for (const cleanup of cleanups) cleanup();
2257
+ }],
2258
+ ["engine", () => engine.dispose()],
2259
+ ["renderer", () => renderer.dispose()]
2260
+ ]);
2261
+ }
2032
2262
  return {
2033
2263
  engine,
2034
2264
  scene,
2265
+ sourceJson,
2035
2266
  renderer,
2036
2267
  physics,
2037
2268
  stats() {
@@ -2044,12 +2275,8 @@ async function createGame3D(opts) {
2044
2275
  ...renderer.stats?.()
2045
2276
  };
2046
2277
  },
2047
- dispose() {
2048
- renderer.dispose();
2049
- physics?.dispose();
2050
- for (const cleanup of cleanups) cleanup();
2051
- engine.dispose();
2052
- }
2278
+ openEditor: editorSwitch ? editorAction.run : null,
2279
+ dispose: disposeGame
2053
2280
  };
2054
2281
  }
2055
2282
  function cloneSceneJson(json) {
@@ -0,0 +1,18 @@
1
+ //#region src/core/debug-draw.ts
2
+ const sources = /* @__PURE__ */ new Set();
3
+ /** @internal physics runtimes self-register on enable. */
4
+ function registerDebugSource(source) {
5
+ sources.add(source);
6
+ return () => sources.delete(source);
7
+ }
8
+ /**
9
+ * @internal renderers pull their OWN engine's sources for their dimension.
10
+ *
11
+ * `engine` is optional only so a caller with nothing to scope by still works;
12
+ * every renderer passes one, and should.
13
+ */
14
+ function debugSources(dimension, engine) {
15
+ return [...sources].filter((s) => s.dimension === dimension && (engine === void 0 || s.engine === engine));
16
+ }
17
+ //#endregion
18
+ export { registerDebugSource as n, debugSources as t };