@vgai/engine 0.5.16 → 0.5.18

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 (109) hide show
  1. package/dist/adapter/adapter-module.d.ts +1 -16
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +1 -11
  4. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +0 -2
  5. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
  6. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +3 -6
  7. package/dist/adapter/renderer-config.d.ts +83 -0
  8. package/dist/adapter/renderer-config.d.ts.map +1 -0
  9. package/dist/adapter/renderer-config.js +121 -0
  10. package/dist/adapter/root-adapter.d.ts +6 -2
  11. package/dist/adapter/root-adapter.d.ts.map +1 -1
  12. package/dist/adapter/root-seam-contract.d.ts +5 -0
  13. package/dist/adapter/root-seam-contract.d.ts.map +1 -1
  14. package/dist/adapter/root-seam-contract.js +1 -0
  15. package/dist/adapter/setup-three-root-adapter.js +9 -9
  16. package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
  17. package/dist/canvas-react/pixi-react-adapter.js +92 -41
  18. package/dist/data/curve.d.ts +1 -1
  19. package/dist/data/curve.js +2 -2
  20. package/dist/dev/instruments.d.ts +4 -4
  21. package/dist/dev/instruments.js +4 -4
  22. package/dist/dev/register-render-vitals.d.ts +2 -2
  23. package/dist/dev/register-render-vitals.js +2 -2
  24. package/dist/dev/static-batch-advisor.d.ts +1 -1
  25. package/dist/dev/static-batch-advisor.js +2 -2
  26. package/dist/input/input-manager.d.ts +15 -0
  27. package/dist/input/input-manager.d.ts.map +1 -1
  28. package/dist/input/input-manager.js +22 -0
  29. package/dist/manifest/load.d.ts +0 -3
  30. package/dist/manifest/load.d.ts.map +1 -1
  31. package/dist/manifest/load.js +0 -4
  32. package/dist/manifest/schema.d.ts +0 -2
  33. package/dist/manifest/schema.d.ts.map +1 -1
  34. package/dist/manifest/schema.js +3 -24
  35. package/dist/pixi/authoring.d.ts +33 -0
  36. package/dist/pixi/authoring.d.ts.map +1 -1
  37. package/dist/pixi/authoring.js +86 -6
  38. package/dist/react/world-state.d.ts +12 -0
  39. package/dist/react/world-state.d.ts.map +1 -1
  40. package/dist/react/world-state.js +14 -0
  41. package/dist/render/soft-particle-depth.d.ts +3 -3
  42. package/dist/render/soft-particle-depth.d.ts.map +1 -1
  43. package/dist/render/soft-particle-depth.js +8 -13
  44. package/dist/runtime/create-runtime.d.ts +2 -16
  45. package/dist/runtime/create-runtime.d.ts.map +1 -1
  46. package/dist/runtime/create-runtime.js +36 -38
  47. package/dist/runtime/dev-build.d.ts +37 -0
  48. package/dist/runtime/dev-build.d.ts.map +1 -0
  49. package/dist/runtime/dev-build.js +49 -0
  50. package/dist/runtime/game.d.ts +9 -1
  51. package/dist/runtime/game.d.ts.map +1 -1
  52. package/dist/runtime/game.js +25 -1
  53. package/dist/runtime/mount-manifest.d.ts +0 -13
  54. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  55. package/dist/runtime/mount-manifest.js +2 -21
  56. package/dist/world3d-react/engine-bridge.d.ts.map +1 -1
  57. package/dist/world3d-react/engine-bridge.js +28 -3
  58. package/dist/world3d-react/index.d.ts +2 -2
  59. package/dist/world3d-react/index.d.ts.map +1 -1
  60. package/dist/world3d-react/index.js +2 -2
  61. package/dist/world3d-react/r3f-adapter.d.ts +2 -2
  62. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
  63. package/dist/world3d-react/r3f-adapter.js +90 -15
  64. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  65. package/dist/world3d-react/r3f-root-factory.js +12 -2
  66. package/dist/world3d-react/rapier-physics-bridge.d.ts +4 -4
  67. package/dist/world3d-react/rapier-physics-bridge.js +3 -3
  68. package/dist/world3d-react/renderer-config.d.ts +7 -69
  69. package/dist/world3d-react/renderer-config.d.ts.map +1 -1
  70. package/dist/world3d-react/renderer-config.js +7 -108
  71. package/dist/world3d-react/world-context.d.ts +43 -0
  72. package/dist/world3d-react/world-context.d.ts.map +1 -1
  73. package/dist/world3d-react/world-context.js +89 -46
  74. package/dist-config/config.js.map +1 -1
  75. package/package.json +1 -1
  76. package/schemas/engine-capabilities.json +3 -3
  77. package/schemas/vgai-project.schema.json +2 -7
  78. package/src/adapter/adapter-module.ts +1 -25
  79. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +3 -8
  80. package/src/adapter/renderer-config.ts +178 -0
  81. package/src/adapter/root-adapter.ts +6 -2
  82. package/src/adapter/root-seam-contract.ts +1 -0
  83. package/src/adapter/setup-three-root-adapter.ts +9 -9
  84. package/src/canvas-react/pixi-react-adapter.tsx +98 -53
  85. package/src/data/curve.ts +2 -2
  86. package/src/dev/instruments.ts +4 -4
  87. package/src/dev/register-render-vitals.ts +2 -2
  88. package/src/dev/static-batch-advisor.ts +2 -2
  89. package/src/input/input-manager.ts +23 -0
  90. package/src/manifest/load.ts +0 -7
  91. package/src/manifest/schema.ts +3 -26
  92. package/src/pixi/authoring.ts +98 -6
  93. package/src/react/world-state.tsx +15 -0
  94. package/src/render/soft-particle-depth.ts +8 -13
  95. package/src/runtime/create-runtime.ts +38 -52
  96. package/src/runtime/dev-build.ts +49 -0
  97. package/src/runtime/game.ts +31 -2
  98. package/src/runtime/mount-manifest.ts +2 -33
  99. package/src/world3d-react/engine-bridge.ts +28 -3
  100. package/src/world3d-react/index.ts +5 -5
  101. package/src/world3d-react/r3f-adapter.tsx +89 -20
  102. package/src/world3d-react/r3f-root-factory.tsx +12 -2
  103. package/src/world3d-react/rapier-physics-bridge.tsx +4 -4
  104. package/src/world3d-react/renderer-config.ts +7 -164
  105. package/src/world3d-react/world-context.ts +100 -51
  106. package/dist/runtime/dev-layers.d.ts +0 -34
  107. package/dist/runtime/dev-layers.d.ts.map +0 -1
  108. package/dist/runtime/dev-layers.js +0 -46
  109. package/src/runtime/dev-layers.ts +0 -46
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.16",
5
+ "version": "0.5.18",
6
6
  "description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
