@vgai/engine 0.2.0 → 0.4.0-canary.20260715.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 (123) hide show
  1. package/README.md +3 -1
  2. package/package.json +24 -4
  3. package/schemas/engine-api.json +124 -0
  4. package/schemas/engine-api.md +53 -0
  5. package/schemas/engine-capabilities.json +124 -0
  6. package/schemas/inputmap.schema.json +314 -0
  7. package/schemas/mat.schema.json +286 -0
  8. package/schemas/prefab.schema.json +10148 -0
  9. package/schemas/scn2d.schema.json +475 -0
  10. package/schemas/vgai-game.schema.json +383 -0
  11. package/schemas/vscn.schema.json +11007 -0
  12. package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
  13. package/src/adapter/authoring.ts +77 -0
  14. package/src/adapter/first-party-systems.ts +23 -34
  15. package/src/adapter/game-adapter.ts +8 -8
  16. package/src/adapter/host-context.ts +2 -4
  17. package/src/adapter/index.ts +4 -4
  18. package/src/adapter/system-adapter.ts +88 -22
  19. package/src/adapter/vgai-scene-game-adapter.ts +244 -194
  20. package/src/animation/anim-graph-types.ts +12 -43
  21. package/src/animation/animation-clock.ts +479 -0
  22. package/src/animation/camera-ownership.ts +467 -0
  23. package/src/animation/cinematic-cues.ts +451 -0
  24. package/src/animation/clip-map.ts +41 -0
  25. package/src/animation/gsap-registration.ts +184 -0
  26. package/src/animation/theatre-clock-binding.ts +111 -0
  27. package/src/animation/theatre-director.ts +347 -0
  28. package/src/animation/theatre-object-binding.ts +661 -0
  29. package/src/animation/xstate-animation-binding.ts +436 -0
  30. package/src/animation/xstate-animation-meta.ts +319 -0
  31. package/src/audio/index.ts +39 -7
  32. package/src/audio/tone-clock-binding.ts +98 -0
  33. package/src/audio/tone-context.ts +129 -0
  34. package/src/audio/tone-offline-render.ts +167 -0
  35. package/src/audio/wav-encode.ts +119 -0
  36. package/src/character/cloth-sim.ts +533 -0
  37. package/src/character/spring-chain.ts +307 -0
  38. package/src/core/game-loop.ts +57 -2
  39. package/src/core/seeded-random.ts +161 -0
  40. package/src/core/system-runner.ts +20 -3
  41. package/src/core/types.ts +50 -0
  42. package/src/data/data-asset.ts +167 -0
  43. package/src/data/data-check-core.ts +242 -0
  44. package/src/data/data-ref.ts +145 -0
  45. package/src/data/vite-plugin-data.ts +290 -0
  46. package/src/dev/performance-profiler.ts +213 -0
  47. package/src/dev/webgl-gpu-timer.ts +53 -0
  48. package/src/ecs/component-manager.ts +45 -12
  49. package/src/ecs/game-component.ts +95 -11
  50. package/src/humanoid/bake.operation.ts +326 -0
  51. package/src/humanoid/body.ts +663 -0
  52. package/src/humanoid/clips.ts +149 -0
  53. package/src/humanoid/compose.ts +209 -0
  54. package/src/humanoid/generate.ts +189 -0
  55. package/src/humanoid/index.ts +36 -0
  56. package/src/humanoid/schema.ts +108 -0
  57. package/src/humanoid/skeleton.ts +345 -0
  58. package/src/index.ts +48 -0
  59. package/src/input/input-manager.ts +1886 -33
  60. package/src/input/input-types.ts +158 -3
  61. package/src/input/prompt-labels.ts +122 -0
  62. package/src/input/rebind-controller.ts +105 -0
  63. package/src/input/schema.ts +206 -52
  64. package/src/manifest/index.ts +5 -5
  65. package/src/manifest/load.ts +125 -72
  66. package/src/manifest/schema.ts +362 -255
  67. package/src/react/game-state.tsx +135 -32
  68. package/src/react/root-adapter.tsx +49 -0
  69. package/src/react/unmanaged-root-detector.ts +66 -0
  70. package/src/react/use-data.ts +124 -0
  71. package/src/react/use-selection.tsx +135 -0
  72. package/src/runtime/create-runtime.ts +112 -273
  73. package/src/runtime/debug-bridge.ts +483 -0
  74. package/src/runtime/debug-registry.ts +856 -0
  75. package/src/runtime/game.ts +342 -93
  76. package/src/runtime/gameplay-rng-trap.ts +134 -0
  77. package/src/runtime/input-router.ts +7 -7
  78. package/src/runtime/mount-game.ts +40 -38
  79. package/src/runtime/mount-manifest.ts +169 -37
  80. package/src/runtime/render-audio-control.ts +168 -0
  81. package/src/runtime/render-control.ts +522 -0
  82. package/src/runtime/render-seed.ts +79 -0
  83. package/src/runtime/state-bridge.ts +24 -10
  84. package/src/runtime/types.ts +110 -33
  85. package/src/scene/asset-loaders.ts +10 -36
  86. package/src/scene/asset-paths.ts +0 -2
  87. package/src/scene/asset-ref-check.ts +248 -0
  88. package/src/scene/asset-registry.ts +22 -0
  89. package/src/scene/component-registry.ts +14 -3
  90. package/src/scene/defaults.ts +1 -0
  91. package/src/scene/light-camera-factory.ts +11 -3
  92. package/src/scene/parse.ts +133 -0
  93. package/src/scene/scene-apply.ts +55 -4
  94. package/src/scene/scene-loader.ts +91 -123
  95. package/src/scene/scene-types.ts +0 -1
  96. package/src/scene/schema/animation.ts +30 -79
  97. package/src/scene/schema/entity.ts +20 -0
  98. package/src/scene/schema/index.ts +2 -46
  99. package/src/scene/schema/light.ts +16 -1
  100. package/src/scene/schema/material.ts +96 -91
  101. package/src/scene/schema/scene-file.ts +1 -7
  102. package/src/scene/user-data.ts +22 -10
  103. package/src/setup/setup-renderer.ts +10 -3
  104. package/src/tools/define-tool.ts +191 -0
  105. package/src/world2d/authoring-2d.ts +17 -1
  106. package/src/world2d/collision-2d.ts +1 -1
  107. package/src/world2d/pixi-game-adapter.ts +19 -17
  108. package/src/world2d/scene2d-loader.ts +1 -0
  109. package/src/world2d/types.ts +8 -2
  110. package/src/animation/anim-graph.ts +0 -406
  111. package/src/animation/anim-system.ts +0 -28
  112. package/src/animation/property-track.ts +0 -178
  113. package/src/animation/schema.ts +0 -204
  114. package/src/audio/ambient.ts +0 -300
  115. package/src/audio/impacts.ts +0 -212
  116. package/src/audio/movement.ts +0 -140
  117. package/src/audio/musical.ts +0 -200
  118. package/src/audio/ui-sounds.ts +0 -171
  119. package/src/audio/vehicle.ts +0 -235
  120. package/src/audio/weapons.ts +0 -152
  121. package/src/runtime/scene-ui-bridge.ts +0 -86
  122. package/src/runtime/scene-ui-data.ts +0 -119
  123. package/src/scene/schema/ui.ts +0 -602
