@vgai/engine 0.5.10 → 0.5.12

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 (157) hide show
  1. package/README.md +6 -5
  2. package/dist/adapter/authoring.d.ts +51 -28
  3. package/dist/adapter/authoring.d.ts.map +1 -1
  4. package/dist/adapter/body-marks.d.ts +59 -0
  5. package/dist/adapter/body-marks.d.ts.map +1 -0
  6. package/dist/adapter/body-marks.js +62 -0
  7. package/dist/adapter/constraint.d.ts +41 -0
  8. package/dist/adapter/constraint.d.ts.map +1 -0
  9. package/dist/adapter/constraint.js +27 -0
  10. package/dist/adapter/hierarchy-marks.d.ts +138 -0
  11. package/dist/adapter/hierarchy-marks.d.ts.map +1 -0
  12. package/dist/adapter/hierarchy-marks.js +148 -0
  13. package/dist/adapter/index.d.ts +1 -1
  14. package/dist/adapter/index.d.ts.map +1 -1
  15. package/dist/adapter/ingest/contract-system-adapters.d.ts +64 -0
  16. package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -0
  17. package/dist/adapter/ingest/contract-system-adapters.js +134 -0
  18. package/dist/adapter/ingest/game-contract.d.ts +89 -5
  19. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  20. package/dist/adapter/ingest/game-contract.js +11 -3
  21. package/dist/adapter/ingest/scene-capture.d.ts +31 -4
  22. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  23. package/dist/adapter/ingest/scene-capture.js +29 -7
  24. package/dist/adapter/ingest/structural-ids.d.ts +1 -1
  25. package/dist/adapter/ingest/structural-ids.js +2 -2
  26. package/dist/adapter/reflection-probe.d.ts +53 -0
  27. package/dist/adapter/reflection-probe.d.ts.map +1 -0
  28. package/dist/adapter/reflection-probe.js +18 -0
  29. package/dist/asset-formats/index.d.ts +4 -10
  30. package/dist/asset-formats/index.d.ts.map +1 -1
  31. package/dist/asset-formats/index.js +3 -8
  32. package/dist/asset-formats/material.d.ts +0 -66
  33. package/dist/asset-formats/material.d.ts.map +1 -1
  34. package/dist/asset-formats/material.js +0 -8
  35. package/dist/asset-formats/mesh.d.ts +1 -2
  36. package/dist/asset-formats/mesh.d.ts.map +1 -1
  37. package/dist/asset-formats/mesh.js +3 -13
  38. package/dist/asset-formats/render-env.d.ts +2 -2
  39. package/dist/asset-parse-error.d.ts +4 -5
  40. package/dist/asset-parse-error.d.ts.map +1 -1
  41. package/dist/asset-parse-error.js +4 -5
  42. package/dist/config.d.ts +7 -0
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js +7 -0
  45. package/dist/data/curve.d.ts +116 -0
  46. package/dist/data/curve.d.ts.map +1 -0
  47. package/dist/data/curve.js +230 -0
  48. package/dist/data/vite-plugin-data.d.ts +2 -2
  49. package/dist/data/vite-plugin-data.d.ts.map +1 -1
  50. package/dist/data/vite-plugin-data.js +24 -15
  51. package/dist/dev/performance-profiler.d.ts +1 -0
  52. package/dist/dev/performance-profiler.d.ts.map +1 -1
  53. package/dist/dev/performance-profiler.js +25 -2
  54. package/dist/dev/register-render-vitals.d.ts +18 -7
  55. package/dist/dev/register-render-vitals.d.ts.map +1 -1
  56. package/dist/dev/register-render-vitals.js +35 -8
  57. package/dist/ecs/user-data.d.ts +49 -1
  58. package/dist/ecs/user-data.d.ts.map +1 -1
  59. package/dist/ecs/user-data.js +34 -0
  60. package/dist/index.d.ts +2 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +2 -3
  63. package/dist/manifest/index.d.ts +3 -3
  64. package/dist/manifest/index.d.ts.map +1 -1
  65. package/dist/manifest/index.js +2 -2
  66. package/dist/manifest/load.d.ts +20 -63
  67. package/dist/manifest/load.d.ts.map +1 -1
  68. package/dist/manifest/load.js +12 -103
  69. package/dist/manifest/schema.d.ts +21 -103
  70. package/dist/manifest/schema.d.ts.map +1 -1
  71. package/dist/manifest/schema.js +66 -142
  72. package/dist/pixi/index.d.ts +2 -3
  73. package/dist/pixi/index.d.ts.map +1 -1
  74. package/dist/pixi/index.js +2 -3
  75. package/dist/pixi/ingest.d.ts +7 -75
  76. package/dist/pixi/ingest.d.ts.map +1 -1
  77. package/dist/pixi/ingest.js +6 -14
  78. package/dist/pixi/scene-capture.d.ts +2 -2
  79. package/dist/pixi/scene-capture.js +2 -2
  80. package/dist/react/use-data.d.ts +10 -5
  81. package/dist/react/use-data.d.ts.map +1 -1
  82. package/dist/react/use-data.js +15 -15
  83. package/dist/render/viewport-shading.d.ts +1 -1
  84. package/dist/render/viewport-shading.d.ts.map +1 -1
  85. package/dist/render/viewport-shading.js +14 -0
  86. package/dist/runtime/create-runtime.d.ts +12 -0
  87. package/dist/runtime/create-runtime.d.ts.map +1 -1
  88. package/dist/runtime/create-runtime.js +7 -2
  89. package/dist/runtime/mount-game.d.ts +1 -1
  90. package/dist/runtime/mount-game.d.ts.map +1 -1
  91. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  92. package/dist/runtime/mount-manifest.js +5 -1
  93. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
  94. package/dist/world3d-react/r3f-adapter.js +129 -6
  95. package/dist/world3d-react/renderer-config.d.ts +15 -0
  96. package/dist/world3d-react/renderer-config.d.ts.map +1 -1
  97. package/dist/world3d-react/renderer-config.js +15 -0
  98. package/dist/world3d-react/world-context.d.ts +11 -1
  99. package/dist/world3d-react/world-context.d.ts.map +1 -1
  100. package/dist/world3d-react/world-context.js +1 -0
  101. package/dist-config/config.js +140 -20
  102. package/dist-config/config.js.map +4 -4
  103. package/package.json +1 -1
  104. package/schemas/engine-capabilities.json +10 -10
  105. package/schemas/vgai-project.schema.json +22 -73
  106. package/src/adapter/authoring.ts +54 -31
  107. package/src/adapter/body-marks.ts +69 -0
  108. package/src/adapter/constraint.ts +71 -0
  109. package/src/adapter/hierarchy-marks.ts +155 -0
  110. package/src/adapter/index.ts +2 -3
  111. package/src/adapter/ingest/contract-system-adapters.ts +174 -0
  112. package/src/adapter/ingest/game-contract.ts +104 -6
  113. package/src/adapter/ingest/scene-capture.ts +61 -11
  114. package/src/adapter/ingest/structural-ids.ts +2 -2
  115. package/src/adapter/reflection-probe.ts +75 -0
  116. package/src/asset-formats/index.ts +4 -10
  117. package/src/asset-formats/material.ts +0 -11
  118. package/src/asset-formats/mesh.ts +3 -16
  119. package/src/asset-parse-error.ts +4 -5
  120. package/src/config.ts +13 -0
  121. package/src/data/curve.ts +263 -0
  122. package/src/data/vite-plugin-data.ts +48 -14
  123. package/src/dev/performance-profiler.ts +21 -2
  124. package/src/dev/register-render-vitals.ts +38 -8
  125. package/src/ecs/user-data.ts +50 -0
  126. package/src/index.ts +2 -3
  127. package/src/manifest/index.ts +3 -15
  128. package/src/manifest/load.ts +33 -139
  129. package/src/manifest/schema.ts +72 -181
  130. package/src/pixi/index.ts +2 -15
  131. package/src/pixi/ingest.ts +13 -87
  132. package/src/pixi/scene-capture.ts +2 -2
  133. package/src/react/use-data.ts +15 -20
  134. package/src/render/viewport-shading.ts +16 -1
  135. package/src/runtime/create-runtime.ts +30 -5
  136. package/src/runtime/mount-game.ts +1 -1
  137. package/src/runtime/mount-manifest.ts +5 -1
  138. package/src/world3d-react/r3f-adapter.tsx +142 -6
  139. package/src/world3d-react/renderer-config.ts +31 -0
  140. package/src/world3d-react/world-context.ts +11 -1
  141. package/dist/asset-formats/instances.d.ts +0 -17
  142. package/dist/asset-formats/instances.d.ts.map +0 -1
  143. package/dist/asset-formats/instances.js +0 -31
  144. package/dist/asset-formats/parse.d.ts +0 -20
  145. package/dist/asset-formats/parse.d.ts.map +0 -1
  146. package/dist/asset-formats/parse.js +0 -34
  147. package/dist/pixi/ingest-iframe.d.ts +0 -82
  148. package/dist/pixi/ingest-iframe.d.ts.map +0 -1
  149. package/dist/pixi/ingest-iframe.js +0 -166
  150. package/dist/render/instance-mesh.d.ts +0 -5
  151. package/dist/render/instance-mesh.d.ts.map +0 -1
  152. package/dist/render/instance-mesh.js +0 -19
  153. package/schemas/mat.schema.json +0 -255
  154. package/src/asset-formats/instances.ts +0 -38
  155. package/src/asset-formats/parse.ts +0 -39
  156. package/src/pixi/ingest-iframe.ts +0 -255
  157. package/src/render/instance-mesh.ts +0 -25