7
7
  "keywords": [
8
8
  "game-engine",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "notes": "Grepped packages/engine/src for ccd/CCD/setCcdEnabled: zero matches. The engine constructs no rigid bodies itself — games build their own descs via ctx.RAPIER — and nothing engine-side ever enables ccd. Workaround: keep dynamic-body per-step travel small relative to the thinnest collider, or add intermediate colliders; a game may also call setCcdEnabled on its own bodies directly.",
17
17
  "evidenceHashes": {
18
- "packages/engine/src/adapter/setup-three-root-adapter.ts": "98fb35199a246d99fe752ea467d48a30c71da7f4d4e6f06efa372b0c6abb5456"
18
+ "packages/engine/src/adapter/setup-three-root-adapter.ts": "14cef0975f38ecceb25b607ef62232c1002bdd172a3456571f50aa9e39263c0c"
19
19
  }
20
20
  },
21
21
  {
@@ -43,7 +43,7 @@
43
43
  "notes": "gamepad disconnect/reconnect needs no special-cased code: poll() re-reads navigator.getGamepads() and every read skips a null slot, so a disconnected gamepad's actions go neutral with a correct isJustReleased edge for free — see input-device-backends-f2.test.ts.",
44
44
  "evidenceHashes": {
45
45
  "packages/engine/src/input/input-types.ts": "62d9005609a72b55a8214c869d1b27825b1a17b9389f3f3f5d0b04725331b382",
46
- "packages/engine/src/input/input-manager.ts": "41b1255eb18c74c022b3cc87f6e050d3fb7ee6883215e3888337ff0d94defa63"
46
+ "packages/engine/src/input/input-manager.ts": "7d34683759d7ab7fd20ca6f5b5e646eb76a31b00c77977816f4ef863ccb06e62"
47
47
  }
48
48
  },