@@ -1,94 +1,45 @@
1
1
  import { z } from 'zod';
2
- // Reuse the UI track easing vocabulary (S1) so there is ONE easing enum across
3
- // the codebase — see F5 (docs/VSCN-STRUCTURAL-GAPS-DESIGN.md). Do NOT reuse
4
- // UIKeyframeSchema itself: its `value` is `z.union([z.number(), z.string()])`,
5
- // and property-track values MUST be numbers only (no expression/string-logic
6
- // values — in-scene scripting is banned, G1).
7
- import { UIEasingSchema } from './ui';
8
-
9
- /** Validated at runtime against AnimGraph parameter definitions. */
10
- const AnimParameterOverridesSchema = z.record(z.string(), z.union([z.number(), z.boolean()]));
11
2
 
12
3
  // ---------------------------------------------------------------------------
13
- // F5Declarative property tracks (deterministic timeline).
4
+ // D8declarative property tracks (formerly F5,
5
+ // docs/VSCN-STRUCTURAL-GAPS-DESIGN.md) were REMOVED: Theatre
6
+ // (`@theatre/core`) is the sole authored continuous-animation source (spec
7
+ // §3.2, §10 D2-D4/D8). `PropertyTrackSchema`/`PropertyTrackRunner` and the
8
+ // `tracks` field are deleted, not deprecated-in-place — see
9
+ // packages/engine/src/scene/parse.ts (`assertNoRemovedPropertyTracks`) for
10
+ // the loud migration-error guard that rejects any `.vscn`/`.prefab.json`
11
+ // still authoring `animation.tracks`, since Zod would otherwise silently
12
+ // strip an unrecognized field rather than failing loudly (Global AC §6).
13
+ //
14
+ // E5 — the AnimGraph state-machine runtime, `.animgraph.json` format, and
15
+ // every consumer were removed (spec §3.3, §11 E5; XState + native Three
16
+ // `AnimationMixer` replace it — see `xstate-animation-binding.ts`). The
17
+ // `animGraph` field is deleted outright, not deprecated-in-place, for the
18
+ // same reason as `tracks` above: Zod's default object mode would otherwise
19
+ // silently STRIP an authored `animation.animGraph` rather than reporting it,
20
+ // violating Global AC §6 ("Removed formats fail with a concise migration/
21
+ // removal error rather than being partially read"). The raw-JSON guard
22
+ // (`assertNoRemovedAnimGraph` in packages/engine/src/scene/parse.ts) rejects
23
+ // any `.vscn`/`.prefab.json` still authoring it before Zod ever parses the
24
+ // data — see packages/engine/test/anim-graph-removed.test.ts.
14
25
  //
