incanto 0.29.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 (38) hide show
  1. package/bin/incanto-editor.mjs +101 -28
  2. package/dist/2d.d.ts +16 -1
  3. package/dist/2d.js +2 -2
  4. package/dist/3d.d.ts +34 -1
  5. package/dist/3d.js +4 -4
  6. package/dist/{create-game-CbuLWorm.js → create-game-5z_QVtLx.js} +13 -9
  7. package/dist/{create-game-CNKXGfpr.js → create-game-DuBTv2zI.js} +14 -10
  8. package/dist/debug-draw-BM3DsvtT.js +18 -0
  9. package/dist/debug.js +1 -1
  10. package/dist/editor.js +1346 -1231
  11. package/dist/{gameplay-L05WgWd1.js → gameplay-Cfr6aFZ1.js} +12 -3
  12. package/dist/gameplay.js +1 -1
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.js +1 -1
  15. package/dist/{physics-2d-CCVTrKOd.js → physics-2d-3kOQCtgd.js} +26 -1
  16. package/dist/{physics-3d-BZZLtwJu.js → physics-3d-CeRH-Ff_.js} +38 -3
  17. package/dist/react.js +1 -1
  18. package/dist/{register-C44aSduO.js → register-DJ0SByQg.js} +1 -1
  19. package/dist/{editor-switch-CAKlJMIY.js → teardown-ByzfDPyu.js} +83 -4
  20. package/dist/test.js +6 -6
  21. package/dist/vite.d.ts +87 -1
  22. package/dist/vite.js +262 -3
  23. package/editor/assets/{agent8-DEVkEa3d.js → agent8-CAp0i5qn.js} +1 -1
  24. package/editor/assets/{debug-zGAtpDF0.js → debug-BoEYfbqK.js} +1 -1
  25. package/editor/assets/index-BO6WU8by.js +10696 -0
  26. package/editor/index.html +1 -1
  27. package/package.json +1 -1
  28. package/skills/incanto-editor.md +61 -8
  29. package/skills/incanto-physics-and-input.md +15 -0
  30. package/skills/incanto-verifying-your-game.md +6 -0
  31. package/templates-app/beacon-isle-3d/package.json +1 -1
  32. package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
  33. package/templates-app/tps-3d/package.json +1 -1
  34. package/templates-app/tps-3d/vite.config.ts +7 -0
  35. package/templates-app/village-quest-3d/package.json +1 -1
  36. package/templates-app/village-quest-3d/vite.config.ts +7 -0
  37. package/dist/debug-draw-CZmOYjL2.js +0 -13
  38. package/editor/assets/index-Bx4UtWYY.js +0 -10586
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * API (the page can only address *.scene.json files under the launch directory):
11
11
  * GET /api/meta → { version, mode, root, input?, output? }
12
- * GET /api/scenes → [{ rel, abs }] (project mode)
12
+ * GET /api/scenes → [{ rel, abs, size, modified, scene?|notScene? }]
13
13
  * POST /api/scenes → { path } creates a minimal scene (project mode)
14
14
  * GET /api/scene[?file=] → scene JSON (single mode ignores ?file=)
15
15
  * PUT /api/scene[?file=] → validates the scene shape, writes the file
@@ -27,7 +27,7 @@
27
27
  * request host. Bodies are capped at 8MB. Addressable paths are root-bounded
28
28
  * and must end in .scene.json.
29
29
  */
30
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
30
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
31
31
  import { createServer } from 'node:http';
32
32
  import { basename, dirname, extname, join, normalize, relative, resolve, sep } from 'node:path';
33
33
  import { fileURLToPath } from 'node:url';