49
49
  {
@@ -73,7 +73,7 @@
73
73
  "notes": "Limits: ARCHITECTURE-CORE.md. Any editor capability a mount does not reach stands warned per capability until it works.",
74
74
  "evidenceHashes": {
75
75
  "vendor/games/verify-unaltered.mjs": "257a362030a10f4d72a3280815069fbd83f436e1619391635b6f8df2ddd6c339",
76
- "packages/engine/src/manifest/schema.ts": "65500eb1594bd2333418bf073858131d449f0a17e0b584fc4dedc15bf01970bf"
76
+ "packages/engine/src/manifest/schema.ts": "ce385bc9181b82f9d5044da6a41a025a5cb8ba1fc7156263665080c9fdf2b11c"
77
77
  }
78
78
  },
79
79
  {
@@ -193,11 +193,6 @@
193
193
  "description": "Whether play-mode pause/step applies to this root",
194
194
  "type": "boolean"
195
195
  },
196
- "dev": {
197
- "default": false,
198
- "description": "Marks this root as a DEV LAYER: the game's own dev GUI, not shipped game content. Four readers act on it. `mountManifestRoots` mounts it only when dev layers are enabled (`devLayersEnabled`, runtime/dev-layers.ts), so a production build carries no dev GUI; the host stacks every dev root ABOVE every non-dev root (a dev layer sits topmost, and as a DOM layer it steals no input from the game while closed); the play compositor EXCLUDES dev layers from a capture unless the capture asks for them; and the editor mounts a dev root as edit-time CHROME rather than authorable content (no hierarchy entry, no OID stamping, no JSX write-back, not selectable).",
199
- "type": "boolean"
200
- },
201
196
  "loop": {
202
197
  "default": "gated",
203
198
  "description": "gated: host-driven loop (default). self-driven: this root drives its own loop (composited, unsynchronized)",
@@ -214,7 +209,7 @@
214
209
  ],
215
210
  "additionalProperties": false
216
211
  },
217
- "description": "The Game's explicit, non-empty adapter-root composition. There is no implicit root. Root ids are unique, and a project declares at most ONE world root per medium — one `three`, one `dom`, one canvas-surface root (first-party canvas, canvas ingest and canvas module are the same medium). A `dev: true` root is counted in its own band: a dev layer is chrome stacked above the game, not content competing for the medium, so a game may declare a `dom` HUD and a `dom` dev layer — but not two of either."
212
+ "description": "The Game's explicit, non-empty adapter-root composition. There is no implicit root. Root ids are unique, and a project declares at most ONE world root per medium — one `three`, one `dom`, one canvas-surface root (first-party canvas, canvas ingest and canvas module are the same medium)."
218
213
  },