@@ -1,88 +1,20 @@
1
1
  import { installSceneCapture2D, type SceneCapture2DHandle } from './scene-capture';
2
2
 
3
- /**
4
- * A manifest's `extraDeps` resolve to TWO kinds of importmap entry:
5
- * `namespaces` are today's registry-known bare specifiers
6
- * (host-namespace-registry.ts), re-exported via a blob module
7
- * (`ingest-iframe-2d.ts`'s `makeHostNamespaceReexportUrl`); `urls` are the
8
- * D-Z3 fallback — a specifier that isn't registered but IS present in the
9
- * open project's own `node_modules`, mapped DIRECTLY to its
10
- * `/project-game-static/node_modules/<spec>/<esm entry>` URL (no host-side
11
- * import, no blob — the dep executes in the iframe realm and its own
12
- * transitive `import 'pixi.js'` resolves through the same importmap back to
13
- * the host pixi trap). All probing/decision logic (which kind a specifier
14
- * resolves to, exports-map parsing, anti-shim throws) lives editor-side
15
- * (`host-namespace-registry.ts`) — this engine module only carries the
16
- * ALREADY-DECIDED shape; `ingest-iframe-2d.ts` stays decision-free and just
17
- * merges both maps into the iframe importmap.
18
- */
19
- export interface ExtraDepsResolution2D {
20
- namespaces: Record<string, Record<string, unknown>>;
21
- urls: Record<string, string>;
22
- }
23
-
24
3
  /** A registered unmodified PixiJS game (the Pixi analog of `IngestGame`). */