@@ -213,9 +213,20 @@ function discoverScenes() {
213
213
  for (const e of entries) {
214
214
  if (e.isDirectory()) {
215
215
  if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), depth + 1);
216
- } else if (e.name.endsWith('.scene.json')) {
216
+ } else if (e.name.endsWith('.json')) {
217
217
  const abs = join(dir, e.name);
218
- found.push({ rel: relative(ROOT, abs), abs });
218
+ // size + mtime are what the browser prints beside each row; a folder
219
+ // whose files are all "just now" is the one you were working in
220
+ let size = 0;
221
+ let modified = 0;
222
+ try {
223
+ const stat = statSync(abs);
224
+ size = stat.size;
225
+ modified = stat.mtimeMs;
226
+ } catch {
227
+ // listed without facts beats not listed at all
228
+ }
229
+ found.push({ rel: relative(ROOT, abs), abs, size, modified, ...inspectJson(abs, size) });
219
230
  }
220
231
  }
221
232
  };
@@ -223,9 +234,65 @@ function discoverScenes() {
223
234
  return found.sort((a, b) => a.rel.localeCompare(b.rel));
224
235
  }
225
236
 
226
- /** Root-bounded, *.scene.json-only — the page can never address anything else. */
237
+ /**
238
+ * Is this JSON a scene? The file says so itself — `type: "scene"`, the same key
239
+ * the loader demands — so a row can be marked instead of guessed from its name.
240
+ */
241
+ function sceneFacts(json) {
242
+ if (json === null || typeof json !== 'object' || Array.isArray(json)) {
243
+ return { ok: false, why: 'the file is JSON, but not a JSON object' };
244
+ }
245
+ if (json.type !== 'scene') {
246
+ return {
247
+ ok: false,
248
+ why:
249
+ json.type === undefined
250
+ ? '"type" must be "scene", and this file has no "type"'
251
+ : `"type" must be "scene", not ${JSON.stringify(json.type)}`,
252
+ };
253
+ }
254
+ if (json.format !== 1)
255
+ return { ok: false, why: `"format" must be 1, not ${JSON.stringify(json.format)}` };
256
+ if (typeof json.root !== 'object' || json.root === null) {
257
+ return { ok: false, why: '"root" must be a node object' };
258
+ }
259
+ const count = (n) =>
260
+ Array.isArray(n?.children) ? n.children.reduce((t, c) => t + count(c), 1) : 1;
261
+ return {
262
+ ok: true,
263
+ name: typeof json.name === 'string' ? json.name : '',
264
+ // only when the file SAYS so — `dimension` is optional and has no default
265
+ ...(json.dimension === '2d' || json.dimension === '3d' ? { dimension: json.dimension } : {}),
266
+ nodes: count(json.root),
267
+ };
268
+ }
269
+
270
+ function inspectJson(abs, size) {
271
+ if (size > 16_000_000) return { notScene: 'too large to check without opening it' };
272
+ let parsed;
273
+ try {
274
+ parsed = JSON.parse(readFileSync(abs, 'utf-8'));
275
+ } catch (error) {
276
+ return { notScene: `not valid JSON: ${String(error.message ?? error)}` };
277
+ }
278
+ const facts = sceneFacts(parsed);
279
+ return facts.ok
280
+ ? {
281
+ scene: {
282
+ name: facts.name,
283
+ ...(facts.dimension ? { dimension: facts.dimension } : {}),
284
+ nodes: facts.nodes,
285
+ },
286
+ }
287
+ : { notScene: facts.why };
288
+ }
289
+
290
+ /**
291
+ * Root-bounded, .json-only — the browser offers every JSON and the LOAD decides,
292
+ * but this must never become a way to read .env or somebody's key file.
293
+ */
227
294
  function resolveSceneFile(rel) {
228
- if (typeof rel !== 'string' || !rel.endsWith('.scene.json')) return null;
295
+ if (typeof rel !== 'string' || !rel.endsWith('.json')) return null;
229
296
  const abs = normalize(resolve(ROOT, rel));
230
297
  if (abs !== ROOT && !abs.startsWith(ROOT + sep)) return null;
231
298
  return abs;
@@ -276,18 +343,6 @@ function originAllowed(req) {
276
343
  }
277
344
  }
