@vgai/engine 0.2.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 (147) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +35 -0
  3. package/package.json +55 -0
  4. package/src/adapter/authoring.ts +402 -0
  5. package/src/adapter/colyseus-networking-adapter.ts +72 -0
  6. package/src/adapter/first-party-systems.ts +103 -0
  7. package/src/adapter/game-adapter.ts +151 -0
  8. package/src/adapter/host-context.ts +77 -0
  9. package/src/adapter/index.ts +85 -0
  10. package/src/adapter/ingest/game-contract.ts +59 -0
  11. package/src/adapter/ingest/overlay-applier.ts +207 -0
  12. package/src/adapter/ingest/overlay-apply.ts +124 -0
  13. package/src/adapter/ingest/overlay-file.ts +126 -0
  14. package/src/adapter/ingest/overlay-report.ts +176 -0
  15. package/src/adapter/ingest/scene-capture.ts +307 -0
  16. package/src/adapter/ingest/upstream-pin.ts +52 -0
  17. package/src/adapter/loop-gate-report.ts +54 -0
  18. package/src/adapter/rapier-physics-adapter.ts +56 -0
  19. package/src/adapter/system-adapter.ts +154 -0
  20. package/src/adapter/transform.ts +18 -0
  21. package/src/adapter/vgai-scene-game-adapter.ts +886 -0
  22. package/src/adapter/world-kind.ts +34 -0
  23. package/src/ai/navigation.ts +164 -0
  24. package/src/animation/anim-graph-types.ts +56 -0
  25. package/src/animation/anim-graph.ts +406 -0
  26. package/src/animation/anim-system.ts +28 -0
  27. package/src/animation/blend-node.ts +119 -0
  28. package/src/animation/property-track.ts +178 -0
  29. package/src/animation/schema.ts +204 -0
  30. package/src/assets.ts +80 -0
  31. package/src/audio/ambient.ts +300 -0
  32. package/src/audio/impacts.ts +212 -0
  33. package/src/audio/index.ts +7 -0
  34. package/src/audio/movement.ts +140 -0
  35. package/src/audio/musical.ts +200 -0
  36. package/src/audio/ui-sounds.ts +171 -0
  37. package/src/audio/vehicle.ts +235 -0
  38. package/src/audio/weapons.ts +152 -0
  39. package/src/core/game-loop.ts +127 -0
  40. package/src/core/system-runner.ts +298 -0
  41. package/src/core/types.ts +58 -0
  42. package/src/dev/console-bridge.ts +83 -0
  43. package/src/dev/debug-draw.ts +80 -0
  44. package/src/dev/logger.ts +119 -0
  45. package/src/ecs/component-manager.ts +748 -0
  46. package/src/ecs/game-component.ts +147 -0
  47. package/src/ecs/hmr-swap-report.ts +65 -0
  48. package/src/input/input-manager.ts +439 -0
  49. package/src/input/input-types.ts +19 -0
  50. package/src/input/schema.ts +129 -0
  51. package/src/loader.ts +70 -0
  52. package/src/manifest/index.ts +24 -0
  53. package/src/manifest/load-file.ts +16 -0
  54. package/src/manifest/load.ts +378 -0
  55. package/src/manifest/schema.ts +375 -0
  56. package/src/physics/collision-system.ts +76 -0
  57. package/src/physics/physics-registry.ts +83 -0
  58. package/src/physics/transform-writer.ts +41 -0
  59. package/src/physics/trigger-dispatch.ts +97 -0
  60. package/src/react/game-state.tsx +172 -0
  61. package/src/render/auto-batcher.ts +169 -0
  62. package/src/render/render-batch-system.ts +268 -0
  63. package/src/render/render-features.ts +146 -0
  64. package/src/render/render-settings.ts +72 -0
  65. package/src/runtime/create-runtime.ts +1152 -0
  66. package/src/runtime/frame-selector-cache.ts +81 -0
  67. package/src/runtime/game.ts +1003 -0
  68. package/src/runtime/input-router.ts +213 -0
  69. package/src/runtime/mount-game.ts +269 -0
  70. package/src/runtime/mount-manifest.ts +361 -0
  71. package/src/runtime/scene-ui-bridge.ts +86 -0
  72. package/src/runtime/scene-ui-data.ts +119 -0
  73. package/src/runtime/state-bridge.ts +79 -0
  74. package/src/runtime/types.ts +196 -0
  75. package/src/scene/asset-loaders.ts +195 -0
  76. package/src/scene/asset-paths.ts +123 -0
  77. package/src/scene/asset-registry.ts +67 -0
  78. package/src/scene/collider-dimensions.ts +125 -0
  79. package/src/scene/component-registry.ts +40 -0
  80. package/src/scene/defaults.ts +164 -0
  81. package/src/scene/geometries/index.ts +7 -0
  82. package/src/scene/geometries/terrain.ts +42 -0
  83. package/src/scene/geometry-registry.ts +42 -0
  84. package/src/scene/instance-registry.ts +84 -0
  85. package/src/scene/instancers/grid.ts +38 -0
  86. package/src/scene/instancers/index.ts +7 -0
  87. package/src/scene/light-camera-factory.ts +97 -0
  88. package/src/scene/material-factory.ts +211 -0
  89. package/src/scene/material-registry.ts +73 -0
  90. package/src/scene/materials/index.ts +7 -0
  91. package/src/scene/materials/water.ts +56 -0
  92. package/src/scene/parse.ts +71 -0
  93. package/src/scene/particles-factory.ts +383 -0
  94. package/src/scene/scene-apply.ts +356 -0
  95. package/src/scene/scene-diff-schema.ts +115 -0
  96. package/src/scene/scene-diff-types.ts +29 -0
  97. package/src/scene/scene-loader.ts +1533 -0
  98. package/src/scene/scene-query.ts +63 -0
  99. package/src/scene/scene-types.ts +34 -0
  100. package/src/scene/scene-version.ts +40 -0
  101. package/src/scene/schema/animation.ts +95 -0
  102. package/src/scene/schema/audio.ts +25 -0
  103. package/src/scene/schema/camera.ts +21 -0
  104. package/src/scene/schema/collider.ts +69 -0
  105. package/src/scene/schema/entity-ref.ts +78 -0
  106. package/src/scene/schema/entity.ts +169 -0
  107. package/src/scene/schema/environment.ts +384 -0
  108. package/src/scene/schema/index.ts +95 -0
  109. package/src/scene/schema/instances.ts +35 -0
  110. package/src/scene/schema/joint.ts +26 -0
  111. package/src/scene/schema/light.ts +38 -0
  112. package/src/scene/schema/material.ts +113 -0
  113. package/src/scene/schema/mesh.ts +108 -0
  114. package/src/scene/schema/particles.ts +398 -0
  115. package/src/scene/schema/physics.ts +49 -0
  116. package/src/scene/schema/scene-file.ts +299 -0
  117. package/src/scene/schema/shadow.ts +24 -0
  118. package/src/scene/schema/spline.ts +21 -0
  119. package/src/scene/schema/tuples.ts +21 -0
  120. package/src/scene/schema/ui.ts +602 -0
  121. package/src/scene/user-data.ts +203 -0
  122. package/src/setup/setup-audio.ts +60 -0
  123. package/src/setup/setup-particles.ts +23 -0
  124. package/src/setup/setup-physics.ts +67 -0
  125. package/src/setup/setup-renderer.ts +529 -0
  126. package/src/types-n8ao.d.ts +37 -0
  127. package/src/types-realism-effects.d.ts +61 -0
  128. package/src/world2d/authoring-2d.ts +208 -0
  129. package/src/world2d/capture-to-scene2d.ts +52 -0
  130. package/src/world2d/collision-2d.ts +106 -0
  131. package/src/world2d/components-2d.ts +86 -0
  132. package/src/world2d/index.ts +66 -0
  133. package/src/world2d/ingest-iframe-2d.ts +255 -0
  134. package/src/world2d/ingest2d.ts +131 -0
  135. package/src/world2d/physics2d-registry.ts +49 -0
  136. package/src/world2d/pixi-game-adapter.ts +325 -0
  137. package/src/world2d/pixi-surface.ts +78 -0
  138. package/src/world2d/scene-capture-2d.ts +117 -0
  139. package/src/world2d/scene2d-loader.ts +308 -0
  140. package/src/world2d/schema/entity2d.ts +145 -0
  141. package/src/world2d/schema/physics2d.ts +53 -0
  142. package/src/world2d/schema/sprite.ts +71 -0
  143. package/src/world2d/schema/tilemap.ts +22 -0
  144. package/src/world2d/schema/tuples2d.ts +25 -0
  145. package/src/world2d/system-adapters-2d.ts +49 -0
  146. package/src/world2d/transform-writer-2d.ts +24 -0
  147. package/src/world2d/types.ts +55 -0