15
- // A track is DATA: a pure function of time (keyframes + named easing), never
16
- // behavior. `target` is a FIXED allowlist, not an arbitrary property path —
17
- // this is the guardrail that keeps the format from becoming an in-scene
18
- // scripting surface (G1, docs/VSCN-STRUCTURAL-GAPS-DESIGN.md). Anything
19
- // state-dependent ("move WHEN the player steps on it") stays a GameComponent,
20
- // which may start/stop/seek a track it never grows conditionals here.
26
+ // `parameters` is deleted alongside `animGraph`, though it isn't itself named
27
+ // in the spec/removal-inventory: its ONLY documented purpose was seeding an
28
+ // `AnimGraph`'s initial parameter values (its only readers were
29
+ // `scene-loader.ts`'s former animGraph branch, the editor's entity-factory.ts,
30
+ // and AnimationSection.tsx's parameter-row UI all deleted with the runtime).
31
+ // Leaving it declared with zero remaining readers would violate this repo's
32
+ // "no schema field without a runtime reader" policy (CLAUDE.md).
21
33
  // ---------------------------------------------------------------------------
22
34
 
23
- export const PropertyTrackTargetSchema = z
24
- .enum([
25
- 'position.x',
26
- 'position.y',
27
- 'position.z',
28
- 'rotation.x',
29
- 'rotation.y',
30
- 'rotation.z',
31
- 'scale.x',
32
- 'scale.y',
33
- 'scale.z',
34
- 'material.opacity',
35
- 'light.intensity',
36
- ])
37
- .describe(
38
- 'The Object3D property this track drives. A FIXED allowlist, not an arbitrary property ' +
39
- 'path — an open path would let scene data address/mutate arbitrary runtime state, which ' +
40
- 'is in-scene scripting (banned, G1). Add new targets here deliberately, one at a time.',
41
- );
42
- export type PropertyTrackTarget = z.infer<typeof PropertyTrackTargetSchema>;
43
-
44
- export const PropertyTrackKeyframeSchema = z
45
- .object({
46
- time: z.number().describe('Seconds from track start'),
47
- value: z
48
- .number()
49
- .describe(
50
- 'Numeric value at this time — numbers only; no expressions/strings (in-scene scripting is banned)',
51
- ),
52
- easing: UIEasingSchema.optional().describe(
53
- 'Easing of the segment after this keyframe (reuses the UI track easing vocabulary)',
54
- ),
55
- })
56
- .describe('A single keyframe (time + numeric value + optional easing) on a property track');
57
- export type PropertyTrackKeyframe = z.infer<typeof PropertyTrackKeyframeSchema>;
58
-
59
- export const PropertyTrackSchema = z
60
- .object({
61
- target: PropertyTrackTargetSchema,
62
- keyframes: z.array(PropertyTrackKeyframeSchema).describe('Ordered by time'),
63
- loop: z.boolean().optional().describe('Whether the track repeats after reaching `duration`'),
64
- duration: z.number().describe('Track length in seconds'),
65
- })
66
- .describe('A deterministic keyframed timeline driving one allowlisted property');
67
- export type PropertyTrack = z.infer<typeof PropertyTrackSchema>;
68
-
69
35
  export const SceneAnimationSchema = z
70
36
  .object({
71
- animGraph: z.string().optional().describe('Path to .animgraph.json state machine file'),
72
- parameters: AnimParameterOverridesSchema.optional().describe(
73
- 'Initial parameter values for the animation graph',
74
- ),
75
- autoplay: z
76
- .string()
77
- .optional()
78
- .describe('Clip name to play automatically on load (when no animGraph is set)'),
37
+ autoplay: z.string().optional().describe('Clip name to play automatically on load'),
79
38
  loop: z.boolean().optional().describe('Whether the autoplay clip should loop'),
80
39
  clipAliases: z
81
40
  .record(z.string(), z.string())
82
41
  .optional()
83
- .describe('Map raw GLTF clip names to clean names used by the animation graph'),
84
- tracks: z
85
- .array(PropertyTrackSchema)
86
- .optional()
87
- .describe(
88
- 'Deterministic keyframed property timelines (data, not behavior — pure function of ' +
89
- 'time; state-dependent motion stays a GameComponent). Runs in the animation phase, ' +
90
- 'honors pause/timeScale.',
91
- ),
42
+ .describe('Map raw GLTF clip names to clean names'),
92
43
  })
93
44
  .describe('Animation playback configuration');
94
45
 
@@ -13,6 +13,15 @@ import { SceneShadowOverrideSchema, SceneShadowSchema } from './shadow';
13
13
  import { SceneSplineOverrideSchema, SceneSplineSchema } from './spline';
14
14
  import { TransformSchema, Vec3Schema } from './tuples';
15
15
 