278
345
 
279
- function isSceneShaped(json) {
280
- return (
281
- json !== null &&
282
- typeof json === 'object' &&
283
- json.format === 1 &&
284
- json.type === 'scene' &&
285
- typeof json.name === 'string' &&
286
- json.root !== null &&
287
- typeof json.root === 'object'
288
- );
289
- }
290
-
291
346
  function readBody(req, res, onJson) {
292
347
  const chunks = [];
293
348
  let received = 0;
@@ -346,18 +401,23 @@ const server = createServer((req, res) => {
346
401
  return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
347
402
  }
348
403
  readBody(req, res, (body) => {
349
- const abs = resolveSceneFile(body?.path);
404
+ // a path with no extension means the usual thing, so `levels/arena` works
405
+ const wanted =
406
+ typeof body?.path === 'string' && body.path && !body.path.endsWith('.json')
407
+ ? `${body.path}.scene.json`
408
+ : body?.path;
409
+ const abs = resolveSceneFile(wanted);
350
410
  if (!abs) {
351
411
  return send(
352
412
  res,
353
413
  400,
354
- JSON.stringify({ error: 'path must be a *.scene.json inside the project' }),
414
+ JSON.stringify({ error: 'path must be a .json file inside the project' }),
355
415
  );
356
416
  }
357
417
  if (existsSync(abs)) {
358
- return send(res, 409, JSON.stringify({ error: `already exists: ${body.path}` }));
418
+ return send(res, 409, JSON.stringify({ error: `already exists: ${wanted}` }));
359
419
  }
360
- const name = basename(abs).replace(/\.scene\.json$/, '') || 'Scene';
420
+ const name = basename(abs).replace(/\.scene\.json$|\.json$/, '') || 'Scene';
361
421
  const scene = {
362
422
  format: 1,
363
423
  type: 'scene',
@@ -388,26 +448,39 @@ const server = createServer((req, res) => {
388
448
  );
389
449
  }
390
450
  if (req.method === 'GET') {
451
+ let text;
452
+ let parsed;
391
453
  try {
392
- const text = readFileSync(target.input, 'utf-8');
393
- JSON.parse(text); // must at least be JSON before the page gets it
394
- return send(res, 200, text);
454
+ text = readFileSync(target.input, 'utf-8');
455
+ parsed = JSON.parse(text); // must at least be JSON before the page gets it
395
456
  } catch (error) {
396
457
  return send(res, 422, JSON.stringify({ error: String(error.message ?? error) }));
397
458
  }
459
+ // A JSON that is not a scene is refused WITH THE REASON and its contents
460
+ // never leave: "offer everything, explain the no" must not also mean
461
+ // "this server will read you any JSON in the project".
462
+ const facts = sceneFacts(parsed);
463
+ if (!facts.ok) {
464
+ return send(
465
+ res,
466
+ 422,
467
+ JSON.stringify({ error: `not an incanto scene — ${facts.why}.`, notScene: true }),
468
+ );
469
+ }
470
+ return send(res, 200, text);
398
471
  }
399
472
  if (req.method === 'PUT') {
400
473
  if (!originAllowed(req)) {
401
474
  return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
402
475
  }
403
476
  readBody(req, res, (json, raw) => {
404
- if (!isSceneShaped(json)) {
477
+ const shape = sceneFacts(json);
478
+ if (!shape.ok) {
405
479
  return send(
406
480
  res,
407
481
  422,
408
482
  JSON.stringify({
409
- error:
410
- 'not an incanto scene — required: format: 1, type: "scene", name: string, root: object. The file was NOT written.',
483
+ error: `not an incanto scene — ${shape.why}. The file was NOT written.`,
411
484
  }),
412
485
  );
413
486
  }
package/dist/2d.d.ts CHANGED
@@ -146,6 +146,12 @@ type Rapier = typeof RapierNs;
146
146
  interface Physics2DOptions {
147
147
  /** px/s², y-down. Default: scene `physics.gravity`, else [0, 980]. */
148
148
  gravity?: [number, number];
149
+ /**
150
+ * Build the colliders and keep them on the tree, but never advance the world
151
+ * (default: `true`, i.e. simulate). See {@link Physics3DOptions.simulate} —
152
+ * the scene editor uses it to draw the REAL colliders without running them.
153
+ */
154
+ simulate?: boolean;
149
155
  }
150
156
  /**
151
157
  * Enable 2D physics for an engine. Dynamically imports Rapier (compat build,
@@ -158,7 +164,7 @@ declare function enablePhysics2D(engine: Engine, opts?: Physics2DOptions): Promi
158
164
  */
159
165
  declare class Physics2D {
160
166
  private readonly R;
161
- private readonly engine;
167
+ readonly engine: Engine;
162
168
  /** Render collider outlines in the GAME view (renderers pick this up). */
163
169
  debugDraw: boolean;
164
170
  /** Narrow those outlines to one node's subtree (see DebugLineSource). */
@@ -177,6 +183,8 @@ declare class Physics2D {
177
183
  private lastScene;
178
184
  constructor(R: Rapier, engine: Engine, opts?: Physics2DOptions);
179
185
  /** @internal Driven by engine.fixedUpdated. */
186
+ /** Whether the solver runs at all. False = collider outlines only. */
187
+ private readonly simulate;
180
188
  step(dt: number): void;
181
189
  /** @internal Called by CharacterBody2D.moveAndSlide (during tree fixedUpdate). */
182
190
  moveAndSlide(node: CharacterBody2D): void;
@@ -210,6 +218,13 @@ declare class Physics2D {
210
218
  private readonly jointSet;
211
219
  private readonly joints;
212
220
  private readonly bodySet;
221
+ /**
222
+ * Move every body to its node's current transform — outline mode only.
223
+ *
224
+ * Computed exactly as `ensureEntry` computes a body's first pose, so an
225
+ * outline drawn in the editor is the outline the running game draws.
226
+ */
227
+ private poseFromTree;
213
228
  private syncBodies;
214
229
  private ensureEntry;
215
230
  }
package/dist/2d.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-CbuLWorm.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-5z_QVtLx.js";
3
3
  import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-DWcWq4QG.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CCVTrKOd.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-3kOQCtgd.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * Turn a library sprite-animation JSON into a scene-ready spritesheet asset
package/dist/3d.d.ts CHANGED
@@ -185,6 +185,13 @@ declare class Node3D extends Node {
185
185
  * back to a local Y by subtracting the parents' offset.
186
186
  */
187
187
  applyGroundSnap(): void;
188
+ /**
189
+ * @internal The backing object IF it has been built — never builds one.
190
+ *
191
+ * `_ensureObject3D()` is the wrong call during teardown: it allocates a mesh
192
+ * for a node that never rendered, purely so the next line can dispose it.
193
+ */
194
+ protected get _builtObject3D(): Object3D | null;
188
195
  /** @internal The backing three object (lazily created). */
189
196
  _ensureObject3D(): Object3D;
190
197
  /** @internal Override point for subclasses (mesh, camera, light…). */
@@ -269,6 +276,20 @@ type Rapier = typeof RapierNs;
269
276
  interface Physics3DOptions {
270
277
  /** m/s², y-up. Default: scene `physics.gravity`, else [0, -9.81, 0]. */
271
278
  gravity?: [number, number, number];
279
+ /**
280
+ * Build the colliders and keep them on the tree, but never advance the world
281
+ * (default: `true`, i.e. simulate).
282
+ *
283
+ * This exists for the scene editor. "Show colliders" there used to hand-draw
284
+ * wireframes from the `collider` JSON — which cannot show what the engine
285
+ * actually creates, because the shape is not always in the JSON (a mesh-fitted
286
+ * body, a scatter's hulls, a character capsule) and the drawing ignored
287
+ * rotation and scale. With `simulate: false` the editor gets the SAME lines
288
+ * play mode gets, from the same Rapier world, without anything falling over
289
+ * the moment you look at it: bodies are posed FROM the tree each tick and the
290
+ * solver never runs.
291
+ */
292
+ simulate?: boolean;
272
293
  }
273
294
  /**
274
295
  * Enable 3D physics for an engine. Dynamically imports Rapier (compat build)
@@ -279,7 +300,7 @@ declare function enablePhysics3D(engine: Engine, opts?: Physics3DOptions): Promi
279
300
  /** Per-engine 3D physics world. Same contract as Physics2D. */
280
301
  declare class Physics3D {
281
302
  private readonly R;
282
- private readonly engine;
303
+ readonly engine: Engine;
283
304
  /** Render collider outlines in the GAME view (renderers pick this up). */
284
305
  debugDraw: boolean;
285
306
  /** Narrow those outlines to one node's subtree (see DebugLineSource). */
@@ -297,8 +318,20 @@ declare class Physics3D {
297
318
  private readonly optsGravity;
298
319
  private lastScene;
299
320
  constructor(R: Rapier, engine: Engine, opts?: Physics3DOptions);
321
+ /** Whether the solver runs at all. False = collider outlines only. */
322
+ private readonly simulate;
300
323
  /** @internal Driven by engine.fixedUpdated. */
301
324
  step(dt: number): void;
325
+ /**
326
+ * Move every body to its node's current transform.
327
+ *
328
+ * Only used in outline mode: normally physics owns the pose and writes it
329
+ * INTO the node, so nothing re-reads the tree. Deliberately computed the same
330
+ * way `ensureEntry` computes a body's initial pose — world position, node-local
331
+ * euler — so an outline in the editor is the outline the game will draw, right
332
+ * down to the places where that pair disagrees with a rotated parent.
333
+ */
334
+ private poseFromTree;
302
335
  /** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
303
336
  moveAndSlide(node: CharacterBody3D): void;
304
337
  /** Rapier debug segments (meters). Null while debugDraw is off. */
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-L05WgWd1.js";
3
- import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-CNKXGfpr.js";
4
- import { A as DEFAULT_TERRAIN_TEXTURE_BASE, B as movementState, C as FLOWER_VARIETIES, D as BoneAttachment3D, E as BoneLookAt3D, F as MeshInstance3D, L as QUARTER_PITCH, M as terrainThemeLayers, N as Joint3D, O as Billboard3D, P as InstancedMesh3D, R as cameraRelative, S as resolveFlowerDensity, T as Camera3D, V as rigPose, _ as DirectionalLight3D, a as Trail3D, b as DENSITY_PRESETS, c as findRiverCoverageGaps, d as riverStepFor, f as smoothCourse, g as LoftMesh3D, h as ModelInstance3D, i as Tree3D, j as TERRAIN_THEMES, k as Terrain3D, l as projectToRiver, m as Particles3D, n as VOXEL_PALETTE, o as River3D, p as traceDownhillPath, r as VoxelGrid3D, s as buildRiverRings, t as registerNodes3D, u as riverCarveChannels, v as OmniLight3D, w as CharacterController3D, x as Flowers3D, y as Foliage3D, z as keyboardIntensity } from "./register-C44aSduO.js";
2
+ import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-Cfr6aFZ1.js";
3
+ import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-DuBTv2zI.js";
4
+ import { A as DEFAULT_TERRAIN_TEXTURE_BASE, B as movementState, C as FLOWER_VARIETIES, D as BoneAttachment3D, E as BoneLookAt3D, F as MeshInstance3D, L as QUARTER_PITCH, M as terrainThemeLayers, N as Joint3D, O as Billboard3D, P as InstancedMesh3D, R as cameraRelative, S as resolveFlowerDensity, T as Camera3D, V as rigPose, _ as DirectionalLight3D, a as Trail3D, b as DENSITY_PRESETS, c as findRiverCoverageGaps, d as riverStepFor, f as smoothCourse, g as LoftMesh3D, h as ModelInstance3D, i as Tree3D, j as TERRAIN_THEMES, k as Terrain3D, l as projectToRiver, m as Particles3D, n as VOXEL_PALETTE, o as River3D, p as traceDownhillPath, r as VoxelGrid3D, s as buildRiverRings, t as registerNodes3D, u as riverCarveChannels, v as OmniLight3D, w as CharacterController3D, x as Flowers3D, y as Foliage3D, z as keyboardIntensity } from "./register-DJ0SByQg.js";
5
5
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
6
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-BZZLtwJu.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CeRH-Ff_.js";
7
7
  //#region src/3d/environment-runtime.ts
8
8
  /**
9
9
  * Live environment editing — the renderer re-applies `scene.environment`
@@ -3,11 +3,11 @@ import { _ as registerBehavior, n as loadScene, o as computeViewport, s as resol
3
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 devServerLibrary, r as openBundledEditor, t as crossFade } from "./editor-switch-CAKlJMIY.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-L05WgWd1.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
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-CZmOYjL2.js";
10
- import { n as enablePhysics2D } from "./physics-2d-CCVTrKOd.js";
9
+ import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-3kOQCtgd.js";
11
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
12
  //#region src/2d/assets.ts
13
13
  /**
@@ -310,7 +310,7 @@ var Renderer2D = class {
310
310
  }
311
311
  syncDebugLines() {
312
312
  let vertices = null;
313
- for (const source of debugSources("2d")) {
313
+ for (const source of debugSources("2d", this.engine)) {
314
314
  vertices = source.debugLines();
315
315
  if (vertices) break;
316
316
  }
@@ -603,10 +603,14 @@ async function createGame2D(opts) {
603
603
  }
604
604
  engine.start();
605
605
  function disposeGame() {
606
- renderer.dispose();
607
- physics?.dispose();
608
- for (const cleanup of cleanups) cleanup();
609
- engine.dispose();
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
+ ]);
610
614
  }
611
615
  return {
612
616
  engine,
@@ -3,11 +3,11 @@ import { _ as registerBehavior, n as loadScene } from "./loader-BZqOKfI2.js";
3
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 { i as poseFromRenderer, n as devServerLibrary, r as openBundledEditor, t as crossFade } from "./editor-switch-CAKlJMIY.js";
7
- import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-L05WgWd1.js";
8
- import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
9
- import { T as Camera3D, _ as DirectionalLight3D, h as ModelInstance3D, t as registerNodes3D } from "./register-C44aSduO.js";
10
- import { n as enablePhysics3D } from "./physics-3d-BZZLtwJu.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";
11
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";
12
12
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
13
13
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -1601,7 +1601,7 @@ var Renderer3D = class {
1601
1601
  }
1602
1602
  syncDebugLines() {
1603
1603
  let vertices = null;
1604
- for (const source of debugSources("3d")) {
1604
+ for (const source of debugSources("3d", this.engine)) {
1605
1605
  vertices = source.debugLines();
1606
1606
  if (vertices) break;
1607
1607
  }
@@ -2250,10 +2250,14 @@ async function createGame3D(opts) {
2250
2250
  } catch {}
2251
2251
  await report(1, "ready");
2252
2252
  function disposeGame() {
2253
- renderer.dispose();
2254
- physics?.dispose();
2255
- for (const cleanup of cleanups) cleanup();
2256
- engine.dispose();
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
+ ]);
2257
2261
  }
2258
2262
  return {
2259
2263
  engine,
@@ -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 };
package/dist/debug.js CHANGED
@@ -1,6 +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
+ import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
4
4
  //#region src/debug/panel.ts
5
5
  /** Keep a panel rect on screen and above the minimum usable size. */
6
6
  function clampPanelRect(x, y, w, h, viewW, viewH, minW = 180, minH = 120) {