@@ -0,0 +1,211 @@
1
+ import * as THREE from 'three';
2
+ import { loadTexture } from './asset-loaders';
3
+ import { DEFAULTS } from './defaults';
4
+ import { buildGeneratedGeometry } from './geometry-registry';
5
+ import { buildCustomMaterial } from './material-registry';
6
+ import type { SceneMaterial, SceneMesh } from './scene-types';
7
+
8
+ /**
9
+ * Single source of truth for turning a `SceneMesh` / `SceneMaterial` descriptor
10
+ * into Three.js geometry/material. Both the runtime scene-loader and the editor's
11
+ * entity-factory import these, so the editor preview renders exactly what the game
12
+ * renders (the WYSIWYG guarantee in ARCHITECTURE.md). Previously these were
13
+ * copy-pasted in two places and had to be hand-synced — any new material feature
14
+ * added to one and not the other silently broke that guarantee.
15
+ *
16
+ * Callers must have imported the side-effect registration modules
17
+ * (`./geometries`, `./materials`, plus any custom defs) so `buildGeneratedGeometry`
18
+ * and `buildCustomMaterial` resolve their registries.
19
+ */
20
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: straightforward type-based switch with fallback args
21
+ export function createGeometry(mesh: SceneMesh): THREE.BufferGeometry {
22
+ if (mesh.generator) {
23
+ return buildGeneratedGeometry(mesh.generator, mesh.generatorParams ?? {});
24
+ }
25
+ const args = mesh.args ?? [];
26
+ switch (mesh.type) {
27
+ case 'box':
28
+ return new THREE.BoxGeometry(args[0] ?? 1, args[1] ?? 1, args[2] ?? 1);
29
+ case 'sphere':
30
+ return new THREE.SphereGeometry(args[0] ?? 0.5, args[1] ?? 32, args[2] ?? 16);
31
+ case 'plane':
32
+ return new THREE.PlaneGeometry(args[0] ?? 1, args[1] ?? 1);
33
+ case 'cylinder':
34
+ return new THREE.CylinderGeometry(
35
+ args[0] ?? 0.5,
36
+ args[1] ?? 0.5,
37
+ args[2] ?? 1,
38
+ args[3] ?? 32,
39
+ );
40
+ case 'capsule':
41
+ return new THREE.CapsuleGeometry(args[0] ?? 0.5, args[1] ?? 1, args[2] ?? 4, args[3] ?? 16);
42
+ default:
43
+ return new THREE.BoxGeometry(1, 1, 1);
44
+ }
45
+ }
46
+
47
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: flat type-based switch building Three materials
48
+ export function createMaterial(mat?: SceneMaterial): THREE.Material {
49
+ if (!mat) return new THREE.MeshStandardMaterial({ color: DEFAULTS.material.color });
50
+
51
+ // Only include texture props when actually defined (Three.js warns on undefined values)
52
+ const textures: {
53
+ map?: THREE.Texture;
54
+ normalMap?: THREE.Texture;
55
+ emissiveMap?: THREE.Texture;
56
+ aoMap?: THREE.Texture;
57
+ lightMap?: THREE.Texture;
58
+ roughnessMap?: THREE.Texture;
59
+ metalnessMap?: THREE.Texture;
60
+ } = {};
61
+ if (mat.map) textures.map = loadTexture(mat.map);
62
+ if (mat.normalMap) textures.normalMap = loadTexture(mat.normalMap);
63
+ if (mat.emissiveMap) textures.emissiveMap = loadTexture(mat.emissiveMap);
64
+ if (mat.aoMap) textures.aoMap = loadTexture(mat.aoMap);
65
+ if (mat.lightMap) textures.lightMap = loadTexture(mat.lightMap);
66
+ if (mat.roughnessMap) textures.roughnessMap = loadTexture(mat.roughnessMap);
67
+ if (mat.metalnessMap) textures.metalnessMap = loadTexture(mat.metalnessMap);
68
+
69
+ // R1c — every fallback below reads DEFAULTS.material.* instead of
70
+ // duplicating its value as a bare literal, so DEFAULTS stays the single
71
+ // source of truth for what an omitted field renders as (see
72
+ // defaults-out-of-scope-consistency.test.ts / material-factory.test.ts's
73
+ // "omitted-field" locks, which fail if either copy drifts from the other).
74
+ const dMat = DEFAULTS.material;
75
+ const sideValue = mat.side ?? dMat.side;
76
+ const side =
77
+ sideValue === 'back'
78
+ ? THREE.BackSide
79
+ : sideValue === 'double'
80
+ ? THREE.DoubleSide
81
+ : THREE.FrontSide;
82
+ const opacity = mat.opacity ?? dMat.opacity;
83
+ const transparent = mat.transparent ?? dMat.transparent;
84
+
85
+ switch (mat.type) {
86
+ case 'basic':
87
+ return new THREE.MeshBasicMaterial({
88
+ color: mat.color ?? dMat.color,
89
+ ...textures,
90
+ lightMapIntensity: mat.lightMapIntensity ?? 1,
91
+ opacity,
92
+ transparent,
93
+ side,
94
+ });
95
+ case 'toon':
96
+ return new THREE.MeshToonMaterial({
97
+ color: mat.color ?? dMat.color,
98
+ ...textures,
99
+ lightMapIntensity: mat.lightMapIntensity ?? 1,
100
+ emissive: mat.emissive ?? dMat.emissive,
101
+ emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
102
+ opacity,
103
+ transparent,
104
+ side,
105
+ });
106
+ case 'physical':
107
+ return new THREE.MeshPhysicalMaterial({
108
+ color: mat.color ?? dMat.color,
109
+ metalness: mat.metalness ?? dMat.metalness,
110
+ roughness: mat.roughness ?? dMat.roughness,
111
+ ...textures,
112
+ lightMapIntensity: mat.lightMapIntensity ?? 1,
113
+ emissive: mat.emissive ?? dMat.emissive,
114
+ emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
115
+ opacity,
116
+ transparent,
117
+ side,
118
+ flatShading: mat.flatShading ?? dMat.flatShading,
119
+ // Clearcoat
120
+ ...(mat.clearcoat
121
+ ? {
122
+ clearcoat: mat.clearcoat.clearcoat,
123
+ clearcoatRoughness:
124
+ mat.clearcoat.clearcoatRoughness ?? dMat.clearcoat.clearcoatRoughness,
125
+ ...(mat.clearcoat.clearcoatMap
126
+ ? { clearcoatMap: loadTexture(mat.clearcoat.clearcoatMap) }
127
+ : {}),
128
+ ...(mat.clearcoat.clearcoatRoughnessMap
129
+ ? { clearcoatRoughnessMap: loadTexture(mat.clearcoat.clearcoatRoughnessMap) }
130
+ : {}),
131
+ }
132
+ : {}),
133
+ // Transmission — `thickness` used to hardcode a literal `0` here,
134
+ // silently diverging from DEFAULTS.material.transmission.thickness
135
+ // (0.5, per the editor's Transmission inspector default). Fixed:
136
+ // read DEFAULTS directly, like every other fallback in this file.
137
+ ...(mat.transmission
138
+ ? {
139
+ transmission: mat.transmission.transmission,
140
+ ior: mat.transmission.ior ?? dMat.transmission.ior,
141
+ thickness: mat.transmission.thickness ?? dMat.transmission.thickness,
142
+ ...(mat.transmission.attenuationColor
143
+ ? { attenuationColor: new THREE.Color(mat.transmission.attenuationColor) }
144
+ : {}),
145
+ attenuationDistance: mat.transmission.attenuationDistance ?? Infinity,
146
+ ...(mat.transmission.transmissionMap
147
+ ? { transmissionMap: loadTexture(mat.transmission.transmissionMap) }
148
+ : {}),
149
+ }
150
+ : {}),
151
+ // Sheen
152
+ ...(mat.sheen
153
+ ? {
154
+ sheen: mat.sheen.sheen,
155
+ sheenColor: new THREE.Color(mat.sheen.sheenColor ?? dMat.sheen.sheenColor),
156
+ sheenRoughness: mat.sheen.sheenRoughness ?? dMat.sheen.sheenRoughness,
157
+ ...(mat.sheen.sheenColorMap
158
+ ? { sheenColorMap: loadTexture(mat.sheen.sheenColorMap) }
159
+ : {}),
160
+ ...(mat.sheen.sheenRoughnessMap
161
+ ? { sheenRoughnessMap: loadTexture(mat.sheen.sheenRoughnessMap) }
162
+ : {}),
163
+ }
164
+ : {}),
165
+ // Iridescence
166
+ ...(mat.iridescence
167
+ ? {
168
+ iridescence: mat.iridescence.iridescence,
169
+ iridescenceIOR: mat.iridescence.iridescenceIOR ?? dMat.iridescence.iridescenceIOR,
170
+ iridescenceThicknessRange:
171
+ mat.iridescence.iridescenceThicknessRange ??
172
+ dMat.iridescence.iridescenceThicknessRange,
173
+ ...(mat.iridescence.iridescenceMap
174
+ ? { iridescenceMap: loadTexture(mat.iridescence.iridescenceMap) }
175
+ : {}),
176
+ ...(mat.iridescence.iridescenceThicknessMap
177
+ ? {
178
+ iridescenceThicknessMap: loadTexture(mat.iridescence.iridescenceThicknessMap),
179
+ }
180
+ : {}),
181
+ }
182
+ : {}),
183
+ // Displacement
184
+ ...(mat.displacementMap ? { displacementMap: loadTexture(mat.displacementMap) } : {}),
185
+ ...(mat.displacementScale !== undefined
186
+ ? { displacementScale: mat.displacementScale }
187
+ : {}),
188
+ ...(mat.displacementBias !== undefined ? { displacementBias: mat.displacementBias } : {}),
189
+ });
190
+ case 'custom': {
191
+ if (!mat.def) {
192
+ throw new Error('Custom material requires a "def" (registered material name)');
193
+ }
194
+ return buildCustomMaterial(mat.def, mat.uniforms ?? {});
195
+ }
196
+ default:
197
+ return new THREE.MeshStandardMaterial({
198
+ color: mat.color ?? dMat.color,
199
+ metalness: mat.metalness ?? dMat.metalness,
200
+ roughness: mat.roughness ?? dMat.roughness,
201
+ ...textures,
202
+ lightMapIntensity: mat.lightMapIntensity ?? 1,
203
+ emissive: mat.emissive ?? dMat.emissive,
204
+ emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
205
+ opacity,
206
+ transparent,
207
+ side,
208
+ flatShading: mat.flatShading ?? dMat.flatShading,
209
+ });
210
+ }
211
+ }
@@ -0,0 +1,73 @@
1
+ import type * as THREE from 'three';
2
+
3
+ /**
4
+ * A custom-material definition. Registered by name and resolved when a scene
5
+ * material declares `{ type: 'custom', def: <name> }`.
6
+ *
7
+ * The GLSL program lives here in code (the "behavior" half); the per-entity
8
+ * authored uniform values arrive as data on the scene material's `uniforms`
9
+ * (the "data" half) and are passed to `build`. A def may optionally tick its
10
+ * own uniforms each frame (e.g. uTime) via `update`.
11
+ *
12
+ * This is the native extension point of the material system — not a wrapper:
13
+ * `build` returns a real THREE.Material (typically a ShaderMaterial, or a stock
14
+ * material patched via onBeforeCompile), and the engine renders it directly.
15
+ */
16
+ export interface MaterialDef {
17
+ /** Build a real THREE material from authored uniform values. */
18
+ build(uniforms: Record<string, unknown>): THREE.Material;
19
+ /** Optional per-frame uniform update for self-animating materials. */
20
+ update?(dt: number, material: THREE.Material, uniforms: Record<string, unknown>): void;
21
+ /** Representative flat color (CSS hex) for the editor's "unlit" view mode. */
22
+ previewColor?: string;
23
+ }
24
+
25
+ const registry = new Map<string, MaterialDef>();
26
+
27
+ /** Register a custom-material definition under `name`. */
28
+ export function registerMaterial(name: string, def: MaterialDef): void {
29
+ registry.set(name, def);
30
+ }
31
+
32
+ /** Look up a registered material definition. */
33
+ export function getMaterialDef(name: string): MaterialDef | undefined {
34
+ return registry.get(name);
35
+ }
36
+
37
+ /**
38
+ * Build a custom material by registered def name. Tags the resulting material's
39
+ * `userData` so the per-frame tick and the editor view-mode override can find it.
40
+ * Throws if the def is unknown (fail loud — same posture as the component registry).
41
+ */
42
+ export function buildCustomMaterial(
43
+ name: string,
44
+ uniforms: Record<string, unknown>,
45
+ ): THREE.Material {
46
+ const def = registry.get(name);
47
+ if (!def) {
48
+ throw new Error(`Custom material def "${name}" not found in material registry`);
49
+ }
50
+ const material = def.build(uniforms ?? {});
51
+ material.userData['__matDef'] = name;
52
+ material.userData['__uniforms'] = uniforms ?? {};
53
+ return material;
54
+ }
55
+
56
+ /**
57
+ * Tick every custom material under `root` (self-animating uniforms). Called from
58
+ * the runtime `preRender` phase and from the editor's edit-mode RAF — one code
59
+ * path, both modes. Walks the scene graph; materials without a `__matDef` tag are
60
+ * skipped.
61
+ */
62
+ export function tickCustomMaterials(root: THREE.Object3D, dt: number): void {
63
+ root.traverse((obj) => {
64
+ const material = (obj as THREE.Mesh).material;
65
+ if (!material) return;
66
+ const list = Array.isArray(material) ? material : [material];
67
+ for (const mat of list) {
68
+ const defName = mat.userData?.['__matDef'] as string | undefined;
69
+ if (!defName) continue;
70
+ registry.get(defName)?.update?.(dt, mat, mat.userData['__uniforms'] ?? {});
71
+ }
72
+ });
73
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Built-in custom-material definitions. Importing this module for its side
3
+ * effects registers the built-ins into the material registry. Both the runtime
4
+ * factory (scene-loader) and the editor factory (entity-factory) import it so
5
+ * custom materials resolve on whichever side builds first.
6
+ */
7
+ import './water';
@@ -0,0 +1,56 @@
1
+ import * as THREE from 'three';
2
+ import { registerMaterial } from '../material-registry';
3
+
4
+ /**
5
+ * Built-in "Water" custom material — the proof-of-concept for first-class custom
6
+ * shaders (roadmap item #1). Vertex shader displaces a plane with travelling sine
7
+ * waves driven by `uTime`; fragment tints by `uColor` with a little wave-crest
8
+ * lightening. Demonstrates: authored uniforms (color, waveHeight), a real
9
+ * ShaderMaterial built from scene JSON, and a self-animating uniform (uTime).
10
+ */
11
+ const vertexShader = /* glsl */ `
12
+ uniform float uTime;
13
+ uniform float uWaveHeight;
14
+ varying float vWave;
15
+ void main() {
16
+ vec3 p = position;
17
+ // PlaneGeometry lies in local XY with normal +Z; displace along the normal so
18
+ // a plane rotated flat (XZ) ripples upward in world Y.
19
+ float wave =
20
+ sin(p.x * 1.5 + uTime) * 0.6 +
21
+ cos(p.y * 1.2 + uTime * 0.8) * 0.4;
22
+ p.z += wave * uWaveHeight;
23
+ vWave = wave;
24
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
25
+ }
26
+ `;
27
+
28
+ const fragmentShader = /* glsl */ `
29
+ uniform vec3 uColor;
30
+ varying float vWave;
31
+ void main() {
32
+ vec3 c = uColor + vWave * 0.12;
33
+ gl_FragColor = vec4(c, 1.0);
34
+ }
35
+ `;
36
+
37
+ registerMaterial('Water', {
38
+ previewColor: '#0a5fa5',
39
+ build(uniforms) {
40
+ const color = (uniforms['color'] as string) ?? '#0a5fa5';
41
+ const waveHeight = (uniforms['waveHeight'] as number) ?? 0.4;
42
+ return new THREE.ShaderMaterial({
43
+ uniforms: {
44
+ uTime: { value: 0 },
45
+ uColor: { value: new THREE.Color(color) },
46
+ uWaveHeight: { value: waveHeight },
47
+ },
48
+ vertexShader,
49
+ fragmentShader,
50
+ });
51
+ },
52
+ update(dt, material) {
53
+ const uTime = (material as THREE.ShaderMaterial).uniforms['uTime'];
54
+ if (uTime) uTime.value += dt;
55
+ },
56
+ });
@@ -0,0 +1,71 @@
1
+ import type { ZodIssue } from 'zod';
2
+ import { checkSceneVersionInRange } from './scene-version';
3
+ import type { InstancesFile, MaterialFile, PrefabFile, SceneFile } from './schema';
4
+ import {
5
+ InstancesFileSchema,
6
+ MaterialFileSchema,
7
+ PrefabFileSchema,
8
+ SceneFileSchema,
9
+ } from './schema';
10
+
11
+ export class SceneParseError extends Error {
12
+ readonly issues: ZodIssue[];
13
+ /** The offending file's path/URL, when the caller knows it (T4.6). */
14
+ readonly filePath?: string | undefined;
15
+
16
+ constructor(issues: ZodIssue[], filePath?: string) {
17
+ const msg = issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
18
+ const header = filePath ? `Scene validation failed (${filePath}):` : 'Scene validation failed:';
19
+ super(`${header}\n${msg}`);
20
+ this.name = 'SceneParseError';
21
+ this.issues = issues;
22
+ this.filePath = filePath;
23
+ }
24
+ }
25
+
26
+ export function parseSceneFile(json: unknown, filePath?: string): SceneFile {
27
+ const result = SceneFileSchema.safeParse(json);
28
+ if (!result.success) {
29
+ throw new SceneParseError(result.error.issues, filePath);
30
+ }
31
+ // Post-Zod-parse range check (T2.3/D3 §1.E) — the Zod shape stays a bare
32
+ // `z.number()`; this is where the file-format `version` axis is actually
33
+ // read/enforced.
34
+ checkSceneVersionInRange(result.data.version, 'scene');
35
+ return result.data;
36
+ }
37
+
38
+ export function parsePrefabFile(json: unknown, filePath?: string): PrefabFile {
39
+ const result = PrefabFileSchema.safeParse(json);
40
+ if (!result.success) {
41
+ throw new SceneParseError(result.error.issues, filePath);
42
+ }
43
+ checkSceneVersionInRange(result.data.version, 'prefab');
44
+ return result.data;
45
+ }
46
+
47
+ /**
48
+ * Parse a `.mat.json` shared material asset (T4.6). Unlike scene/prefab files,
49
+ * there is no version-range gate yet for this format — `MaterialFileSchema`'s
50
+ * `version` field is RESERVED (see `schema-consumption-map.ts`).
51
+ */
52
+ export function parseMaterialFile(json: unknown, filePath?: string): MaterialFile {
53
+ const result = MaterialFileSchema.safeParse(json);
54
+ if (!result.success) {
55
+ throw new SceneParseError(result.error.issues, filePath);
56
+ }
57
+ return result.data;
58
+ }
59
+
60
+ /**
61
+ * Parse a `.instances.json` instance-transform asset (F3,
62
+ * docs/VSCN-STRUCTURAL-GAPS-DESIGN.md). Mirrors `parseMaterialFile` exactly —
63
+ * no version-range gate (the format is a bare tuple array, no `version` field).
64
+ */
65
+ export function parseInstancesFile(json: unknown, filePath?: string): InstancesFile {
66
+ const result = InstancesFileSchema.safeParse(json);
67
+ if (!result.success) {
68
+ throw new SceneParseError(result.error.issues, filePath);
69
+ }
70
+ return result.data;
71
+ }