16
+ const BoneAttachmentSchema = z
17
+ .object({
18
+ target: z.string().describe('Entity ID whose animated skeleton owns the target bone'),
19
+ bone: z.string().describe('Exact name of the target bone or socket Object3D'),
20
+ })
21
+ .describe(
22
+ 'Attach this entity to a named bone/socket after the complete scene has loaded. The entity transform becomes its local attachment offset',
23
+ );
24
+
16
25
  /**
17
26
  * User-defined ECS component data. Validated at runtime against ComponentRegistry.
18
27
  *
@@ -65,6 +74,9 @@ const SharedEntityFields = {
65
74
  .optional()
66
75
  .describe('Path to shared .mat.json material asset. Mutually exclusive with inline material'),
67
76
  transform: TransformSchema.optional().describe('Local transform relative to parent'),
77
+ boneAttachment: BoneAttachmentSchema.optional().describe(
78
+ 'Animated bone/socket attachment. Use transform for the local position, rotation, and scale offset',
79
+ ),
68
80
  pivot: Vec3Schema.optional().describe(
69
81
  'Local-space pivot point for rotation/scale. [0,0,0] = geometry center',
70
82
  ),
@@ -165,5 +177,13 @@ export const SceneEntitySchema: z.ZodType<SceneEntity> = z.lazy(() =>
165
177
  path: ['material'],
166
178
  });
167
179
  }
180
+ if (entity.boneAttachment !== undefined && entity.physics !== undefined) {
181
+ ctx.addIssue({
182
+ code: z.ZodIssueCode.custom,
183
+ message:
184
+ 'A bone-attached entity cannot declare physics. Attachments follow an animated transform; add gameplay collision to a separate entity.',
185
+ path: ['boneAttachment'],
186
+ });
187
+ }
168
188
  }),
169
189
  ) as z.ZodType<SceneEntity>;
@@ -1,17 +1,7 @@
1
1
  // Zod schemas
2
2
 
3
- export type {
4
- PropertyTrack,
5
- PropertyTrackKeyframe,
6
- PropertyTrackTarget,
7
- SceneAnimation,
8
- } from './animation';
9
- export {
10
- PropertyTrackKeyframeSchema,
11
- PropertyTrackSchema,
12
- PropertyTrackTargetSchema,
13
- SceneAnimationSchema,
14
- } from './animation';
3
+ export type { SceneAnimation } from './animation';
4
+ export { SceneAnimationSchema } from './animation';
15
5
  export type { SceneAudio } from './audio';
16
6
  export { SceneAudioSchema } from './audio';
17
7
  export type { SceneCamera } from './camera';
@@ -59,37 +49,3 @@ export { SceneSplineOverrideSchema, SceneSplineSchema } from './spline';
59
49
  // Inferred types
60
50
  export type { Quat, SceneTransform, Vec3 } from './tuples';
61
51
  export { QuatSchema, TransformSchema, Vec3Schema } from './tuples';
62
- // UI scene-graph schema (editable React UI in the scene graph)
63
- export type {
64
- UIAnimatableProperty,
65
- UIAnimation,
66
- UIBinding,
67
- UIEasing,
68
- UIKeyframe,
69
- UILayout,
70
- UINode,
71
- UINodeKind,
72
- UIPrefabFile,
73
- UIRoot,
74
- UIStyle,
75
- UITheme,
76
- UIThemeFile,
77
- UITrack,
78
- } from './ui';
79
- export {
80
- UIAnimatablePropertySchema,
81
- UIAnimationSchema,
82
- UIBindingSchema,
83
- UIEasingSchema,
84
- UIKeyframeSchema,
85
- UILayoutSchema,
86
- UINodeKindSchema,
87
- UINodeSchema,
88
- UIPrefabFileSchema,
89
- UIRootSchema,
90
- UIStateStylesSchema,
91
- UIStyleSchema,
92
- UIThemeFileSchema,
93
- UIThemeSchema,
94
- UITrackSchema,
95
- } from './ui';
@@ -3,12 +3,27 @@ import { z } from 'zod';
3
3
  const SceneLightBase = z.object({
4
4
  type: z.enum(['directional', 'point', 'spot', 'hemisphere', 'area']).describe('Light type'),
5
5
  color: z.string().optional().describe('Light color as CSS hex string'),
6
- intensity: z.number().optional().describe('Light intensity multiplier'),
6
+ intensity: z
7
+ .number()
8
+ .optional()
9
+ .describe(
10
+ 'Native Three.js light intensity. Point and spot lights use candela with inverse-square ' +
11
+ 'falloff (a value near 1 is candle-scale); area lights use nit; directional and ' +
12
+ 'hemisphere lights use Three.js scene-light intensity units.',
13
+ ),
7
14
  groundColor: z.string().optional().describe('Ground color for hemisphere lights'),
8
15
  distance: z
9
16
  .number()
10
17
  .optional()
11
18
  .describe('Maximum range of the light. 0 = infinite (point/spot only)'),
19
+ decay: z
20
+ .number()
21
+ .nonnegative()
22
+ .optional()
23
+ .describe(
24
+ 'Native Three.js distance-decay exponent for point/spot lights. Default 2 gives ' +
25
+ 'physically correct inverse-square falloff.',
26
+ ),
12
27
  angle: z.number().optional().describe('Spotlight cone angle in radians (spot only)'),
13
28
  penumbra: z.number().optional().describe('Spotlight penumbra softness 0–1 (spot only)'),
14
29
  width: z
@@ -1,103 +1,108 @@
1
1
  import { z } from 'zod';
2
2
 
3
- export const SceneMaterialSchema = z
4
- .object({
5
- type: z
6
- .enum(['standard', 'physical', 'basic', 'toon', 'custom'])
7
- .describe('Material shading model. "custom" resolves `def` against the material registry'),
8
- def: z
9
- .string()
10
- .optional()
11
- .describe('Registered custom-material name (required when type is "custom")'),
12
- uniforms: z
13
- .record(z.string(), z.union([z.number(), z.string(), z.boolean(), z.array(z.number())]))
14
- .optional()
15
- .describe('Authored uniform values for a custom material, keyed by uniform name'),
16
- color: z.string().optional().describe('Base color as CSS hex string (e.g. "#ff0000")'),
17
- metalness: z.number().optional().describe('Metalness factor 0–1 (standard/physical only)'),
18
- roughness: z.number().optional().describe('Roughness factor 0–1 (standard/physical only)'),
19
- map: z.string().optional().describe('Path to albedo/diffuse texture image'),
20
- normalMap: z.string().optional().describe('Path to normal map texture image'),
21
- emissive: z.string().optional().describe('Emissive color as CSS hex string'),
22
- emissiveIntensity: z.number().optional().describe('Emissive light intensity multiplier'),
23
- emissiveMap: z.string().optional().describe('Path to emissive map texture image'),
24
- aoMap: z.string().optional().describe('Path to ambient occlusion map texture image'),
25
- lightMap: z
26
- .string()
27
- .optional()
28
- .describe(
29
- "Path to a baked lightmap texture (uses the mesh's second UV set). Applies the pre-baked GI/shadow result on top of the material.",
30
- ),
31
- lightMapIntensity: z.number().optional().describe('Lightmap intensity multiplier (default 1).'),
32
- roughnessMap: z.string().optional().describe('Path to roughness map texture image'),
33
- metalnessMap: z.string().optional().describe('Path to metalness map texture image'),
34
- opacity: z.number().optional().describe('Opacity 0–1. Set transparent: true to enable'),
35
- transparent: z.boolean().optional().describe('Enable alpha blending for this material'),
36
- side: z.enum(['front', 'back', 'double']).optional().describe('Which face sides to render'),
37
- flatShading: z.boolean().optional().describe('Use flat (faceted) shading instead of smooth'),
3
+ const SceneMaterialObjectSchema = z.object({
4
+ type: z
5
+ .enum(['standard', 'physical', 'basic', 'toon', 'custom'])
6
+ .describe('Material shading model. "custom" resolves `def` against the material registry'),
7
+ def: z
8
+ .string()
9
+ .optional()
10
+ .describe('Registered custom-material name (required when type is "custom")'),
11
+ uniforms: z
12
+ .record(z.string(), z.union([z.number(), z.string(), z.boolean(), z.array(z.number())]))
13
+ .optional()
14
+ .describe('Authored uniform values for a custom material, keyed by uniform name'),
15
+ color: z.string().optional().describe('Base color as CSS hex string (e.g. "#ff0000")'),
16
+ metalness: z.number().optional().describe('Metalness factor 0–1 (standard/physical only)'),
17
+ roughness: z.number().optional().describe('Roughness factor 0–1 (standard/physical only)'),
18
+ map: z.string().optional().describe('Path to albedo/diffuse texture image'),
19
+ normalMap: z.string().optional().describe('Path to normal map texture image'),
20
+ emissive: z.string().optional().describe('Emissive color as CSS hex string'),
21
+ emissiveIntensity: z.number().optional().describe('Emissive light intensity multiplier'),
22
+ emissiveMap: z.string().optional().describe('Path to emissive map texture image'),
23
+ aoMap: z.string().optional().describe('Path to ambient occlusion map texture image'),
24
+ lightMap: z
25
+ .string()
26
+ .optional()
27
+ .describe(
28
+ "Path to a baked lightmap texture (uses the mesh's second UV set). Applies the pre-baked GI/shadow result on top of the material.",
29
+ ),
30
+ lightMapIntensity: z.number().optional().describe('Lightmap intensity multiplier (default 1).'),
31
+ roughnessMap: z.string().optional().describe('Path to roughness map texture image'),
32
+ metalnessMap: z.string().optional().describe('Path to metalness map texture image'),
33
+ opacity: z.number().optional().describe('Opacity 0–1. Set transparent: true to enable'),
34
+ transparent: z.boolean().optional().describe('Enable alpha blending for this material'),
35
+ side: z.enum(['front', 'back', 'double']).optional().describe('Which face sides to render'),
36
+ flatShading: z.boolean().optional().describe('Use flat (faceted) shading instead of smooth'),
38
37
 
39
- // Physical material feature groups (physical only, all optional)
40
- clearcoat: z
41
- .object({
42
- clearcoat: z.number().describe('Clearcoat layer intensity 0–1'),
43
- clearcoatRoughness: z.number().optional().describe('Clearcoat roughness 0–1'),
44
- clearcoatMap: z.string().optional().describe('Path to clearcoat intensity map'),
45
- clearcoatRoughnessMap: z.string().optional().describe('Path to clearcoat roughness map'),
46
- })
47
- .optional()
48
- .describe('Clearcoat layer (physical only)'),
38
+ // Physical material feature groups (physical only, all optional)
39
+ clearcoat: z
40
+ .object({
41
+ clearcoat: z.number().describe('Clearcoat layer intensity 0–1'),
42
+ clearcoatRoughness: z.number().optional().describe('Clearcoat roughness 0–1'),
43
+ clearcoatMap: z.string().optional().describe('Path to clearcoat intensity map'),
44
+ clearcoatRoughnessMap: z.string().optional().describe('Path to clearcoat roughness map'),
45
+ })
46
+ .optional()
47
+ .describe('Clearcoat layer (physical only)'),
49
48
 
50
- transmission: z
51
- .object({
52
- transmission: z.number().describe('Transmission intensity 0–1 (glass, liquid)'),
53
- ior: z.number().optional().describe('Index of refraction (default 1.5)'),
54
- thickness: z.number().optional().describe('Volume thickness for refraction'),
55
- attenuationColor: z.string().optional().describe('Color absorbed over distance'),
56
- attenuationDistance: z
57
- .number()
58
- .optional()
59
- .describe('Distance at which attenuation color takes full effect'),
60
- transmissionMap: z.string().optional().describe('Path to transmission map'),
61
- })
62
- .optional()
63
- .describe('Light transmission (physical only)'),
49
+ transmission: z
50
+ .object({
51
+ transmission: z.number().describe('Transmission intensity 0–1 (glass, liquid)'),
52
+ ior: z.number().optional().describe('Index of refraction (default 1.5)'),
53
+ thickness: z.number().optional().describe('Volume thickness for refraction'),
54
+ attenuationColor: z.string().optional().describe('Color absorbed over distance'),
55
+ attenuationDistance: z
56
+ .number()
57
+ .optional()
58
+ .describe('Distance at which attenuation color takes full effect'),
59
+ transmissionMap: z.string().optional().describe('Path to transmission map'),
60
+ })
61
+ .optional()
62
+ .describe('Light transmission (physical only)'),
64
63
 
65
- sheen: z
66
- .object({
67
- sheen: z.number().describe('Sheen intensity 0–1 (fabric, velvet)'),
68
- sheenColor: z.string().optional().describe('Sheen tint color'),
69
- sheenRoughness: z.number().optional().describe('Sheen roughness 0–1'),
70
- sheenColorMap: z.string().optional().describe('Path to sheen color map'),
71
- sheenRoughnessMap: z.string().optional().describe('Path to sheen roughness map'),
72
- })
73
- .optional()
74
- .describe('Sheen layer (physical only)'),
64
+ sheen: z
65
+ .object({
66
+ sheen: z.number().describe('Sheen intensity 0–1 (fabric, velvet)'),
67
+ sheenColor: z.string().optional().describe('Sheen tint color'),
68
+ sheenRoughness: z.number().optional().describe('Sheen roughness 0–1'),
69
+ sheenColorMap: z.string().optional().describe('Path to sheen color map'),
70
+ sheenRoughnessMap: z.string().optional().describe('Path to sheen roughness map'),
71
+ })
72
+ .optional()
73
+ .describe('Sheen layer (physical only)'),
75
74
 
76
- iridescence: z
77
- .object({
78
- iridescence: z.number().describe('Iridescence intensity 0–1 (soap bubbles, oil slick)'),
79
- iridescenceIOR: z.number().optional().describe('Thin-film IOR (default 1.3)'),
80
- iridescenceThicknessRange: z
81
- .tuple([z.number(), z.number()])
82
- .optional()
83
- .describe('Min/max thin-film thickness in nm'),
84
- iridescenceMap: z.string().optional().describe('Path to iridescence intensity map'),
85
- iridescenceThicknessMap: z
86
- .string()
87
- .optional()
88
- .describe('Path to iridescence thickness map'),
89
- })
90
- .optional()
91
- .describe('Iridescence thin-film (physical only)'),
75
+ iridescence: z
76
+ .object({
77
+ iridescence: z.number().describe('Iridescence intensity 0–1 (soap bubbles, oil slick)'),
78
+ iridescenceIOR: z.number().optional().describe('Thin-film IOR (default 1.3)'),
79
+ iridescenceThicknessRange: z
80
+ .tuple([z.number(), z.number()])
81
+ .optional()
82
+ .describe('Min/max thin-film thickness in nm'),
83
+ iridescenceMap: z.string().optional().describe('Path to iridescence intensity map'),
84
+ iridescenceThicknessMap: z.string().optional().describe('Path to iridescence thickness map'),
85
+ })
86
+ .optional()
87
+ .describe('Iridescence thin-film (physical only)'),
92
88
 
93
- displacementMap: z.string().optional().describe('Path to displacement/height map'),
94
- displacementScale: z.number().optional().describe('Displacement height multiplier'),
95
- displacementBias: z.number().optional().describe('Displacement offset'),
96
- })
97
- .describe('Material appearance properties');
89
+ displacementMap: z.string().optional().describe('Path to displacement/height map'),
90
+ displacementScale: z.number().optional().describe('Displacement height multiplier'),
91
+ displacementBias: z.number().optional().describe('Displacement offset'),
92
+ });
93
+
94
+ export const SceneMaterialSchema = SceneMaterialObjectSchema.superRefine((material, ctx) => {
95
+ if (material.type === 'custom' && !material.def?.trim()) {
96
+ ctx.addIssue({
97
+ code: 'custom',
98
+ path: ['def'],
99
+ message: 'Custom material requires a non-empty registered definition name',
100
+ });
101
+ }
102
+ }).describe('Material appearance properties');
98
103
 
99
104
  /** Partial schema for prefab instance overrides (type not required). */