219
214
  "authoring": {
220
215
  "description": "Editor-only authoring metadata; never runtime ownership",
@@ -336,7 +331,7 @@
336
331
  "properties": {
337
332
  "antialias": {
338
333
  "type": "boolean",
339
- "description": "Whether every three root is built with a multisampled drawing buffer. This is the ONE render property a world cannot declare for itself (world3d-react/renderer-config.ts): a WebGL context fixes its sample count at CREATION from this boolean, long before a world mounts, so it belongs to the PROJECT. The runtime reader is mount-manifest.ts, which threads it into createHostRenderer for each three root. WebGL exposes no sample COUNT — the implementation picks one (4x on every desktop browser measured), so a source engine that authored 8x/16x gets multisampling but not its exact count."
334
+ "description": "Whether every three root is built with a multisampled drawing buffer. This is the ONE render property a world cannot declare for itself (adapter/renderer-config.ts): a WebGL context fixes its sample count at CREATION from this boolean, long before a world mounts, so it belongs to the PROJECT. The runtime reader is mount-manifest.ts, which threads it into createHostRenderer for each three root. WebGL exposes no sample COUNT — the implementation picks one (4x on every desktop browser measured), so a source engine that authored 8x/16x gets multisampling but not its exact count."
340
335
  }
341
336
  },
342
337
  "required": [
@@ -100,18 +100,6 @@ export interface AdapterRegion {
100
100
  readonly id: string;
101
101
  /** What the host hands this root. */
102
102
  readonly surface: AdapterSurface;
103
- /**
104
- * `true` = this region grades the game's own DEV LAYER (the manifest root's
105
- * `dev: true`), not shipped game content.
106
- *
107
- * Carried here because a dev root occupies the same MEDIUM as the content
108
- * root beside it — a dev GUI is a `dom` root next to the HUD's `dom` root —
109
- * and anything that maps a subject to "the region on this surface" would
110
- * otherwise be choosing between them by array order. A region consumer that
111
- * means shipped content says so by skipping `dev` regions (the loader's
112
- * `regionForSurface` does), rather than by guessing which one matched.
113
- */
114
- readonly dev: boolean;
115
103
  /** Projection library, by name. */
116
104
  readonly projector: ProjectorName;
117
105
  /**
@@ -235,12 +223,6 @@ const AdapterRegionSchema = z
235
223
  .object({
236
224
  id: z.string().min(1).describe('Manifest root id this region grades'),
237
225
  surface: z.enum(['three', 'canvas', 'dom']).describe('Render surface the host hands this root'),
238
- dev: z
239
- .boolean()
240
- .describe(
241
- "true = this region grades the game's own dev layer, not shipped content (the " +
242
- "manifest root's `dev: true`)",
243
- ),
244
226
  projector: z.enum(PROJECTOR_NAMES).describe('Projection library serving this surface, by name'),
245
227
  dialect: z
246
228
  .string()
@@ -431,7 +413,6 @@ export function unmatchedRegionIncludeIds(
431
413
  /** One root's region, before any declared parameters are merged onto it. */
432
414
  function derivedRegion(root: ResolvedAdapterRoot): AdapterRegion {
433
415
  const surface = root.surface;
434
- const dev = root.dev;
435
416
  const firstParty = root.adapter.type === 'builtin';
436
417
  const projector: ProjectorName = surface === 'canvas' ? 'pixi' : surface;
437
418
  const basis = NATIVE_REGION_BASIS;
@@ -439,7 +420,6 @@ function derivedRegion(root: ResolvedAdapterRoot): AdapterRegion {
439
420
  return {
440
421
  id: root.id,
441
422
  surface,
442
- dev,
443
423
  projector,
444
424
  dialect: null,
445
425
  anchors: ['live-only'] as const,
@@ -450,7 +430,6 @@ function derivedRegion(root: ResolvedAdapterRoot): AdapterRegion {
450
430
  return {
451
431
  id: root.id,
452
432
  surface,
453
- dev,
454
433
  projector,
455
434
  dialect: 'jsx',
456
435
  anchors: ['source-prop', 'source-structure'] as const,
@@ -460,7 +439,6 @@ function derivedRegion(root: ResolvedAdapterRoot): AdapterRegion {
460
439
  return {
461
440
  id: root.id,
462
441
  surface,
463
- dev,
464
442
  projector,
465
443
  dialect: surface === 'three' ? 'r3f' : 'pixi-react',
466
444
  anchors: ['source-prop', 'source-structure', 'construction-literal'] as const,
@@ -474,9 +452,7 @@ function derivedRegion(root: ResolvedAdapterRoot): AdapterRegion {
474
452
 
475
453
  /**
476
454
  * The rule form of a finder's region targeting: every region the host mounts
477
- * from an exported composition (a root with an `entry`) that is not a declared
478
- * DEV layer. A dev root is the game's own dev GUI — the manifest already says
479
- * so (`dev: true`), so skipping it reads a declaration rather than guessing.
455
+ * from an exported composition a root with an `entry`.
480
456
  */
481
457
  export const EXPORTED_COMPOSITION_REGIONS = 'exported-composition-regions';
482
458
 
@@ -53,8 +53,6 @@ export interface EntrypointSource {
53
53
  readonly path: string;
54
54
  /** The module's bytes; `null` when they could not be read. */
55
55
  readonly source: string | null;
56
- /** The root's declared `dev` flag — a dev layer is the game's own dev GUI. */
57
- readonly dev: boolean;
58
56
  }
59
57
 
60
58
  export interface ScenesFromEntrypointSelectionInput {
@@ -284,11 +282,8 @@ function sourceFor(
284
282
  /**
285
283
  * Which entrypoints this selection targets.
286
284
  *
287
- * The RULE form is every exported-composition region that is not a declared
288
- * DEV layer. Reading `dev` is reading the manifest's own declaration — the dev
289
- * GUI is edit-time chrome, not authorable game content — never a name
290
- * heuristic. A named region with no matching entrypoint becomes a note, not a
291
- * silent omission.
285
+ * The RULE form is every exported-composition region. A named region with no
286
+ * matching entrypoint becomes a note, not a silent omission.
292
287
  */
293
288
  function targetedEntrypoints(
294
289
  params: ScenesFromEntrypointSelectionParams,
@@ -296,7 +291,7 @@ function targetedEntrypoints(
296
291
  notes: string[],
297
292
  ): EntrypointSource[] {
298
293
  if (params.regions === EXPORTED_COMPOSITION_REGIONS) {
299
- return entrypoints.filter((entrypoint) => !entrypoint.dev);
294
+ return [...entrypoints];
300
295
  }
301
296
  const found: EntrypointSource[] = [];
302
297
  for (const regionId of params.regions) {
@@ -0,0 +1,178 @@
1
+ /**
2
+ * adapter/renderer-config.ts — the seam a WORLD uses to state how its own frame is rendered.
3
+ *
4
+ * It lives in the ADAPTER SEAM, not in a surface entry, because `WorldRendererConfig` IS part of
5
+ * the root contract: `MountedThreeRoot.rendererConfig` reports it (`./root-adapter.ts`) and
6
+ * `./root-seam-contract.ts` already lists it as a contract field. The seam defines the shape; the
7
+ * surface entries and the editor CONFORM to it — `world3d-react/r3f-adapter.tsx` is the declarer,
8
+ * `runtime/create-runtime.ts` the host, `editor/src/components/ViewportPanel.tsx` the editor's
9
+ * applier. Do NOT move it back under `world3d-react/`: nothing here touches react, and a core
10
+ * adapter file reaching into a react entry is exactly what `react-core-import-ban.test.ts` forbids
11
+ * (that ban carries no type-only carve-out on purpose — unlike its pixi sibling, whose carve-out
12
+ * exists only because `PIXI.Container` is a third-party type core cannot relocate).
13
+ *
14
+ * The host owns the `WebGLRenderer` (`ThreeHostContext.renderer`) and configures it with this
15
+ * engine's defaults: ACES tone mapping, sRGB output, PCF-soft shadows. Those defaults are right for
16
+ * a world authored against them and WRONG for a world that was authored against a different
17
+ * engine's pipeline — an imported Godot 3 GLES2 game does gamma-space lighting with no tonemapper
18
+ * at all, so ACES quietly desaturates and darkens every colour its author picked.
19
+ *
20
+ * So a world may DECLARE the pipeline it was authored for, and `createR3FAdapter` applies it to the
21
+ * host's renderer for the life of the mount, restoring what it found on dispose. Three properties
22
+ * of that shape are load-bearing:
23
+ *
24
+ * - **It is per-renderer, never engine-wide.** Every field here is a `WebGLRenderer` instance
25
+ * property, and a play root gets its own renderer (`create-runtime.ts`). Nothing here reaches a
26
+ * module-level three global, so one world's declaration cannot change how the editor's own
27
+ * viewport, another root, or a thumbnail bake renders.
28
+ * - **Absent means "leave the host's value alone".** Every field is optional and an omitted one
29
+ * is never written, so declaring a tone mapping does not silently reset the clear colour.
30
+ * - **It is restored on dispose.** The renderer outlives the mount, so a world that did not put
31
+ * back what it found would leak its pipeline into whatever mounts next.
32
+ *
33
+ * This is deliberately NOT a general render-settings system. It carries what a world can honestly
34
+ * state about its own colour pipeline and nothing else; a property the host fixes at CONSTRUCTION
35
+ * (the WebGL context's `antialias` attribute, and therefore the MSAA sample count) cannot be
36
+ * declared here, because there would be no honest moment to apply it.
37
+ */
38
+
39
+ import type * as THREE from 'three';
40
+
41
+ /** The tone-mapping operators three exposes, named as data rather than as three's numeric enum. */
42
+ export type WorldToneMapping =
43
+ | 'none'
44
+ | 'linear'
45
+ | 'reinhard'
46
+ | 'cineon'
47
+ | 'aces'
48
+ | 'agx'
49
+ | 'neutral';
50
+
51
+ /**
52
+ * The output transfer function the frame is written with.
53
+ *
54
+ * - `srgb` — three's own default and this engine's: linear lighting, sRGB encode on output.
55
+ * - `srgb-linear` — NO output transform. This is what a gamma-space renderer needs: the shading
56
+ * result is already in display space and encoding it a second time washes the frame out.
57
+ */
58
+ export type WorldOutputColorSpace = 'srgb' | 'srgb-linear';
59
+
60
+ /**
61
+ * The shadow-map filter, named as data rather than as three's numeric enum.
62
+ *
63
+ * This is a renderer INSTANCE property (`WebGLRenderer.shadowMap.type`), not a context attribute,
64
+ * so unlike MSAA it has an honest moment at which a world can ask for it — which is the whole test
65
+ * this file's header states. A source engine that declares its own shadow filter (Godot 3's
66
+ * `rendering/quality/shadows/filter_mode`) would otherwise inherit whatever the host built with.
67
+ */
68
+ export type WorldShadowMapType = 'basic' | 'pcf' | 'pcf-soft' | 'vsm';
69
+
70
+ /** What a world may declare about the renderer that draws it. Every field is optional; see header. */
71
+ export interface WorldRendererConfig {
72
+ readonly toneMapping?: WorldToneMapping | undefined;
73
+ readonly toneMappingExposure?: number | undefined;
74
+ readonly outputColorSpace?: WorldOutputColorSpace | undefined;
75
+ /**
76
+ * `WebGLRenderer.shadowMap.type`. Writing it after a shadow map has already been built needs
77
+ * `shadowMap.needsUpdate`, which this function sets — three caches the compiled depth material
78
+ * per type and would otherwise keep filtering with the previous one.
79
+ */
80
+ readonly shadowMapType?: WorldShadowMapType | undefined;
81
+ /**
82
+ * The colour the frame is cleared to, as a CSS hex string. The renderer's existing clear ALPHA
83
+ * is preserved: a stacked canvas is transparent on purpose (`create-runtime.ts` gives every
84
+ * non-bottom root `alpha: true`), and forcing it opaque here would hide every layer below.
85
+ */
86
+ readonly clearColor?: string | undefined;
87
+ }
88
+
89
+ /**
90
+ * Apply `config` to `renderer`, returning the restore function that puts back what was there.
91
+ *
92
+ * `three` is passed in rather than imported for values so the enum constants come from the HOST's
93
+ * three instance — the same identity rule `r3f-adapter.tsx` follows for the scene and camera.
94
+ */
95
+ export function applyWorldRendererConfig(
96
+ three: typeof THREE,
97
+ renderer: THREE.WebGLRenderer,
98
+ config: WorldRendererConfig,
99
+ ): () => void {
100
+ // A host that mounts a world WITHOUT rasterizing it hands the adapter a duck-typed renderer —
101
+ // the editor's design session (`createDesignTimeRenderer`: four members, deliberately never
102
+ // widened) and jsdom test harnesses both do. Such a surface has no colour pipeline to configure:
103
+ // the frame the user sees is drawn by a DIFFERENT renderer (the editor's own), so applying the
104
+ // world's config there is meaningless — and calling `getClearColor` on it is a TypeError that
105
+ // unmounts the whole world at edit time (measured: every Godot port's edit viewport blanked with
106
+ // '"world" failed to mount — renderer.getClearColor is not a function'). Detect the real
107
+ // `WebGLRenderer` surface by the one method this function must call, and no-op otherwise.
108
+ if (typeof renderer.getClearColor !== 'function') {
109
+ return () => {};
110
+ }
111
+ const toneMappings: Record<WorldToneMapping, THREE.ToneMapping> = {
112
+ none: three.NoToneMapping,
113
+ linear: three.LinearToneMapping,
114
+ reinhard: three.ReinhardToneMapping,
115
+ cineon: three.CineonToneMapping,
116
+ aces: three.ACESFilmicToneMapping,
117
+ agx: three.AgXToneMapping,
118
+ neutral: three.NeutralToneMapping,
119
+ };
120
+ const colorSpaces: Record<WorldOutputColorSpace, THREE.ColorSpace> = {
121
+ srgb: three.SRGBColorSpace,
122
+ 'srgb-linear': three.LinearSRGBColorSpace,
123
+ };
124
+ const shadowMapTypes: Record<WorldShadowMapType, THREE.ShadowMapType> = {
125
+ basic: three.BasicShadowMap,
126
+ pcf: three.PCFShadowMap,
127
+ 'pcf-soft': three.PCFSoftShadowMap,
128
+ vsm: three.VSMShadowMap,
129
+ };
130
+
131
+ const restores: (() => void)[] = [];
132
+
133
+ if (config.toneMapping !== undefined) {
134
+ const previous = renderer.toneMapping;
135
+ renderer.toneMapping = toneMappings[config.toneMapping];
136
+ restores.push(() => {
137
+ renderer.toneMapping = previous;
138
+ });
139
+ }
140
+ if (config.toneMappingExposure !== undefined) {
141
+ const previous = renderer.toneMappingExposure;
142
+ renderer.toneMappingExposure = config.toneMappingExposure;
143
+ restores.push(() => {
144
+ renderer.toneMappingExposure = previous;
145
+ });
146
+ }
147
+ if (config.outputColorSpace !== undefined) {
148
+ const previous = renderer.outputColorSpace;
149
+ renderer.outputColorSpace = colorSpaces[config.outputColorSpace];
150
+ restores.push(() => {
151
+ renderer.outputColorSpace = previous;
152
+ });
153
+ }
154
+ if (config.shadowMapType !== undefined && renderer.shadowMap !== undefined) {
155
+ const previous = renderer.shadowMap.type;
156
+ renderer.shadowMap.type = shadowMapTypes[config.shadowMapType];
157
+ renderer.shadowMap.needsUpdate = true;
158
+ restores.push(() => {
159
+ renderer.shadowMap.type = previous;
160
+ renderer.shadowMap.needsUpdate = true;
161
+ });
162
+ }
163
+ if (config.clearColor !== undefined) {
164
+ const previousColor = new three.Color();
165
+ renderer.getClearColor(previousColor);
166
+ // Alpha is READ BACK and re-passed, never assumed: see `clearColor`'s doc above.
167
+ const alpha = renderer.getClearAlpha();
168
+ renderer.setClearColor(new three.Color(config.clearColor), alpha);
169
+ restores.push(() => {
170
+ renderer.setClearColor(previousColor, alpha);
171
+ });
172
+ }
173
+
174
+ return () => {
175
+ // Reverse order, so a field written twice (it cannot be, today) unwinds correctly.
176
+ for (let i = restores.length - 1; i >= 0; i--) restores[i]?.();
177
+ };
178
+ }
@@ -9,10 +9,10 @@
9
9
 
10
10
  import type { Container } from 'pixi.js';
11
11
  import type * as THREE from 'three';
12
- import type { WorldRendererConfig } from '../world3d-react/renderer-config';
13
12
  import type { AdapterSurface } from './adapter-surface';
14
13
  import type { AuthoringAdapter } from './authoring';
15
14
  import type { HostContextFor } from './host-context';
15
+ import type { WorldRendererConfig } from './renderer-config';
16
16
  import type { SystemAdapters } from './system-adapter';
17
17
 
18
18
  /**
@@ -72,6 +72,10 @@ export interface MountedRootBase {
72
72
 
73
73
  resize?(width: number, height: number): void;
74
74
  dispose(): void;
75
+ /** Resolves after an asynchronous native reconciler has run every component
76
+ * cleanup started by {@link dispose}. Omitted by synchronously-disposing
77
+ * roots. Hosts use this before auditing/reclaiming realm-owned resources. */
78
+ readonly disposeComplete?: Promise<void>;
75
79
 
76
80
  /** Optional capability providers — absence = "not supported", host degrades. */
77
81
  readonly authoring?: AuthoringAdapter;
@@ -94,7 +98,7 @@ export interface MountedThreeRoot extends MountedRootBase {
94
98
  /**
95
99
  * The colour pipeline this world was authored for, REPORTED rather than
96
100
  * applied — the adapter has already applied it to the renderer its own host
97
- * handed it (`world3d-react/renderer-config.ts`).
101
+ * handed it (`./renderer-config.ts`).
98
102
  *
99
103
  * It is here because the host that MOUNTS a world is not always the host
100
104
  * that DRAWS it. The editor's design session mounts against a
@@ -24,6 +24,7 @@ export const MOUNTED_ROOT_BASE_SHAPE = defineSeamShape<MountedRootBase>()({
24
24
  step: { optional: true, kind: 'function', required: 'effect' },
25
25
  resize: { optional: true, kind: 'function', required: 'effect' },
26
26
  dispose: { optional: false, kind: 'function', required: 'effect' },
27
+ disposeComplete: { optional: true, kind: 'value', required: 'effect' },
27
28
  authoring: { optional: true, kind: 'value', required: 'operation' },
28
29
  systems: { optional: true, kind: 'value', required: 'operation' },
29
30
  observe: { optional: true, kind: 'value', required: 'effect' },
@@ -64,7 +64,7 @@ import {
64
64
  type DebugRegistry,
65
65
  getDebugRegistry,
66
66
  } from '../runtime/debug-registry';
67
- import { devLayersEnabled } from '../runtime/dev-layers';
67
+ import { devBuildEnabled } from '../runtime/dev-build';
68
68
  import { disposeDebrisSubtree, type RootFrameHooks } from '../runtime/game';
69
69
  import type { EditorPreview, GameCleanup, GameContext, GameSetupFn } from '../runtime/types';
70
70
  import { type AudioContext as GameAudio, setupAudio } from '../setup/setup-audio';
@@ -591,8 +591,8 @@ export class SetupThreeRootAdapter implements RootAdapter {
591
591
  // registry, with no capability to install and nothing for a game to write.
592
592
  //
593
593
  // THE GATE, in one place. Three conditions, and the reason for each:
594
- // 1. `devLayersEnabled()` — the ONE owner of "is this a dev/editor
595
- // context" (`runtime/dev-layers.ts`). A ship build registers nothing,
594
+ // 1. `devBuildEnabled()` — the ONE owner of "is this a dev/editor
595
+ // context" (`runtime/dev-build.ts`). A ship build registers nothing,
596
596
  // subscribes to nothing, and never enables the profiler.
597
597
  // 2. a `Game` shell exists — the readings are folded out of that game's
598
598
  // profiler frames, and a bare mount has no profiler to fold.
@@ -620,12 +620,12 @@ export class SetupThreeRootAdapter implements RootAdapter {
620
620
  // The packaged runtime bakes `import.meta.env.DEV = false` into its
621
621
  // prebuilt bundle, which is a fact about how the EDITOR was built, not
622
622
  // about whether this mount is a dev session — found live: a
623
- // package-native project's vitals never seeded while its dev menu,
624
- // capabilities, and every project-graph dev layer ran fine. The
625
- // override argument is dev-layers' own documented seam for exactly
626
- // this caller-knows-better case; a standalone/ship mount (no
627
- // editorPreview) still resolves purely from the build env.
628
- if (!host.game || headless || !devLayersEnabled(editorPreview ? true : undefined)) return;
623
+ // package-native project's vitals never seeded while its capabilities
624
+ // and every other project-graph module ran fine. The override argument
625
+ // is `devBuildEnabled`'s own documented seam for exactly this
626
+ // caller-knows-better case; a standalone/ship mount (no editorPreview)
627
+ // still resolves purely from the build env.
628
+ if (!host.game || headless || !devBuildEnabled(editorPreview ? true : undefined)) return;
629
629
  renderVitals?.dispose();
630
630
  host.game.profiler.enabled = true;
631
631
  renderVitals = registerRenderVitals({