25
4
  export interface IngestGame2D {
26
5
  id: string;
27
6
  name: string;
28
7
  description: string;
29
- /** Shared-pixi path: import + run the unmodified game module. */
30
- load?: () => Promise<unknown>;
31
- /** Tier ceiling: how reachable the game's pixi instance is. */
32
- tier?: 'shared' | 'iframe-reachable' | 'opaque-embed';
8
+ /** Import + run the unmodified game module in the host's own realm. */
9
+ load: () => Promise<unknown>;
10
+ /** How long to wait for the game's first captured frame before failing. */
33
11
  captureTimeoutMs?: number;
34
- /**
35
- * IFRAME-REACHABLE-MULTI fields (Track P): an externalized built bundle
36
- * mounted via `mountIngestGame2DIframeReachableMulti`
37
- * (`./ingest-iframe-2d.ts`) instead of `load()` — populated only when `tier`
38
- * is `'iframe-reachable'` AND `load` is absent. Mirror
39
- * `IframeReachableMultiOpts`'s fields, EXCEPT `extraDeps`/`pixiNamespace` are
40
- * THUNKS here, not resolved values — the same "lazy, only invoked at mount
41
- * time" shape `load` already uses, so building this descriptor
42
- * (`resolveIngest2DDescriptor` / the canvas surface's
43
- * `ingest/surface-canvas.ts`, editor-side — Wave 0.6 replaced the three
44
- * per-surface `discovery*.ts` modules with one manifest-driven registry) never
45
- * has to be async just to wire up which specifiers/ version-skew URL a game
46
- * needs; only actually MOUNTING it (the ingest-mode mount bridge) pays for the
47
- * dynamic import of `@pixi/sound`/`gsap`/spine/ a standalone pixi build.
48
- */
49
- bundleUrl?: string;
50
- /** <base href> for the iframe (see `IframeReachableMultiOpts.baseHref`). */
51
- baseHref?: string;
52
- /** Host-side Assets.init basePath remap (see `IframeReachableMultiOpts.assetBaseUrl`). */
53
- assetBaseUrl?: string;
54
- /**
55
- * Lazily resolve this game's bare-specifier -> extraDeps resolution via the
56
- * editor's host-namespace registry. Absent (or omitted) means no extra deps
57
- * beyond pixi.js.
58
- */
59
- extraDeps?: () => Promise<ExtraDepsResolution2D>;
60
- /**
61
- * Lazily resolve the trapped-pixi namespace for version-skewed games (a
62
- * dynamic `import()` of the manifest's `pixiModuleUrl`) — absent means
63
- * "mount against the host's own `pixi.js`" (no skew).
64
- */
65
- pixiNamespace?: () => Promise<unknown>;
66
- /** DOM the game expects before boot (see `IframeReachableMultiOpts.bodyHtml`). */
67
- bodyHtml?: string;
68
- /**
69
- * Lazily read an OPAQUE prebuilt bundle's self-contained HTML (D-W2) — the pixi
70
- * twin of `IngestGame.embedHtml`. Always a lazy thunk here (no in-tree Pixi
71
- * opaque-embed fixture exists yet to need a plain-string form, unlike the three
72
- * side): `resolveIngest2DDescriptor` (adapter-resolver.ts) builds one that reads
73
- * the declared `entryHtml` file's bytes through the `/project-game-static/`
74
- * route, only when the game is actually mounted. The host runs the resolved HTML
75
- * in a sandboxed iframe at the embed-only floor (Tier 0-1),
76
- * `mountIngestGame2DEmbed` (`./ingest-iframe-2d.ts`).
77
- */
78
- embedHtml?: () => Promise<string>;
79
12
  }
80
13
 
81
14
  export interface IngestMount2D {
82
- /** The captured live stage Container (null if capture failed/degraded). */
83
- stage: unknown | null;
15
+ /** The captured live stage Container. */
16
+ stage: unknown;
84
17
  capture: SceneCapture2DHandle;
85
- embedOnly: boolean;
86
18
  setPaused(paused: boolean): void;
87
19
  dispose(): void;
88
20
  }