100
- export const SceneMaterialOverrideSchema = SceneMaterialSchema.partial();
105
+ export const SceneMaterialOverrideSchema = SceneMaterialObjectSchema.partial();
101
106
 
102
107
  export type SceneMaterial = z.infer<typeof SceneMaterialSchema>;
103
108
 
@@ -1,7 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { type SceneEntity, SceneEntitySchema } from './entity';
3
3
  import { SceneEnvironmentSchema } from './environment';
4
- import { UIRootSchema } from './ui';
5
4
 
6
5
  export const SceneFileSchema = z
7
6
  .object({
@@ -9,13 +8,8 @@ export const SceneFileSchema = z
9
8
  name: z.string().describe('Scene display name'),
10
9
  environment: SceneEnvironmentSchema.optional().describe('Scene-level environment settings'),
11
10
  entities: z.array(SceneEntitySchema).describe('Top-level entities in the scene'),
12
- // UI roots (HUD/menus) attached to the scene graph as data. Optional and
13
- // additive — pre-UI scenes (no `ui` field) load unchanged (A4 versioning).
14
- ui: z
15
- .array(UIRootSchema)
16
- .optional()
17
- .describe('React UI roots (canvases) attached to the scene graph (A1)'),
18
11
  })
12
+ .strict()
19
13
  .describe('Scene file (.vscn.json)');
20
14
 
21
15
  export type SceneFile = z.infer<typeof SceneFileSchema>;
@@ -36,9 +36,14 @@
36
36
  *
37
37
  * Animation (load-bearing — ED5 disposal contract):
38
38
  * - `_animMixer` — THREE.AnimationMixer driving this subtree's clips.
39
- * - `_animGraph` — live AnimGraph instance (state machine) for this entity.
40
- * - `_animGraphData` — the AnimGraphFile the graph was built from (inspector cache).
41
- * - `_availableClips` string[] of clip names discovered on the GLTF.
39
+ * - `_animClips` — Map<string, AnimationClip> discovered on the GLTF (E5
40
+ * lets a GameComponent build its own XState-driven
41
+ * binding via `bindXStateAnimation` over the SAME
42
+ * mixer; see xstate-animation-binding.ts).
43
+ * - `_availableClips` — string[] of clip names discovered on the GLTF
44
+ * (inspector dropdown; same names as `_animClips`' keys).
45
+ * - `_xstateAnimation` — live native XState/Three binding exposed for
46
+ * play-mode inspection; removed when binding disposes.
42
47
  *
43
48
  * Disposal contract:
44
49
  * - `__sharedGeometry` — `true` when a mesh's geometry is shared/cached and MUST
@@ -74,6 +79,8 @@
74
79
  * - `editorHelper` — `true` for editor-only helper objects (gizmos, wireframes).
75
80
  * - `editorHelperType` — which kind of helper (lights/particles/pivot/navmesh/...).
76
81
  * - `editorIcon` — `true` for editor billboard icon sprites.
82
+ * - `skeletonVisible` — per-entity editor preference for its bone overlay.
83
+ * - `skeletonEnabled` — resolved visibility preference on a skeleton helper.
77
84
  * - `envObject` — `true` for environment objects (ambient light, etc.).
78
85
  * - `splineControlPoint` — index of a spline control-point drag handle.
79
86
  * - `vcDirIdx` — view-cube face direction index (0=+X,1=-X,2=+Y,...).
@@ -81,8 +88,8 @@
81
88
 
82
89
  import type * as THREE from 'three';
83
90
  import type { ParticleSystem } from 'three.quarks';
84
- import type { AnimGraph } from '../animation/anim-graph';
85
- import type { AnimGraphFile } from '../animation/anim-graph-types';
91
+ import type { AnyActor } from 'xstate';
92
+ import type { XStateAnimationBinding } from '../animation/xstate-animation-binding';
86
93
  import type { SceneEntity } from './scene-types';
87
94
 
88
95
  /** The kinds of editor helper objects tagged via `editorHelperType`. */
@@ -94,7 +101,8 @@ export type EditorHelperType =
94
101
  | 'splines'
95
102
  | 'particles'
96
103
  | 'pivot'
97
- | 'navmesh';
104
+ | 'navmesh'
105
+ | 'skeletons';
98
106
 
99
107
  /**
100
108
  * Maps each canonical accessor name to its value type. This is the single
@@ -112,9 +120,9 @@ export interface UserDataSchema {
112
120
  _camera: THREE.Camera;
113
121
  _particleSystem: ParticleSystem;
114
122
  _animMixer: THREE.AnimationMixer;
115
- _animGraph: AnimGraph;
116
- _animGraphData: AnimGraphFile;
123
+ _animClips: Map<string, THREE.AnimationClip>;
117
124
  _availableClips: string[];
125
+ _xstateAnimation: XStateAnimationBinding & { readonly actor: AnyActor };
118
126
  __sharedGeometry: boolean;
119
127
  __shadeOrig: THREE.Material | THREE.Material[];
120
128
  __shadeUnlit: THREE.Material[];
@@ -128,6 +136,8 @@ export interface UserDataSchema {
128
136
  editorHelper: boolean;
129
137
  editorHelperType: EditorHelperType;
130
138
  editorIcon: boolean;
139
+ skeletonVisible: boolean;
140
+ skeletonEnabled: boolean;
131
141
  envObject: boolean;
132
142
  vcDirIdx: number;
133
143
  }
@@ -152,9 +162,9 @@ export const UserDataKeys = {
152
162
  _camera: '_camera',
153
163
  _particleSystem: '_particleSystem',
154
164
  _animMixer: '_animMixer',
155
- _animGraph: '_animGraph',
156
- _animGraphData: '_animGraphData',
165
+ _animClips: '_animClips',
157
166
  _availableClips: '_availableClips',
167
+ _xstateAnimation: '_xstateAnimation',
158
168
  __sharedGeometry: '__sharedGeometry',
159
169
  __shadeOrig: '__shadeOrig',
160
170
  __shadeUnlit: '__shadeUnlit',
@@ -168,6 +178,8 @@ export const UserDataKeys = {
168
178
  editorHelper: 'editorHelper',
169
179
  editorHelperType: 'editorHelperType',
170
180
  editorIcon: 'editorIcon',
181
+ skeletonVisible: 'skeletonVisible',
182
+ skeletonEnabled: 'skeletonEnabled',
171
183
  envObject: 'envObject',
172
184
  vcDirIdx: 'vcDirIdx',
173
185
  } as const satisfies Record<UserDataKey, string>;
@@ -74,7 +74,7 @@ export function applyRendererSettings(
74
74
  * `opts` is additive (T6.1 slice 1, COMPOSITION-DESIGN.md D5 §1/§4): omitted
75
75
  * entirely, construction is byte-identical to before (no `alpha`/
76
76
  * `preserveDrawingBuffer` keys at all) — the legacy single-canvas host never
77
- * passes it. The worlds path passes `alpha:true` for every stacked canvas
77
+ * passes it. The roots path passes `alpha:true` for every stacked canvas
78
78
  * above the bottom one (so its clear-alpha-0 shows the layer below through
79
79
  * it) and `preserveDrawingBuffer:true` for every stacked canvas (the
80
80
  * recorded capture-tier cost, paid once here rather than re-derived later).
@@ -86,6 +86,11 @@ export function createHostRenderer(
86
86
  rendering?: RenderScope,
87
87
  opts?: { alpha?: boolean; preserveDrawingBuffer?: boolean },
88
88
  ): THREE.WebGLRenderer {
89
+ // A newly-shown editor tab can report 0x0 for one layout frame. WebGL render
90
+ // targets cannot have zero-sized attachments, so boot at the smallest valid
91
+ // backing size and let the normal resize path apply the real dimensions.
92
+ const safeWidth = Math.max(1, width);
93
+ const safeHeight = Math.max(1, height);
89
94
  const settings = resolveRenderSettings(rendering);
90
95
  const renderer = new THREE.WebGLRenderer({
91
96
  canvas,
@@ -94,7 +99,7 @@ export function createHostRenderer(
94
99
  ...(opts?.alpha ? { alpha: true } : {}),
95
100
  ...(opts?.preserveDrawingBuffer ? { preserveDrawingBuffer: true } : {}),
96
101
  });
97
- renderer.setSize(width, height);
102
+ renderer.setSize(safeWidth, safeHeight);
98
103
  applyRendererSettings(renderer, rendering);
99
104
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
100
105
  renderer.toneMapping = toneMappingModes[DEFAULTS.toneMapping.mode] ?? THREE.ACESFilmicToneMapping;
@@ -112,12 +117,14 @@ export function createSceneView(
112
117
  width = window.innerWidth,
113
118
  height = window.innerHeight,
114
119
  ): { scene: THREE.Scene; camera: THREE.PerspectiveCamera; composer: EffectComposer } {
120
+ const safeWidth = Math.max(1, width);
121
+ const safeHeight = Math.max(1, height);
115
122
  const scene = new THREE.Scene();
116
123
  scene.background = new THREE.Color(0x1a1a2e);
117
124
 
118
125
  const camera = new THREE.PerspectiveCamera(
119
126
  DEFAULTS.camera.fov,
120
- width / height,
127
+ safeWidth / safeHeight,
121
128
  DEFAULTS.camera.near,
122
129
  DEFAULTS.camera.far,
123
130
  );