@@ -91,9 +23,8 @@ export interface IngestMount2D {
91
23
  * Mount an UNMODIFIED PixiJS game via the shared-instance capture path — the 2D
92
24
  * analog of `mountIngestGame`. Installs the render trap on the host's pixi
93
25
  * instance, runs the game's `load()`, waits for the game to render its first
94
- * frame (capturing its live stage), and exposes loop gating + cleanup. If capture
95
- * times out (pixi unreachable / bundled / mismatched), degrades to embed-only
96
- * never a silent failure.
26
+ * frame (capturing its live stage), and exposes loop gating + cleanup. A game
27
+ * whose pixi the trap cannot reach never captures, and the mount FAILS by name.
97
28
  */
98
29
  export async function mountIngestGame2D(
99
30
  pixiNamespace: unknown,
@@ -102,7 +33,7 @@ export async function mountIngestGame2D(
102
33
  ): Promise<IngestMount2D> {
103
34
  const capture = installSceneCapture2D(pixiNamespace);
104
35
  try {
105
- await game.load?.();
36
+ await game.load();
106
37
  } catch (err) {
107
38
  capture.uninstall();
108
39
  throw new Error(`pixi ingest: game "${game.id}" failed to load: ${err}`);
@@ -114,18 +45,13 @@ export async function mountIngestGame2D(
114
45
  return {
115
46
  stage: rt.stage,
116
47
  capture,
117
- embedOnly: false,
118
48
  setPaused: (p) => capture.setPaused(p),
119
49
  dispose: () => capture.uninstall(),
120
50
  };
121
- } catch {
122
- // Capture failed → pixi unreachable/mismatched → degrade to embed-only.
123
- return {
124
- stage: null,
125
- capture,
126
- embedOnly: true,
127
- setPaused: () => {},
128
- dispose: () => capture.uninstall(),
129
- };
51
+ } catch (err) {
52
+ capture.uninstall();
53
+ throw new Error(
54
+ `pixi ingest: game "${game.id}" rendered no capturable frame within ${timeout}ms: ${err}`,
55
+ );
130
56
  }
131
57
  }
@@ -14,8 +14,8 @@
14
14
  * CRITICAL (same module-identity gatekeeper as the 3D path): the trap must be
15
15
  * installed on the SAME `pixi.js` module instance the game uses. A bundler/dev-server
16
16
  * that dedupes `pixi.js` makes an ESM game's `import 'pixi.js'` resolve to the host
17
- * instance, so it is trapped; a game bundling its own pixi cannot be captured this
18
- * way (degrade to iframe-reachable or opaque-embed tiers — see the spec).
17
+ * instance, so it is trapped; a game bundling its own pixi cannot be captured at
18
+ * all, and its mount fails by name.
19
19
  */
20
20
 
21
21
  /** A live Pixi runtime captured from an external PixiJS game. */
@@ -59,11 +59,16 @@ export function useData<T>(handle: DataHandle<T>): T {
59
59
  * this rejects with a teaching error (a shipped game could never reach the
60
60
  * route anyway — builds strip `src/tools/` entirely, §4).
61
61
  *
62
- * Serialization matches the editor Data panel's diff-minimal contract:
63
- * `"$schema"` first, 2-space indent, trailing newline. When `value` carries
64
- * no `"$schema"` key (the common case`DataHandle.get()` strips it), the
65
- * conventional sibling reference (`./<name>.schema.json`, §2.1) is restored
66
- * so a tool write never silently drops VS Code validation from the file.
62
+ * The body is PARSED VALUES, never serialized text: the dev server's fold
63
+ * (`packages/editor/server/data-file-serialize.ts`) owns key order and the
64
+ * `"$schema"`-first diff-minimal serialization, and it must a parsed JS
65
+ * object has already reindexed integer-like keys ("1","2","10"), so only the
66
+ * server, which still has the file's raw text, can keep a one-field write a
67
+ * one-line diff. When `value` carries no `"$schema"` key (the common case —
68
+ * `DataHandle.get()` strips it), the conventional sibling reference
69
+ * (`./<name>.schema.json`, §2.1) is restored so a tool write never silently
70
+ * drops VS Code validation from the file. Undo for data writes is git;
71
+ * every accepted write is recorded server-side as a project revision.
67
72
  */
68
73
  export async function writeData(
69
74
  projectRelativePath: string,
@@ -77,9 +82,9 @@ export async function writeData(
77
82
  );
78
83
  }
79
84
 
80
- // "$schema" first for minimal diffs; restore the conventional sibling
81
- // reference when the caller's object (typically `{ ...handle.get() }`,
82
- // which never carries it) omits the line.
85
+ // Restore the conventional sibling "$schema" reference when the caller's
86
+ // object (typically `{ ...handle.get() }`, which never carries it) omits
87
+ // it; the server fold serializes it first.
83
88
  const { $schema, ...rest } = value;
84
89
  const schemaRef =
85
90
  $schema ??
@@ -87,24 +92,14 @@ export async function writeData(
87
92
  .split('/')
88
93
  .pop()
89
94
  ?.replace(/\.data\.json$/, '')}.schema.json`;
90
- const content = `${JSON.stringify({ $schema: schemaRef, ...rest }, null, 2)}\n`;
91
-
92
- const transactionalWriter = (
93
- globalThis as typeof globalThis & {
94
- __vgaiHistoryWriteData?: (path: string, content: string) => Promise<void>;
95
- }
96
- ).__vgaiHistoryWriteData;
97
- if (transactionalWriter) {
98
- await transactionalWriter(projectRelativePath, content);
99
- return;
100
- }
95
+ const values = { $schema: schemaRef, ...rest };
101
96
 
102
97
  let res: Response;
103
98
  try {
104
99
  res = await fetch('/__editor/data-file', {
105
100
  method: 'POST',
106
101
  headers: { 'Content-Type': 'application/json' },
107
- body: JSON.stringify({ path: projectRelativePath, content }),
102
+ body: JSON.stringify({ path: projectRelativePath, values }),
108
103
  });
109
104
  } catch (err) {
110
105
  throw new Error(
@@ -1,9 +1,10 @@
1
1
  import * as THREE from 'three';
2
2
 
3
3
  /** Temporary developer-facing shading modes. These never become scene data. */
4
- export type ViewportShadingMode = 'solid' | 'unlit' | 'wireframe' | 'normals' | 'overdraw';
4
+ export type ViewportShadingMode = 'solid' | 'clay' | 'unlit' | 'wireframe' | 'normals' | 'overdraw';
5
5
 
6
6
  type MaterialPair = {
7
+ clay: THREE.MeshStandardMaterial;
7
8
  unlit: THREE.MeshBasicMaterial;
8
9
  wireframe: THREE.MeshBasicMaterial;
9
10
  };
@@ -73,6 +74,7 @@ export class ViewportShadingRenderer {
73
74
 
74
75
  dispose(): void {
75
76
  for (const pair of this._derived.values()) {
77
+ pair.clay.dispose();
76
78
  pair.unlit.dispose();
77
79
  pair.wireframe.dispose();
78
80
  }
@@ -106,6 +108,19 @@ export class ViewportShadingRenderer {
106
108
  fog: (material as THREE.Material & { fog?: boolean }).fog ?? true,
107
109
  };
108
110
  pair = {
111
+ clay: new THREE.MeshStandardMaterial({
112
+ color: 0xaeb6c0,
113
+ roughness: 0.82,
114
+ metalness: 0,
115
+ flatShading: true,
116
+ alphaMap: textureOf(material, 'alphaMap'),
117
+ alphaTest: material.alphaTest,
118
+ opacity: material.opacity,
119
+ transparent: material.transparent,
120
+ side: material.side,
121
+ depthTest: material.depthTest,
122
+ depthWrite: material.depthWrite,
123
+ }),
109
124
  unlit: new THREE.MeshBasicMaterial(common),
110
125
  wireframe: new THREE.MeshBasicMaterial({ ...common, wireframe: true }),
111
126
  };
@@ -280,6 +280,18 @@ export interface RootsRuntimeConfig {
280
280
  * a 2D canvas renderer with no GPU. Never set `true` in a real host.
281
281
  */
282
282
  headless?: boolean | undefined;
283
+ /**
284
+ * `vgai.project.json`'s `rendering.antialias` — whether every three root's
285
+ * WebGL context is built with a multisampled drawing buffer.
286
+ *
287
+ * It is a RUNTIME-CONFIG option rather than a world's own
288
+ * `WorldRendererConfig` field for the reason that file's header gives: a
289
+ * context's sample count is fixed at CREATION, before any world exists, so
290
+ * there is no honest moment at which a world could ask for it. The
291
+ * manifest-aware boot path (`mount-manifest.ts`) is the real reader;
292
+ * omitting it leaves `createHostRenderer`'s own registry default alone.
293
+ */
294
+ antialias?: boolean | undefined;
283
295
  /** D15 (T-D15.1) — the root seed `ctx.random` boots from on every world
284
296
  * mounted onto this session's Game, forwarded to `createGame` BEFORE any
285
297
  * world's `mount()`/`setup()` runs (this function constructs the Game
@@ -414,6 +426,9 @@ interface OneRootContext {
414
426
  readonly dpr: number;
415
427
  readonly isBottom: boolean;
416
428
  readonly headless: boolean;
429
+ /** `RootsRuntimeConfig.antialias` — see that field. Undefined leaves
430
+ * `createHostRenderer`'s own registry default alone. */
431
+ readonly antialias: boolean | undefined;
417
432
  readonly assets: ReturnType<typeof createAssetCache>;
418
433
  }
419
434
 
@@ -425,13 +440,22 @@ async function mountOneThreeRoot(
425
440
  spec: ThreeRootMountSpec,
426
441
  ctx: OneRootContext,
427
442
  ): Promise<OneRootResult> {
428
- const { game, canvas, w, h, dpr, isBottom, headless, assets } = ctx;
443
+ const { game, canvas, w, h, dpr, isBottom, headless, assets, antialias } = ctx;
429
444
  const renderer = headless
430
445
  ? createHeadlessRendererStub()
431
- : createHostRenderer(canvas, w, h, undefined, {
432
- alpha: !isBottom,
433
- preserveDrawingBuffer: true,
434
- });
446
+ : createHostRenderer(
447
+ canvas,
448
+ w,
449
+ h,
450
+ // The manifest's `rendering.antialias` reaches the WebGL context here and NOWHERE else:
451
+ // `createHostRenderer` passes it straight to the constructor, and the sample count is
452
+ // fixed from that moment. Omitted → the render-settings registry's own default.
453
+ antialias === undefined ? undefined : { antialias },
454
+ {
455
+ alpha: !isBottom,
456
+ preserveDrawingBuffer: true,
457
+ },
458
+ );
435
459
  renderer.setSize(w, h, false); // backing resolution only — CSS stacking owns layout size
436
460
  // Defect E4.R1 (Fable review): `createHostRenderer`'s OWN construction-time
437
461
  // `setSize` call (`setup-renderer.ts`, `updateStyle` defaulting `true` —
@@ -896,6 +920,7 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
896
920
  dpr,
897
921
  headless,
898
922
  assets,
923
+ antialias: config.antialias,
899
924
  });
900
925
 
901
926
  // --- Delegating input router (D5 §2a) ---
@@ -56,7 +56,7 @@ export interface ManifestHost {
56
56
  /** Resolve asset/scene URLs relative to the project — forwarded to a
57
57
  * registered {@link AdapterSurfaceFactory} via {@link AdapterSurfaceFactoryContext},
58
58
  * never called by this module itself (a factory only needs it for a
59
- * non-default resolution scheme, e.g. a hosted-editor iframe). */
59
+ * non-default resolution scheme, e.g. a project served under a base path). */
60
60
  resolveUrl?(path: string): string;
61
61
  readonly width?: number | undefined;
62
62
  readonly height?: number | undefined;
@@ -276,7 +276,7 @@ function resolveThreeAdapter(
276
276
  throw new Error(
277
277
  `mountManifestRoots: world "${world.id}" (three) declares an { ingest } adapter — ` +
278
278
  "ingest roots require the editor's dev-server-backed mount machinery (an EditorStore " +
279
- "plus iframe/DOM capture, see adapter-resolver.ts's resolveIngestThreeAdapter) and are " +
279
+ "plus in-realm scene capture, see adapter-resolver.ts's resolveIngestThreeAdapter) and are " +
280
280
  'not supported by mountManifestRoots (no porting aids, no hosted ingest routes).',
281
281
  );
282
282
  }
@@ -442,6 +442,10 @@ export async function mountManifestRoots(opts: MountManifestOptions): Promise<Ga
442
442
  width: opts.width ?? manifest.resolution?.width,
443
443
  height: opts.height ?? manifest.resolution?.height,
444
444
  headless: opts.headless,
445
+ // `rendering.antialias` reaches the WebGL context at CONSTRUCTION and can be honoured
446
+ // nowhere else — see the manifest schema's own `rendering` block and
447
+ // `world3d-react/renderer-config.ts`'s header for why it is not a world-level declaration.
448
+ ...(manifest.rendering === undefined ? {} : { antialias: manifest.rendering.antialias }),
445
449
  seed: resolvedSeed,
446
450
  playtest: opts.playtest,
447
451
  });
@@ -28,11 +28,30 @@ import {
28
28
  } from '@react-three/fiber';
29
29
  import { createElement, type ReactNode } from 'react';
30
30
  import type { MountedThreeRoot, RootAdapter, ThreeHostContext } from '../adapter';
31
+ import { type RenderVitalsRegistration, registerRenderVitals } from '../dev/register-render-vitals';
32
+ import {
33
+ createRenderDebugAdapter,
34
+ frameCaptureContextFor,
35
+ type RenderDebugWiring,
36
+ } from '../dev/render-debug-adapter';
37
+ import { collectRenderMemory } from '../dev/render-memory';
38
+ import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
39
+ import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
31
40
  import { WorldProvider } from '../react/world-state';
41
+ import { devLayersEnabled } from '../runtime/dev-layers';
32
42
  import { EngineBridge, type EngineBridgeValue } from './engine-bridge';
33
43
  import { applyWorldRendererConfig, type WorldRendererConfig } from './renderer-config';
34
44
  import { createR3FRootContext } from './world-context';
35
45
 
46
+ /** The slice of `WebGLRenderer.info` the vitals reporter reads. Declared
47
+ * structurally rather than imported from `three`, per this module's own
48
+ * identity rule (see the header): a duck-typed host renderer legitimately has
49
+ * no `info` at all, which is why every read of it is guarded. */
50
+ interface RendererCounters {
51
+ readonly render: { readonly calls: number; readonly triangles: number; readonly frame: number };
52
+ readonly memory: { readonly geometries: number; readonly textures: number };
53
+ }
54
+
36
55
  /** What {@link createR3FAdapter} needs to build one `RootAdapter`. */
37
56
  export interface CreateR3FAdapterOptions {
38
57
  /** Stable id (telemetry/registry/conformance) — `RootAdapter.id`. */
@@ -336,6 +355,73 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
336
355
  // supported way to see the live root. It is a plain map read.
337
356
  const live = (): RootState => state.get();
338
357
 
358
+ // --- Live render vitals (issue #1504), for an R3F world -----------------
359
+ // The SAME door the classic adapter seeds
360
+ // (`../adapter/setup-three-root-adapter.ts`), seeded here for the same
361
+ // reason: a running game must be able to explain its own frame cost
362
+ // through the debug registry, with no capability to install and nothing
363
+ // for a game to write. It has to be seeded in BOTH adapters because they
364
+ // are two independent mount paths onto the same `Game` — and a TSX/R3F
365
+ // world is the only shape a scaffolded project's three root has, so a
366
+ // door wired only into the classic path is a door no scaffolded project
367
+ // ever gets (issue #1518: `game.state('render.vitals')` answered "no
368
+ // state provider registered" in the field while every other dev layer of
369
+ // the same world worked).
370
+ //
371
+ // The gate is the same three conditions, minus the one that cannot
372
+ // arise: `devLayersEnabled()` (`../runtime/dev-layers.ts`, the ONE owner
373
+ // of "is this a dev context" — a ship build registers nothing and pays
374
+ // nothing), and a `Game` shell (the readings are folded out of that
375
+ // game's profiler frames). "Not headless" needs no term here: a headless
376
+ // mount returned above, before fiber was ever configured.
377
+ //
378
+ // Two module instances, one answer: under the packaged editor this
379
+ // module is served out of the PROJECT's own Vite graph (see
380
+ // `packages/editor/vite-plugin-react-world-runtime.ts` — the packaged
381
+ // shell imports `resolveR3FEntryAdapter` through the project-rooted
382
+ // server precisely so fiber/react/three identity is the project's), so
383
+ // `import.meta.env.DEV` here is the project server's `true`, not the
384
+ // prebuilt shell's `false`.
385
+ //
386
+ // OWNER of the registration+fold: this mount, ended by `dispose()` below
387
+ // (the fold) and `runtime.dispose()`'s scoped `strip(id)` (the
388
+ // registrations) — the same split the classic adapter documents.
389
+ let renderVitals: RenderVitalsRegistration | null = null;
390
+ if (host.game && devLayersEnabled()) {
391
+ // Same auto-enable as the classic adapter: the readings are folded out
392
+ // of profiler frames, and the profiler is a flag the editor already
393
+ // sets on play — under the dev gate this is the same cost arriving a
394
+ // little earlier, not a new one.
395
+ host.game.profiler.enabled = true;
396
+ renderVitals = registerRenderVitals({
397
+ registry: runtime.debugRegistry,
398
+ worldId: id,
399
+ profiler: host.game.profiler,
400
+ scene: live().scene,
401
+ renderer: host.renderer,
402
+ });
403
+ }
404
+
405
+ // Engine-owned render introspection, built over the SAME renderer and
406
+ // scene Fiber uses. A design-time stand-in has no context and a
407
+ // headless mount returned above, so both honestly omit the capability.
408
+ // `frameCaptureContextFor` is the same construction gate as the classic
409
+ // Three adapter; no editor-side renderer inference is involved.
410
+ const rendererContext =
411
+ typeof host.renderer.getContext === 'function' ? host.renderer.getContext() : undefined;
412
+ const frameCaptureContext = frameCaptureContextFor(false, rendererContext);
413
+ const frameCapture = frameCaptureContext
414
+ ? createWebGLFrameCapture(frameCaptureContext)
415
+ : null;
416
+ const renderDebugWiring: RenderDebugWiring | null = frameCapture
417
+ ? createRenderDebugAdapter({
418
+ capture: frameCapture,
419
+ scene: state.scene,
420
+ memory: () => collectRenderMemory(live().scene, host.renderer.info),
421
+ })
422
+ : null;
423
+ if (renderDebugWiring) runtime.systemAdapters.renderDebug = renderDebugWiring.adapter;
424
+
339
425
  // The engine drives every `useFrame` through the mounted world's
340
426
  // `update(dt)` hook, never off a raw host-loop callback. That is the
341
427
  // whole pause story: `runFrameImpl` (`runtime/game.ts`) calls
@@ -388,9 +474,10 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
388
474
  return live().camera;
389
475
  },
390
476
  drivesOwnLoop: false,
391
- // Adapter surface: `debug` pre-seeded (the shared game registry's
392
- // adapter); game code adds capabilities via
393
- // `ctx.registerSystemAdapter` exactly as in a classic world.
477
+ // Adapter surface: `debug` (the shared game registry) and, when a real
478
+ // WebGL2 context exists, `renderDebug` are engine-seeded. Game code
479
+ // adds its own capabilities via `ctx.registerSystemAdapter` exactly
480
+ // as in a classic world.
394
481
  systems: runtime.systemAdapters,
395
482
  update(dt: number): void {
396
483
  elapsed += dt;
@@ -408,15 +495,64 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
408
495
  // then fiber's advance (useFrame callbacks + the actual render see
409
496
  // the fresh state). Both are host-gated: neither runs while frozen.
410
497
  systems.run(dt);
411
- advance(elapsed, true, current);
498
+ renderDebugWiring?.beforeRender();
499
+ try {
500
+ const profiler = host.game?.profiler;
501
+ if (!profiler?.enabled) {
502
+ advance(elapsed, true, current);
503
+ return;
504
+ }
505
+ // Issue #1504's measurement, produced where the draw actually
506
+ // happens. `advance()` under `frameloop: 'never'` is what calls
507
+ // `gl.render(scene, camera)`, so this bracket — not a phase span
508
+ // inside `systems` — is the frame's CPU submission cost, and it is
509
+ // also what makes this frame a PRESENTATION as far as the vitals
510
+ // fold is concerned (`../dev/render-vitals.ts`: no bracket, no
511
+ // display frame). The profiler's phase clock is a stack, so
512
+ // bracketing here cannot truncate an enclosing phase.
513
+ const info = host.renderer.info as RendererCounters | undefined;
514
+ const passesBefore = info?.render.frame ?? 0;
515
+ profiler.beginPhase();
516
+ try {
517
+ advance(elapsed, true, current);
518
+ } finally {
519
+ profiler.endPhase(RENDER_SUBMIT_PHASE);
520
+ }
521
+ // Checked, not assumed — a duck-typed/design-time renderer supplies
522
+ // only what a mount needs, and a mount with no counters must report
523
+ // NOTHING rather than throw once per frame (the classic adapter's
524
+ // `postFrame` pays for the same assumption for the same reason).
525
+ // `gpuMs` is null: this path runs no GPU timer, and the profiler
526
+ // keeps the previous reading rather than inventing a zero.
527
+ if (info) {
528
+ profiler.reportRender({
529
+ gpuMs: null,
530
+ drawCalls: info.render.calls,
531
+ triangles: info.render.triangles,
532
+ geometries: info.memory.geometries,
533
+ textures: info.memory.textures,
534
+ renderPasses: info.render.frame - passesBefore,
535
+ });
536
+ }
537
+ } finally {
538
+ renderDebugWiring?.afterRender();
539
+ }
412
540
  },
413
541
  resize(width: number, height: number): void {
414
542
  live().setSize(width, height);
415
543
  },
416
544
  dispose(): void {
417
- // Unmount FIRST so component effect cleanups run, then tear the
418
- // runtime down (scoped debug-registry strip).
545
+ // Reject/restore a pending capture while its renderables still
546
+ // exist, then unmount component effects and strip runtime state.
547
+ renderDebugWiring?.dispose();
419
548
  root.unmount();
549
+ // The vitals fold owns two `profiler.subscribe` registrations, and
550
+ // `runtime.dispose()`'s `strip(id)` removes the REGISTRATIONS only —
551
+ // this is the one path that ends the subscriptions, exactly as the
552
+ // classic adapter's teardown documents. Before the strip, so a
553
+ // subscription can never outlive the door it feeds.
554
+ renderVitals?.dispose();
555
+ renderVitals = null;
420
556
  runtime.dispose();
421
557
  restoreRendererConfig();
422
558
  },
@@ -47,11 +47,27 @@ export type WorldToneMapping =
47
47
  */
48
48
  export type WorldOutputColorSpace = 'srgb' | 'srgb-linear';
49
49
 
50
+ /**
51
+ * The shadow-map filter, named as data rather than as three's numeric enum.
52
+ *
53
+ * This is a renderer INSTANCE property (`WebGLRenderer.shadowMap.type`), not a context attribute,
54
+ * so unlike MSAA it has an honest moment at which a world can ask for it — which is the whole test
55
+ * this file's header states. A source engine that declares its own shadow filter (Godot 3's
56
+ * `rendering/quality/shadows/filter_mode`) would otherwise inherit whatever the host built with.
57
+ */
58
+ export type WorldShadowMapType = 'basic' | 'pcf' | 'pcf-soft' | 'vsm';
59
+
50
60
  /** What a world may declare about the renderer that draws it. Every field is optional; see header. */
51
61
  export interface WorldRendererConfig {
52
62
  readonly toneMapping?: WorldToneMapping | undefined;
53
63
  readonly toneMappingExposure?: number | undefined;
54
64
  readonly outputColorSpace?: WorldOutputColorSpace | undefined;
65
+ /**
66
+ * `WebGLRenderer.shadowMap.type`. Writing it after a shadow map has already been built needs
67
+ * `shadowMap.needsUpdate`, which this function sets — three caches the compiled depth material
68
+ * per type and would otherwise keep filtering with the previous one.
69
+ */
70
+ readonly shadowMapType?: WorldShadowMapType | undefined;
55
71
  /**
56
72
  * The colour the frame is cleared to, as a CSS hex string. The renderer's existing clear ALPHA
57
73
  * is preserved: a stacked canvas is transparent on purpose (`create-runtime.ts` gives every
@@ -95,6 +111,12 @@ export function applyWorldRendererConfig(
95
111
  srgb: three.SRGBColorSpace,
96
112
  'srgb-linear': three.LinearSRGBColorSpace,
97
113
  };
114
+ const shadowMapTypes: Record<WorldShadowMapType, THREE.ShadowMapType> = {
115
+ basic: three.BasicShadowMap,
116
+ pcf: three.PCFShadowMap,
117
+ 'pcf-soft': three.PCFSoftShadowMap,
118
+ vsm: three.VSMShadowMap,
119
+ };
98
120
 
99
121
  const restores: (() => void)[] = [];
100
122
 
@@ -119,6 +141,15 @@ export function applyWorldRendererConfig(
119
141
  renderer.outputColorSpace = previous;
120
142
  });
121
143
  }
144
+ if (config.shadowMapType !== undefined && renderer.shadowMap !== undefined) {
145
+ const previous = renderer.shadowMap.type;
146
+ renderer.shadowMap.type = shadowMapTypes[config.shadowMapType];
147
+ renderer.shadowMap.needsUpdate = true;
148
+ restores.push(() => {
149
+ renderer.shadowMap.type = previous;
150
+ renderer.shadowMap.needsUpdate = true;
151
+ });
152
+ }
122
153
  if (config.clearColor !== undefined) {
123
154
  const previousColor = new three.Color();
124
155
  renderer.getClearColor(previousColor);
@@ -130,8 +130,17 @@ export interface R3FRootRuntime {
130
130
  /** The world-local phase runner `mount().update(dt)` ticks. */
131
131
  readonly systems: ReturnType<typeof createSystemRunner>;
132
132
  /** The mounted world's `SystemAdapters` bag (`mounted.systems`) —
133
- * `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded. */
133
+ * `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded here,
134
+ * and the R3F mount adds renderer-backed engine capabilities after commit. */
134
135
  readonly systemAdapters: SystemAdapters;
136
+ /** The registry every registration of this world's lands in — the game's
137
+ * own when a `Game` hosts it, a private mount-local one otherwise. Exposed
138
+ * (rather than re-derived by the caller) because `createR3FAdapter` seeds
139
+ * the render-vitals door onto THIS registry after fiber's first commit
140
+ * resolves the scene, and two `getDebugRegistry` calls that are "obviously"
141
+ * the same object is exactly the second source of truth the classic
142
+ * adapter's single `debugRegistry` local avoids. */
143
+ readonly debugRegistry: DebugRegistry;
135
144
  /** Resolves once the input map load settles (immediately when skipped).
136
145
  * Never rejects — a failed load reports loudly and resolves. */
137
146
  readonly inputMapReady: Promise<void>;
@@ -288,6 +297,7 @@ export function createR3FRootContext(
288
297
  ctx,
289
298
  systems,
290
299
  systemAdapters,
300
+ debugRegistry,
291
301
  inputMapReady,
292
302
  setSceneCamera(scene, camera): void {
293
303
  const sameScene = ctxRaw['scene'] === scene && sceneIndex !== null;
@@ -1,17 +0,0 @@
1
- import { z } from 'zod';
2
- /**
3
- * `.instances.json` — a declarative, authored-data instance-transform asset
4
- * (F3). Referenced by `mesh.instances` as a bare array of 10-number tuples,
5
- * one per instance, each `[posX,posY,posZ, quatX,quatY,quatZ,quatW,
6
- * scaleX,scaleY,scaleZ]`. The scene loader fetches+parses this file and
7
- * renders it as one `THREE.InstancedMesh` (one draw call) — bulk transform
8
- * data is referenced as an asset, never inlined into the referencing
9
- * document (gate G4).
10
- *
11
- * WO-8: the document that referenced it was `.vscn.json`, and that format is
12
- * deleted. `.instances.json` survives as a FETCHED-ASSET format with its own
13
- * `parseInstancesFile` — nothing in the shipped tree references one today.
14
- */
15
- export declare const InstancesFileSchema: z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
16
- export type InstancesFile = z.infer<typeof InstancesFileSchema>;
17
- //# sourceMappingURL=instances.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"instances.d.ts","sourceRoot":"","sources":["../../src/asset-formats/instances.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,kKAoB7B,CAAC;AAEJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}