@hraness/slopcamera 3.2.8 → 3.3.2

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 (153) hide show
  1. package/README.md +29 -9
  2. package/apps/desktop/application/context.ts +6 -0
  3. package/apps/desktop/application/default-registry.ts +26 -0
  4. package/apps/desktop/application/operation.ts +14 -1
  5. package/apps/desktop/application/operations/index.ts +4 -0
  6. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  7. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  8. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  9. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  10. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  11. package/apps/desktop/application/registry.ts +2 -2
  12. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  13. package/apps/desktop/application/spatial-assets.ts +160 -31
  14. package/apps/desktop/application/spatial-render.ts +217 -20
  15. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  16. package/apps/desktop/application/spatial-review.ts +361 -0
  17. package/apps/desktop/application/spatial-spz.ts +48 -3
  18. package/apps/desktop/application/spatial-world-import.ts +26 -5
  19. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  20. package/apps/desktop/cli/args.ts +514 -11
  21. package/apps/desktop/cli/capability-manifest.ts +374 -0
  22. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  23. package/apps/desktop/cli/command-host-resources.ts +29 -1
  24. package/apps/desktop/cli/commands.ts +1188 -4
  25. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  26. package/apps/desktop/cli/help.ts +172 -6
  27. package/apps/desktop/cli/main.ts +3 -3
  28. package/apps/desktop/cli/portable-surface.ts +6 -1
  29. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  30. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  31. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  32. package/apps/desktop/cli/spatial-scene-service.ts +271 -4
  33. package/apps/desktop/cli/workflow-code.ts +1 -1
  34. package/apps/desktop/code/semantic-builder.ts +24 -0
  35. package/apps/desktop/contracts/cinema.ts +736 -0
  36. package/apps/desktop/contracts/index.ts +1 -0
  37. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  38. package/apps/desktop/contracts/spatial-world.ts +26 -3
  39. package/apps/desktop/core/cinema-plan.ts +1320 -0
  40. package/apps/desktop/core/index.ts +1 -0
  41. package/apps/desktop/core/storage.ts +75 -0
  42. package/apps/desktop/dist/cli/main.js +725 -273
  43. package/apps/desktop/html-overlay/spatial.ts +619 -57
  44. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  45. package/apps/desktop/workflows/index.ts +6 -0
  46. package/dist/cli.js +25 -429
  47. package/dist/code/advanced.js +1 -269
  48. package/dist/code/index.js +16 -2239
  49. package/dist/generate.js +1 -23
  50. package/dist/host-resources.js +1 -25
  51. package/dist/index-0bbesn4m.js +35 -0
  52. package/dist/index-296qhpf2.js +3 -0
  53. package/dist/index-3cxf2c58.js +3 -0
  54. package/dist/index-77fjfg9f.js +3 -0
  55. package/dist/index-80a36bc0.js +3 -0
  56. package/dist/index-a3vvs3rf.js +3 -0
  57. package/dist/index-b3geqczn.js +5 -0
  58. package/dist/index-grk47tgs.js +3 -0
  59. package/dist/index-gs48afpt.js +7 -0
  60. package/dist/index-qry58nj2.js +10 -0
  61. package/dist/index-zhg36tf9.js +3 -0
  62. package/dist/index.js +1 -226
  63. package/dist/operations.js +1 -28
  64. package/dist/skill-install-gx7afxsd.js +2 -0
  65. package/dist/vectorize/worker.js +1 -141
  66. package/dist/workflow.js +1 -15
  67. package/docs/README.md +5 -1
  68. package/docs/studio.md +12 -0
  69. package/package.json +7 -5
  70. package/skills/slopcamera/SKILL.md +7 -1
  71. package/skills/slopcamera/references/directed-scenes.md +48 -2
  72. package/skills/slopcamera/references/gateway-media.md +15 -1
  73. package/skills/slopcamera/references/image-galleries.md +166 -0
  74. package/skills/slopcamera/references/install.md +1 -1
  75. package/skills/slopcamera/references/music-video.md +4 -0
  76. package/skills/slopcamera/references/native-studio.md +19 -0
  77. package/skills/slopcamera/references/parametric-design.md +14 -0
  78. package/skills/slopcamera/references/scene-building.md +172 -0
  79. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  80. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  81. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  82. package/src/capability-manifest.ts +361 -0
  83. package/src/cli.ts +222 -4
  84. package/src/code/canonical-json.ts +9 -0
  85. package/src/generate.ts +2 -2
  86. package/src/icon.ts +1111 -0
  87. package/src/image-gallery.ts +1049 -0
  88. package/src/index.ts +11 -0
  89. package/src/mcp/boundary.ts +11 -11
  90. package/src/mcp/index.ts +3 -0
  91. package/src/mcp/server.ts +2 -1
  92. package/src/mcp/tools.ts +1649 -132
  93. package/src/mcp/types.ts +19 -0
  94. package/src/operations.ts +412 -0
  95. package/src/portable-capability-manifest.ts +67 -0
  96. package/src/scene-gallery.ts +184 -0
  97. package/src/spatial-scene/asset-admission.ts +183 -0
  98. package/src/spatial-scene/audit-rendered.ts +692 -0
  99. package/src/spatial-scene/audit.ts +617 -0
  100. package/src/spatial-scene/behavior-audit.ts +252 -0
  101. package/src/spatial-scene/behavior-bake.ts +232 -0
  102. package/src/spatial-scene/behavior-fns.ts +585 -0
  103. package/src/spatial-scene/behavior-gallery.ts +108 -0
  104. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  105. package/src/spatial-scene/behavior-trace.ts +325 -0
  106. package/src/spatial-scene/behavior.ts +617 -0
  107. package/src/spatial-scene/build.ts +632 -0
  108. package/src/spatial-scene/camera-rig.ts +125 -0
  109. package/src/spatial-scene/camera-track.ts +4 -2
  110. package/src/spatial-scene/character.ts +124 -0
  111. package/src/spatial-scene/contracts.ts +75 -7
  112. package/src/spatial-scene/design-templates.ts +351 -0
  113. package/src/spatial-scene/design.ts +327 -0
  114. package/src/spatial-scene/direction-compile.ts +584 -0
  115. package/src/spatial-scene/direction.ts +122 -0
  116. package/src/spatial-scene/effects.ts +149 -0
  117. package/src/spatial-scene/evaluate.ts +109 -29
  118. package/src/spatial-scene/gallery.ts +277 -0
  119. package/src/spatial-scene/generate.ts +239 -0
  120. package/src/spatial-scene/geometry-native.ts +165 -0
  121. package/src/spatial-scene/geometry.ts +1216 -0
  122. package/src/spatial-scene/gltf.ts +600 -77
  123. package/src/spatial-scene/identity.ts +55 -2
  124. package/src/spatial-scene/index.ts +36 -0
  125. package/src/spatial-scene/inspect.ts +35 -6
  126. package/src/spatial-scene/material-lighting.ts +609 -0
  127. package/src/spatial-scene/math.ts +21 -4
  128. package/src/spatial-scene/motion-evidence.ts +88 -0
  129. package/src/spatial-scene/parametric.ts +765 -0
  130. package/src/spatial-scene/particle-preparation.ts +183 -0
  131. package/src/spatial-scene/particle.ts +195 -0
  132. package/src/spatial-scene/patch.ts +109 -15
  133. package/src/spatial-scene/performance.ts +1613 -0
  134. package/src/spatial-scene/probe.ts +137 -0
  135. package/src/spatial-scene/recipe-pack.ts +86 -0
  136. package/src/spatial-scene/render-effects.ts +240 -0
  137. package/src/spatial-scene/review.ts +639 -0
  138. package/src/spatial-scene/simulation-bake.ts +206 -0
  139. package/src/spatial-scene/simulation.ts +187 -0
  140. package/src/spatial-scene/solve.ts +366 -0
  141. package/src/spatial-scene/temporal-audit.ts +354 -0
  142. package/src/version.ts +1 -1
  143. package/dist/index-42zsesc1.js +0 -2257
  144. package/dist/index-7131pg1c.js +0 -1820
  145. package/dist/index-7308egqr.js +0 -48
  146. package/dist/index-8txs6fkn.js +0 -712
  147. package/dist/index-fava6pge.js +0 -192
  148. package/dist/index-h1k0fnjq.js +0 -2363
  149. package/dist/index-r7gdhmsp.js +0 -424
  150. package/dist/index-sh6xbav6.js +0 -1219
  151. package/dist/index-z1w83f81.js +0 -4
  152. package/dist/index-zfnddgay.js +0 -2029
  153. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -0,0 +1,1613 @@
1
+ import { z } from "zod"
2
+ import { canonicalJsonSha256 } from "../code/canonical-json.js"
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import { HUMANOID_BONE_NAMES, type SpatialHumanoidMapping } from "./character.js"
5
+ import {
6
+ SPATIAL_SCENE_LIMITS, SpatialDigestSchema, SpatialFrameRateSchema, SpatialQuaternionSchema,
7
+ SpatialTimeUsSchema, SpatialTransformSchema, SpatialVec3Schema,
8
+ type SpatialFrameRate,
9
+ } from "./contracts.js"
10
+ import { parseSpatialValue, sortSpatialBy, SpatialSceneError } from "./identity.js"
11
+ import {
12
+ composeTransform, multiplyTransforms, normalizeQuaternion, slerpQuaternion,
13
+ type Quaternion, type Vec3,
14
+ } from "./math.js"
15
+ import { spatialFrameCount, spatialFrameSample } from "./time.js"
16
+
17
+ export const SPATIAL_PERFORMANCE_LIMITS = Object.freeze({
18
+ directives: 256,
19
+ clips: 128,
20
+ props: 128,
21
+ samples: 1_024,
22
+ channels: 512,
23
+ keysPerChannel: SPATIAL_SCENE_LIMITS.keysPerChannel,
24
+ bones: 128,
25
+ morphTargets: 16,
26
+ findings: 1_024,
27
+ attachments: 64,
28
+ cameras: 64,
29
+ galleryCandidates: 64,
30
+ bakeProfiles: 32,
31
+ })
32
+
33
+ /** Canonical deterministic body masks. Each maps to an ordered bone set. */
34
+ export const SPATIAL_PERFORMANCE_BODY_MASKS = Object.freeze({
35
+ fullBody: HUMANOID_BONE_NAMES,
36
+ upperBody: [
37
+ "hips", "spine", "chest", "upperChest",
38
+ "leftShoulder", "leftUpperArm", "leftLowerArm", "leftHand",
39
+ "rightShoulder", "rightUpperArm", "rightLowerArm", "rightHand",
40
+ "neck", "head", "leftEye", "rightEye", "jaw",
41
+ "leftThumbProximal", "leftThumbIntermediate", "leftThumbDistal",
42
+ "leftIndexProximal", "leftIndexIntermediate", "leftIndexDistal",
43
+ "leftMiddleProximal", "leftMiddleIntermediate", "leftMiddleDistal",
44
+ "leftRingProximal", "leftRingIntermediate", "leftRingDistal",
45
+ "leftLittleProximal", "leftLittleIntermediate", "leftLittleDistal",
46
+ "rightThumbProximal", "rightThumbIntermediate", "rightThumbDistal",
47
+ "rightIndexProximal", "rightIndexIntermediate", "rightIndexDistal",
48
+ "rightMiddleProximal", "rightMiddleIntermediate", "rightMiddleDistal",
49
+ "rightRingProximal", "rightRingIntermediate", "rightRingDistal",
50
+ "rightLittleProximal", "rightLittleIntermediate", "rightLittleDistal",
51
+ ],
52
+ lowerBody: [
53
+ "hips", "spine",
54
+ "leftUpperLeg", "leftLowerLeg", "leftFoot", "leftToes",
55
+ "rightUpperLeg", "rightLowerLeg", "rightFoot", "rightToes",
56
+ ],
57
+ head: ["neck", "head", "leftEye", "rightEye", "jaw"],
58
+ spine: ["hips", "spine", "chest", "upperChest", "neck"],
59
+ leftArm: ["leftShoulder", "leftUpperArm", "leftLowerArm", "leftHand"],
60
+ rightArm: ["rightShoulder", "rightUpperArm", "rightLowerArm", "rightHand"],
61
+ leftArmFull: ["leftShoulder", "leftUpperArm", "leftLowerArm", "leftHand",
62
+ "leftThumbProximal", "leftThumbIntermediate", "leftThumbDistal",
63
+ "leftIndexProximal", "leftIndexIntermediate", "leftIndexDistal",
64
+ "leftMiddleProximal", "leftMiddleIntermediate", "leftMiddleDistal",
65
+ "leftRingProximal", "leftRingIntermediate", "leftRingDistal",
66
+ "leftLittleProximal", "leftLittleIntermediate", "leftLittleDistal"],
67
+ rightArmFull: ["rightShoulder", "rightUpperArm", "rightLowerArm", "rightHand",
68
+ "rightThumbProximal", "rightThumbIntermediate", "rightThumbDistal",
69
+ "rightIndexProximal", "rightIndexIntermediate", "rightIndexDistal",
70
+ "rightMiddleProximal", "rightMiddleIntermediate", "rightMiddleDistal",
71
+ "rightRingProximal", "rightRingIntermediate", "rightRingDistal",
72
+ "rightLittleProximal", "rightLittleIntermediate", "rightLittleDistal"],
73
+ leftLeg: ["leftUpperLeg", "leftLowerLeg", "leftFoot", "leftToes"],
74
+ rightLeg: ["rightUpperLeg", "rightLowerLeg", "rightFoot", "rightToes"],
75
+ } as const)
76
+
77
+ export type SpatialPerformanceBodyMaskName = keyof typeof SPATIAL_PERFORMANCE_BODY_MASKS
78
+
79
+ export const SPATIAL_PERFORMANCE_COMPILER_ID = "slopcamera.spatial-performance-compiler@v3"
80
+
81
+ const humanoidBoneName = z.enum(HUMANOID_BONE_NAMES)
82
+ const unit = z.number().finite().min(0).max(1)
83
+ const finiteCoord = z.number().finite().min(-1_000_000).max(1_000_000)
84
+ const positiveTimeScale = z.number().finite().min(0.001).max(1_000)
85
+ const directiveId = z.string().min(1).max(128)
86
+ const bodyMaskName = z.enum(Object.keys(SPATIAL_PERFORMANCE_BODY_MASKS) as [string, ...string[]])
87
+ const bodyMaskBones = z.array(humanoidBoneName).min(1).max(HUMANOID_BONE_NAMES.length)
88
+ const bodyMask = z.union([bodyMaskName, bodyMaskBones])
89
+
90
+ const clipKey = z.strictObject({
91
+ timeUs: SpatialTimeUsSchema,
92
+ position: SpatialVec3Schema,
93
+ rotation: SpatialQuaternionSchema,
94
+ })
95
+
96
+ export const SpatialPerformanceClipSchema = z.strictObject({
97
+ kind: z.literal("slopcamera.spatial-performance-clip"),
98
+ schemaVersion: z.literal(1),
99
+ durationUs: SpatialTimeUsSchema.refine((value) => value > 0, "Clip duration must be positive."),
100
+ channels: z.array(z.strictObject({
101
+ bone: humanoidBoneName,
102
+ keys: z.array(clipKey).min(1).max(SPATIAL_PERFORMANCE_LIMITS.keysPerChannel),
103
+ })).min(1).max(SPATIAL_PERFORMANCE_LIMITS.channels),
104
+ }).superRefine((clip, context) => {
105
+ for (const [index, channel] of clip.channels.entries()) {
106
+ const times = channel.keys.map((key) => key.timeUs)
107
+ for (let i = 1; i < times.length; i += 1) {
108
+ if (times[i]! <= times[i - 1]!) {
109
+ context.addIssue({ code: "custom", path: ["channels", index, "keys"], message: "Clip keys must be sorted by time." })
110
+ }
111
+ }
112
+ if (channel.keys[0]!.timeUs > 0 || channel.keys[channel.keys.length - 1]!.timeUs < clip.durationUs) {
113
+ context.addIssue({ code: "custom", path: ["channels", index], message: "Clip channel keys must cover [0, durationUs]." })
114
+ }
115
+ }
116
+ })
117
+
118
+ export const SpatialPerformancePropSchema = z.strictObject({
119
+ propId: z.string().min(1).max(128),
120
+ localOffset: SpatialTransformSchema,
121
+ })
122
+
123
+ const clipDirective = z.strictObject({
124
+ directiveId,
125
+ kind: z.literal("clip"),
126
+ clipDigest: SpatialDigestSchema,
127
+ startUs: SpatialTimeUsSchema,
128
+ endUs: SpatialTimeUsSchema,
129
+ trimStartUs: SpatialTimeUsSchema,
130
+ trimEndUs: SpatialTimeUsSchema,
131
+ loop: z.union([z.number().int().min(1).max(1_024), z.literal("once")]),
132
+ timeScale: positiveTimeScale,
133
+ mode: z.enum(["override", "additive"]).optional().default("override"),
134
+ mask: bodyMask.optional(),
135
+ }).superRefine((directive, context) => {
136
+ if (directive.endUs <= directive.startUs) context.addIssue({ code: "custom", path: ["endUs"], message: "Clip output end must exceed start." })
137
+ if (directive.trimEndUs <= directive.trimStartUs) context.addIssue({ code: "custom", path: ["trimEndUs"], message: "Clip trim end must exceed start." })
138
+ if (directive.mode === "additive" && directive.loop !== "once") {
139
+ context.addIssue({ code: "custom", path: ["loop"], message: "Additive clips must loop once." })
140
+ }
141
+ })
142
+
143
+ const crossfadeDirective = z.strictObject({
144
+ directiveId,
145
+ kind: z.literal("crossfade"),
146
+ fromClipId: directiveId,
147
+ toClipId: directiveId,
148
+ startUs: SpatialTimeUsSchema,
149
+ durationUs: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.durationUs),
150
+ })
151
+
152
+ const lookAtDirective = z.strictObject({
153
+ directiveId,
154
+ kind: z.literal("look-at"),
155
+ bone: humanoidBoneName,
156
+ target: SpatialVec3Schema,
157
+ })
158
+
159
+ const twoBoneIkDirective = z.strictObject({
160
+ directiveId,
161
+ kind: z.literal("two-bone-ik"),
162
+ endBone: humanoidBoneName,
163
+ target: SpatialVec3Schema,
164
+ pole: SpatialVec3Schema,
165
+ policy: z.enum(["stretch", "preserve"]).default("stretch"),
166
+ })
167
+
168
+ const footPlantDirective = z.strictObject({
169
+ directiveId,
170
+ kind: z.literal("foot-plant"),
171
+ bone: humanoidBoneName,
172
+ groundY: finiteCoord,
173
+ })
174
+
175
+ const morphDirective = z.strictObject({
176
+ directiveId,
177
+ kind: z.literal("morph"),
178
+ index: z.number().int().min(0).max(SPATIAL_PERFORMANCE_LIMITS.morphTargets - 1),
179
+ weight: unit,
180
+ })
181
+
182
+ const attachDirective = z.strictObject({
183
+ directiveId,
184
+ kind: z.literal("attach"),
185
+ propId: z.string().min(1).max(128),
186
+ bone: humanoidBoneName,
187
+ localOffset: SpatialTransformSchema,
188
+ startUs: SpatialTimeUsSchema,
189
+ endUs: SpatialTimeUsSchema,
190
+ }).superRefine((directive, context) => {
191
+ if (directive.endUs <= directive.startUs) context.addIssue({ code: "custom", path: ["endUs"], message: "Attach end must exceed start." })
192
+ })
193
+
194
+ const releaseDirective = z.strictObject({
195
+ directiveId,
196
+ kind: z.literal("release"),
197
+ propId: z.string().min(1).max(128),
198
+ startUs: SpatialTimeUsSchema,
199
+ })
200
+
201
+ const rootTrajectoryDirective = z.strictObject({
202
+ directiveId,
203
+ kind: z.literal("root-trajectory"),
204
+ waypoints: z.array(z.strictObject({
205
+ timeUs: SpatialTimeUsSchema,
206
+ position: SpatialVec3Schema,
207
+ rotation: SpatialQuaternionSchema,
208
+ })).min(2).max(SPATIAL_PERFORMANCE_LIMITS.samples),
209
+ }).superRefine((directive, context) => {
210
+ for (let i = 1; i < directive.waypoints.length; i += 1) {
211
+ if (directive.waypoints[i]!.timeUs <= directive.waypoints[i - 1]!.timeUs) {
212
+ context.addIssue({ code: "custom", path: ["waypoints"], message: "Trajectory waypoints must be sorted by time." })
213
+ }
214
+ }
215
+ })
216
+
217
+ const springDirective = z.strictObject({
218
+ directiveId,
219
+ kind: z.literal("spring"),
220
+ bone: humanoidBoneName,
221
+ amplitude: z.number().finite().min(0).max(1),
222
+ frequency: z.number().finite().min(0).max(100),
223
+ seed: z.number().int().min(0).max(0xffff_ffff),
224
+ })
225
+
226
+ export const SpatialPerformanceDirectiveSchema = z.discriminatedUnion("kind", [
227
+ clipDirective,
228
+ crossfadeDirective,
229
+ lookAtDirective,
230
+ twoBoneIkDirective,
231
+ footPlantDirective,
232
+ morphDirective,
233
+ attachDirective,
234
+ releaseDirective,
235
+ rootTrajectoryDirective,
236
+ springDirective,
237
+ ])
238
+ export type SpatialPerformanceDirective = DeepReadonly<z.infer<typeof SpatialPerformanceDirectiveSchema>>
239
+
240
+ export const SpatialPerformancePlanSchema = z.strictObject({
241
+ kind: z.literal("slopcamera.spatial-performance-plan"),
242
+ schemaVersion: z.literal(1),
243
+ sceneSha256: SpatialDigestSchema,
244
+ rigSha256: SpatialDigestSchema,
245
+ mappingSha256: SpatialDigestSchema,
246
+ compilerVersion: z.literal(SPATIAL_PERFORMANCE_COMPILER_ID),
247
+ seed: z.number().int().min(0).max(0xffff_ffff),
248
+ durationUs: SpatialTimeUsSchema.refine((value) => value > 0, "Duration must be positive."),
249
+ frameRate: SpatialFrameRateSchema,
250
+ directives: z.array(SpatialPerformanceDirectiveSchema).min(1).max(SPATIAL_PERFORMANCE_LIMITS.directives),
251
+ })
252
+
253
+ export const SpatialPerformanceSourcesSchema = z.strictObject({
254
+ sceneSha256: SpatialDigestSchema,
255
+ rigSha256: SpatialDigestSchema,
256
+ mapping: z.custom<SpatialHumanoidMapping>((value) => value !== null && typeof value === "object"),
257
+ clips: z.record(SpatialDigestSchema, SpatialPerformanceClipSchema).refine(
258
+ (record) => Object.keys(record).length <= SPATIAL_PERFORMANCE_LIMITS.clips,
259
+ "Too many source clips.",
260
+ ),
261
+ props: z.record(z.string().min(1).max(128), SpatialPerformancePropSchema).refine(
262
+ (record) => Object.keys(record).length <= SPATIAL_PERFORMANCE_LIMITS.props,
263
+ "Too many props.",
264
+ ),
265
+ })
266
+
267
+ export const SpatialPerformanceBoneChannelSchema = z.strictObject({
268
+ kind: z.literal("bone-pose"),
269
+ bone: humanoidBoneName,
270
+ keys: z.array(z.strictObject({
271
+ timeUs: SpatialTimeUsSchema,
272
+ position: SpatialVec3Schema,
273
+ rotation: SpatialQuaternionSchema,
274
+ })).max(SPATIAL_PERFORMANCE_LIMITS.samples),
275
+ })
276
+
277
+ export const SpatialPerformanceMorphChannelSchema = z.strictObject({
278
+ kind: z.literal("morph"),
279
+ index: z.number().int().min(0).max(SPATIAL_PERFORMANCE_LIMITS.morphTargets - 1),
280
+ keys: z.array(z.strictObject({
281
+ timeUs: SpatialTimeUsSchema,
282
+ value: unit,
283
+ })).max(SPATIAL_PERFORMANCE_LIMITS.samples),
284
+ })
285
+
286
+ export const SpatialPerformanceAttachmentChannelSchema = z.strictObject({
287
+ kind: z.literal("attachment"),
288
+ propId: z.string().min(1).max(128),
289
+ keys: z.array(z.strictObject({
290
+ timeUs: SpatialTimeUsSchema,
291
+ attached: z.boolean(),
292
+ parentBone: humanoidBoneName,
293
+ localOffset: SpatialTransformSchema,
294
+ })).max(SPATIAL_PERFORMANCE_LIMITS.samples),
295
+ })
296
+
297
+ export const SpatialPerformanceChannelSchema = z.discriminatedUnion("kind", [
298
+ SpatialPerformanceBoneChannelSchema,
299
+ SpatialPerformanceMorphChannelSchema,
300
+ SpatialPerformanceAttachmentChannelSchema,
301
+ ])
302
+
303
+ const attachmentSample = z.strictObject({
304
+ attached: z.boolean(),
305
+ parentBone: humanoidBoneName.optional(),
306
+ localOffset: SpatialTransformSchema.optional(),
307
+ worldPosition: SpatialVec3Schema,
308
+ })
309
+
310
+ export const SpatialPerformanceSampleSchema = z.strictObject({
311
+ timeUs: SpatialTimeUsSchema,
312
+ boneWorld: z.record(humanoidBoneName, z.strictObject({
313
+ position: SpatialVec3Schema,
314
+ rotation: SpatialQuaternionSchema,
315
+ })),
316
+ attachments: z.record(z.string().min(1).max(128), attachmentSample).refine(
317
+ (record) => Object.keys(record).length <= SPATIAL_PERFORMANCE_LIMITS.attachments,
318
+ "Too many attachments.",
319
+ ),
320
+ })
321
+
322
+ export const SpatialPerformanceReceiptSchema = z.strictObject({
323
+ kind: z.literal("slopcamera.spatial-performance-receipt"),
324
+ schemaVersion: z.literal(1),
325
+ compiler: z.literal(SPATIAL_PERFORMANCE_COMPILER_ID),
326
+ compilerSha256: SpatialDigestSchema,
327
+ sceneSha256: SpatialDigestSchema,
328
+ rigSha256: SpatialDigestSchema,
329
+ mappingSha256: SpatialDigestSchema,
330
+ directivesSha256: SpatialDigestSchema,
331
+ seed: z.number().int().min(0).max(0xffff_ffff),
332
+ outputSha256: SpatialDigestSchema,
333
+ durationUs: SpatialTimeUsSchema,
334
+ sampleCount: z.number().int().min(1).max(SPATIAL_PERFORMANCE_LIMITS.samples),
335
+ })
336
+
337
+ export const SpatialPerformanceTakeSchema = z.strictObject({
338
+ kind: z.literal("slopcamera.spatial-performance-take"),
339
+ schemaVersion: z.literal(1),
340
+ planSha256: SpatialDigestSchema,
341
+ durationUs: SpatialTimeUsSchema,
342
+ samples: z.array(SpatialPerformanceSampleSchema).max(SPATIAL_PERFORMANCE_LIMITS.samples),
343
+ channels: z.array(SpatialPerformanceChannelSchema).max(SPATIAL_PERFORMANCE_LIMITS.channels),
344
+ receipt: SpatialPerformanceReceiptSchema,
345
+ })
346
+
347
+ export const SpatialPerformanceFindingSchema = z.strictObject({
348
+ kind: z.enum([
349
+ "joint-limit",
350
+ "foot-slide",
351
+ "ground-penetration",
352
+ "gaze-error",
353
+ "attachment-drift",
354
+ "clip-discontinuity",
355
+ "character-camera-collision",
356
+ "character-character-collision",
357
+ ]),
358
+ severity: z.enum(["info", "warning"]),
359
+ entityId: z.string().min(1).max(128).optional(),
360
+ bone: humanoidBoneName.optional(),
361
+ timeUs: SpatialTimeUsSchema.optional(),
362
+ detail: z.string().min(1).max(1_024),
363
+ })
364
+
365
+ export const SpatialPerformanceAuditReportSchema = z.strictObject({
366
+ kind: z.literal("slopcamera.spatial-performance-audit"),
367
+ schemaVersion: z.literal(1),
368
+ takeSha256: SpatialDigestSchema,
369
+ findings: z.array(SpatialPerformanceFindingSchema).max(SPATIAL_PERFORMANCE_LIMITS.findings),
370
+ omittedFindings: z.number().int().min(0),
371
+ })
372
+
373
+ export const SpatialPerformanceAuditOptionsSchema = z.strictObject({
374
+ characterId: z.string().min(1).max(128).default("character"),
375
+ groundY: finiteCoord.optional(),
376
+ gazeTarget: SpatialVec3Schema.optional(),
377
+ cameras: z.array(z.strictObject({
378
+ cameraId: z.string().min(1).max(128),
379
+ position: SpatialVec3Schema,
380
+ near: z.number().finite().min(0.000001).max(1_000_000),
381
+ })).max(SPATIAL_PERFORMANCE_LIMITS.cameras).optional(),
382
+ otherCharacters: z.array(z.strictObject({
383
+ characterId: z.string().min(1).max(128),
384
+ position: SpatialVec3Schema,
385
+ radius: z.number().finite().min(0.000001).max(100),
386
+ })).max(16).optional(),
387
+ clipBoundaries: z.array(SpatialTimeUsSchema).max(SPATIAL_PERFORMANCE_LIMITS.directives).optional(),
388
+ })
389
+
390
+ export const SpatialPerformanceGalleryCandidateSchema = z.strictObject({
391
+ candidateId: z.string().min(1).max(128),
392
+ takeSha256: SpatialDigestSchema,
393
+ receiptOutputSha256: SpatialDigestSchema,
394
+ label: z.string().min(1).max(256),
395
+ })
396
+
397
+ export const SpatialPerformanceGalleryPlanSchema = z.strictObject({
398
+ kind: z.literal("slopcamera.spatial-performance-gallery-plan"),
399
+ schemaVersion: z.literal(1),
400
+ planSha256: SpatialDigestSchema,
401
+ candidates: z.array(SpatialPerformanceGalleryCandidateSchema)
402
+ .min(1)
403
+ .max(SPATIAL_PERFORMANCE_LIMITS.galleryCandidates),
404
+ }).superRefine((plan, context) => {
405
+ const ids = new Set<string>()
406
+ for (const [index, candidate] of plan.candidates.entries()) {
407
+ if (ids.has(candidate.candidateId)) {
408
+ context.addIssue({ code: "custom", path: ["candidates", index, "candidateId"], message: "Candidate id must be unique." })
409
+ }
410
+ ids.add(candidate.candidateId)
411
+ }
412
+ })
413
+
414
+ export const SpatialPerformanceGallerySelectionSchema = z.strictObject({
415
+ kind: z.literal("slopcamera.spatial-performance-gallery-selection"),
416
+ schemaVersion: z.literal(1),
417
+ galleryPlanSha256: SpatialDigestSchema,
418
+ selectedCandidateId: z.string().min(1).max(128),
419
+ takeSha256: SpatialDigestSchema,
420
+ receiptOutputSha256: SpatialDigestSchema,
421
+ })
422
+
423
+ export const SpatialPerformanceBakeEngineSchema = z.strictObject({
424
+ engineId: z.string().min(1).max(128),
425
+ profile: z.string().min(1).max(256),
426
+ })
427
+
428
+ export const SpatialPerformanceBakeInputSchema = z.strictObject({
429
+ inputSha256: SpatialDigestSchema,
430
+ inputProfile: z.string().min(1).max(256),
431
+ })
432
+
433
+ export const SpatialPerformanceBakeOutputSchema = z.strictObject({
434
+ outputProfile: z.string().min(1).max(256),
435
+ outputSha256: SpatialDigestSchema.optional(),
436
+ })
437
+
438
+ export const SpatialPerformanceBakeRequestSchema = z.strictObject({
439
+ kind: z.literal("slopcamera.spatial-performance-bake-request"),
440
+ schemaVersion: z.literal(1),
441
+ engine: SpatialPerformanceBakeEngineSchema,
442
+ input: SpatialPerformanceBakeInputSchema,
443
+ outputs: z.array(SpatialPerformanceBakeOutputSchema).min(1).max(SPATIAL_PERFORMANCE_LIMITS.bakeProfiles),
444
+ })
445
+
446
+ export const SpatialPerformanceBakeReceiptSchema = z.strictObject({
447
+ kind: z.literal("slopcamera.spatial-performance-bake-receipt"),
448
+ schemaVersion: z.literal(1),
449
+ requestSha256: SpatialDigestSchema,
450
+ engine: SpatialPerformanceBakeEngineSchema,
451
+ input: SpatialPerformanceBakeInputSchema,
452
+ outputs: z.array(z.strictObject({
453
+ outputProfile: z.string().min(1).max(256),
454
+ outputSha256: SpatialDigestSchema,
455
+ })).min(1).max(SPATIAL_PERFORMANCE_LIMITS.bakeProfiles),
456
+ })
457
+
458
+ type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T
459
+
460
+ export type SpatialPerformancePlan = DeepReadonly<z.infer<typeof SpatialPerformancePlanSchema>>
461
+ export type SpatialPerformanceClip = DeepReadonly<z.infer<typeof SpatialPerformanceClipSchema>>
462
+ export type SpatialPerformanceProp = DeepReadonly<z.infer<typeof SpatialPerformancePropSchema>>
463
+ export type SpatialPerformanceSources = DeepReadonly<z.infer<typeof SpatialPerformanceSourcesSchema>>
464
+ export type SpatialPerformanceTake = DeepReadonly<z.infer<typeof SpatialPerformanceTakeSchema>>
465
+ export type SpatialPerformanceBoneChannel = DeepReadonly<z.infer<typeof SpatialPerformanceBoneChannelSchema>>
466
+ export type SpatialPerformanceMorphChannel = DeepReadonly<z.infer<typeof SpatialPerformanceMorphChannelSchema>>
467
+ export type SpatialPerformanceAttachmentChannel = DeepReadonly<z.infer<typeof SpatialPerformanceAttachmentChannelSchema>>
468
+ export type SpatialPerformanceReceipt = DeepReadonly<z.infer<typeof SpatialPerformanceReceiptSchema>>
469
+ export type SpatialPerformanceFinding = DeepReadonly<z.infer<typeof SpatialPerformanceFindingSchema>>
470
+ export type SpatialPerformanceAuditReport = DeepReadonly<z.infer<typeof SpatialPerformanceAuditReportSchema>>
471
+ export type SpatialPerformanceAuditOptions = DeepReadonly<z.infer<typeof SpatialPerformanceAuditOptionsSchema>>
472
+ export type SpatialPerformanceGalleryPlan = DeepReadonly<z.infer<typeof SpatialPerformanceGalleryPlanSchema>>
473
+ export type SpatialPerformanceGalleryCandidate = DeepReadonly<z.infer<typeof SpatialPerformanceGalleryCandidateSchema>>
474
+ export type SpatialPerformanceGallerySelection = DeepReadonly<z.infer<typeof SpatialPerformanceGallerySelectionSchema>>
475
+ export type SpatialPerformanceBakeRequest = DeepReadonly<z.infer<typeof SpatialPerformanceBakeRequestSchema>>
476
+ export type SpatialPerformanceBakeReceipt = DeepReadonly<z.infer<typeof SpatialPerformanceBakeReceiptSchema>>
477
+
478
+ const BONE_PARENTS: Readonly<Record<string, string | null>> = Object.freeze({
479
+ hips: null,
480
+ spine: "hips",
481
+ chest: "spine",
482
+ upperChest: "chest",
483
+ neck: "upperChest",
484
+ head: "neck",
485
+ leftEye: "head",
486
+ rightEye: "head",
487
+ jaw: "head",
488
+ leftShoulder: "upperChest",
489
+ leftUpperArm: "leftShoulder",
490
+ leftLowerArm: "leftUpperArm",
491
+ leftHand: "leftLowerArm",
492
+ leftUpperLeg: "hips",
493
+ leftLowerLeg: "leftUpperLeg",
494
+ leftFoot: "leftLowerLeg",
495
+ leftToes: "leftFoot",
496
+ rightShoulder: "upperChest",
497
+ rightUpperArm: "rightShoulder",
498
+ rightLowerArm: "rightUpperArm",
499
+ rightHand: "rightLowerArm",
500
+ rightUpperLeg: "hips",
501
+ rightLowerLeg: "rightUpperLeg",
502
+ rightFoot: "rightLowerLeg",
503
+ rightToes: "rightFoot",
504
+ leftThumbProximal: "leftHand",
505
+ leftThumbIntermediate: "leftThumbProximal",
506
+ leftThumbDistal: "leftThumbIntermediate",
507
+ leftIndexProximal: "leftHand",
508
+ leftIndexIntermediate: "leftIndexProximal",
509
+ leftIndexDistal: "leftIndexIntermediate",
510
+ leftMiddleProximal: "leftHand",
511
+ leftMiddleIntermediate: "leftMiddleProximal",
512
+ leftMiddleDistal: "leftMiddleIntermediate",
513
+ leftRingProximal: "leftHand",
514
+ leftRingIntermediate: "leftRingProximal",
515
+ leftRingDistal: "leftRingIntermediate",
516
+ leftLittleProximal: "leftHand",
517
+ leftLittleIntermediate: "leftLittleProximal",
518
+ leftLittleDistal: "leftLittleIntermediate",
519
+ rightThumbProximal: "rightHand",
520
+ rightThumbIntermediate: "rightThumbProximal",
521
+ rightThumbDistal: "rightThumbIntermediate",
522
+ rightIndexProximal: "rightHand",
523
+ rightIndexIntermediate: "rightIndexProximal",
524
+ rightIndexDistal: "rightIndexIntermediate",
525
+ rightMiddleProximal: "rightHand",
526
+ rightMiddleIntermediate: "rightMiddleProximal",
527
+ rightMiddleDistal: "rightMiddleIntermediate",
528
+ rightRingProximal: "rightHand",
529
+ rightRingIntermediate: "rightRingProximal",
530
+ rightRingDistal: "rightRingIntermediate",
531
+ rightLittleProximal: "rightHand",
532
+ rightLittleIntermediate: "rightLittleProximal",
533
+ rightLittleDistal: "rightLittleIntermediate",
534
+ })
535
+
536
+ function assertBoneExists(mapping: SpatialHumanoidMapping, bone: string, path: string): void {
537
+ if (mapping.bones.find((b) => b.canonicalName === bone) === undefined) {
538
+ throw new SpatialSceneError("not-found", `Canonical bone ${bone} is not mapped.`, path)
539
+ }
540
+ }
541
+
542
+ function assertPropExists(sources: SpatialPerformanceSources, propId: string, path: string): void {
543
+ if (sources.props[propId] === undefined) {
544
+ throw new SpatialSceneError("not-found", `Prop ${propId} is not defined.`, path)
545
+ }
546
+ }
547
+
548
+ function validatePerformancePlan(plan: SpatialPerformancePlan, sources: SpatialPerformanceSources): void {
549
+ const directiveIds = new Set<string>()
550
+ const clipDirectives = plan.directives.filter((d): d is Extract<SpatialPerformancePlan["directives"][number], { kind: "clip" }> => d.kind === "clip")
551
+ const clipById = new Map(clipDirectives.map((d) => [d.directiveId, d] as const))
552
+ const mappedBones = new Set<string>(sources.mapping.bones.map((bone) => bone.canonicalName))
553
+
554
+ const usedBones = new Set<string>()
555
+ for (const directive of plan.directives) {
556
+ if ("bone" in directive && directive.bone !== undefined) usedBones.add(directive.bone)
557
+ if ("endBone" in directive && directive.endBone !== undefined) usedBones.add(directive.endBone)
558
+ }
559
+ for (const clip of Object.values(sources.clips)) {
560
+ for (const channel of clip.channels) usedBones.add(channel.bone)
561
+ }
562
+ for (const bone of usedBones) {
563
+ if (!mappedBones.has(bone)) continue
564
+ const parent = BONE_PARENTS[bone]
565
+ if (parent !== null && parent !== undefined && !mappedBones.has(parent)) {
566
+ throw new SpatialSceneError("invalid-data", `Mapped bone ${bone} has unmapped parent ${parent}.`, "performance.mapping")
567
+ }
568
+ }
569
+
570
+ for (const directive of plan.directives) {
571
+ if (directiveIds.has(directive.directiveId)) {
572
+ throw new SpatialSceneError("invalid-data", `Duplicate directiveId ${directive.directiveId}.`, "performance.directives")
573
+ }
574
+ directiveIds.add(directive.directiveId)
575
+
576
+ if (directive.kind === "clip") {
577
+ if (directive.endUs > plan.durationUs) {
578
+ throw new SpatialSceneError("invalid-data", `Clip ${directive.directiveId} ends after plan duration.`, `performance.directive.${directive.directiveId}`)
579
+ }
580
+ const clip = sources.clips[directive.clipDigest]
581
+ if (clip === undefined) {
582
+ throw new SpatialSceneError("not-found", `Clip ${directive.clipDigest} is missing.`, `performance.directive.${directive.directiveId}`)
583
+ }
584
+ if (directive.trimStartUs > clip.durationUs || directive.trimEndUs > clip.durationUs) {
585
+ throw new SpatialSceneError("invalid-data", `Clip ${directive.directiveId} trim bounds exceed clip duration.`, `performance.directive.${directive.directiveId}`)
586
+ }
587
+ for (const channel of clip.channels) {
588
+ if (!mappedBones.has(channel.bone)) {
589
+ throw new SpatialSceneError("invalid-data", `Clip ${directive.clipDigest} references unmapped bone ${channel.bone}.`, `performance.directive.${directive.directiveId}`)
590
+ }
591
+ }
592
+ if (directive.mode === "additive" && directive.mask === undefined) {
593
+ throw new SpatialSceneError("invalid-data", `Additive clip ${directive.directiveId} must declare a body mask.`, `performance.directive.${directive.directiveId}`)
594
+ }
595
+ } else if (directive.kind === "crossfade") {
596
+ const from = clipById.get(directive.fromClipId)
597
+ const to = clipById.get(directive.toClipId)
598
+ if (from === undefined || from.kind !== "clip") {
599
+ throw new SpatialSceneError("not-found", `Crossfade ${directive.directiveId} references unknown fromClipId ${directive.fromClipId}.`, `performance.directive.${directive.directiveId}`)
600
+ }
601
+ if (to === undefined || to.kind !== "clip") {
602
+ throw new SpatialSceneError("not-found", `Crossfade ${directive.directiveId} references unknown toClipId ${directive.toClipId}.`, `performance.directive.${directive.directiveId}`)
603
+ }
604
+ if (from.mode === "additive" || to.mode === "additive") {
605
+ throw new SpatialSceneError("invalid-data", `Crossfade ${directive.directiveId} cannot reference additive clips.`, `performance.directive.${directive.directiveId}`)
606
+ }
607
+ if (directive.startUs < from.startUs || directive.startUs < to.startUs) {
608
+ throw new SpatialSceneError("invalid-data", `Crossfade ${directive.directiveId} starts before one of its clips.`, `performance.directive.${directive.directiveId}`)
609
+ }
610
+ const fadeEnd = directive.startUs + directive.durationUs
611
+ if (fadeEnd > plan.durationUs) {
612
+ throw new SpatialSceneError("invalid-data", `Crossfade ${directive.directiveId} ends after plan duration.`, `performance.directive.${directive.directiveId}`)
613
+ }
614
+ if (fadeEnd > from.endUs || fadeEnd > to.endUs) {
615
+ throw new SpatialSceneError("invalid-data", `Crossfade ${directive.directiveId} extends beyond one of its clips.`, `performance.directive.${directive.directiveId}`)
616
+ }
617
+ } else if (directive.kind === "attach") {
618
+ if (directive.endUs > plan.durationUs) {
619
+ throw new SpatialSceneError("invalid-data", `Attach ${directive.directiveId} ends after plan duration.`, `performance.directive.${directive.directiveId}`)
620
+ }
621
+ } else if (directive.kind === "release") {
622
+ if (directive.startUs > plan.durationUs) {
623
+ throw new SpatialSceneError("invalid-data", `Release ${directive.directiveId} starts after plan duration.`, `performance.directive.${directive.directiveId}`)
624
+ }
625
+ } else if (directive.kind === "two-bone-ik") {
626
+ const endBone = directive.endBone
627
+ const lowerBone = BONE_PARENTS[endBone]
628
+ const upperBone = lowerBone !== null && lowerBone !== undefined ? BONE_PARENTS[lowerBone] : undefined
629
+ if (lowerBone === null || lowerBone === undefined || upperBone === null || upperBone === undefined) {
630
+ throw new SpatialSceneError("invalid-data", `Two-bone IK ${directive.directiveId} requires at least two parent bones above ${endBone}.`, `performance.directive.${directive.directiveId}`)
631
+ }
632
+ for (const bone of [endBone, lowerBone, upperBone]) {
633
+ if (!mappedBones.has(bone)) {
634
+ throw new SpatialSceneError("invalid-data", `Two-bone IK ${directive.directiveId} chain bone ${bone} is not mapped.`, `performance.directive.${directive.directiveId}`)
635
+ }
636
+ }
637
+ } else if (directive.kind === "root-trajectory") {
638
+ for (const waypoint of directive.waypoints) {
639
+ if (waypoint.timeUs > plan.durationUs) {
640
+ throw new SpatialSceneError("invalid-data", `Root trajectory waypoint time ${waypoint.timeUs} exceeds plan duration.`, `performance.directive.${directive.directiveId}`)
641
+ }
642
+ }
643
+ }
644
+ }
645
+ }
646
+
647
+ export function parseSpatialPerformancePlan(input: unknown): SpatialPerformancePlan {
648
+ const value = parseSpatialValue(SpatialPerformancePlanSchema, input, "performance plan")
649
+ return deepFreezeJson(value) as unknown as SpatialPerformancePlan
650
+ }
651
+
652
+ export function parseSpatialPerformanceSources(input: unknown): SpatialPerformanceSources {
653
+ const value = parseSpatialValue(SpatialPerformanceSourcesSchema, input, "performance sources")
654
+ return deepFreezeJson(value) as unknown as SpatialPerformanceSources
655
+ }
656
+
657
+ export function parseSpatialPerformanceAuditOptions(input: unknown): SpatialPerformanceAuditOptions {
658
+ const value = parseSpatialValue(SpatialPerformanceAuditOptionsSchema, input, "performance audit options")
659
+ return deepFreezeJson(value) as unknown as SpatialPerformanceAuditOptions
660
+ }
661
+
662
+ export function parseSpatialPerformanceGalleryPlan(input: unknown): SpatialPerformanceGalleryPlan {
663
+ const value = parseSpatialValue(SpatialPerformanceGalleryPlanSchema, input, "performance gallery plan")
664
+ return deepFreezeJson(value) as unknown as SpatialPerformanceGalleryPlan
665
+ }
666
+
667
+ export function parseSpatialPerformanceGallerySelection(input: unknown): SpatialPerformanceGallerySelection {
668
+ const value = parseSpatialValue(SpatialPerformanceGallerySelectionSchema, input, "performance gallery selection")
669
+ return deepFreezeJson(value) as unknown as SpatialPerformanceGallerySelection
670
+ }
671
+
672
+ export function parseSpatialPerformanceBakeRequest(input: unknown): SpatialPerformanceBakeRequest {
673
+ const value = parseSpatialValue(SpatialPerformanceBakeRequestSchema, input, "performance bake request")
674
+ return deepFreezeJson(value) as unknown as SpatialPerformanceBakeRequest
675
+ }
676
+
677
+ export function parseSpatialPerformanceBakeReceipt(input: unknown): SpatialPerformanceBakeReceipt {
678
+ const value = parseSpatialValue(SpatialPerformanceBakeReceiptSchema, input, "performance bake receipt")
679
+ return deepFreezeJson(value) as unknown as SpatialPerformanceBakeReceipt
680
+ }
681
+
682
+ export function validatePerformanceGallerySelection(
683
+ selection: SpatialPerformanceGallerySelection,
684
+ plan: SpatialPerformanceGalleryPlan,
685
+ take: SpatialPerformanceTake,
686
+ ): void {
687
+ const planSha = canonicalJsonSha256(plan)
688
+ if (planSha !== selection.galleryPlanSha256) {
689
+ throw new SpatialSceneError("invalid-data", "Stale or mismatched gallery plan digest.", "performance.gallery.plan")
690
+ }
691
+ const candidate = plan.candidates.find((c) => c.candidateId === selection.selectedCandidateId)
692
+ if (candidate === undefined) {
693
+ throw new SpatialSceneError("not-found", `Candidate ${selection.selectedCandidateId} is not in the gallery plan.`, "performance.gallery.selection")
694
+ }
695
+ const takeSha = canonicalJsonSha256(take)
696
+ if (takeSha !== selection.takeSha256) {
697
+ throw new SpatialSceneError("invalid-data", "Stale or mismatched take digest.", "performance.gallery.take")
698
+ }
699
+ if (take.receipt.outputSha256 !== selection.receiptOutputSha256) {
700
+ throw new SpatialSceneError("invalid-data", "Stale or mismatched receipt output digest.", "performance.gallery.receipt")
701
+ }
702
+ if (candidate.takeSha256 !== selection.takeSha256 || candidate.receiptOutputSha256 !== selection.receiptOutputSha256) {
703
+ throw new SpatialSceneError("invalid-data", "Selection does not match the gallery candidate's bound digests.", "performance.gallery.selection")
704
+ }
705
+ }
706
+
707
+ export function validatePerformanceBakeReceipt(request: SpatialPerformanceBakeRequest, receipt: SpatialPerformanceBakeReceipt): void {
708
+ const requestSha = canonicalJsonSha256(request)
709
+ if (requestSha !== receipt.requestSha256) {
710
+ throw new SpatialSceneError("invalid-data", "Stale or mismatched bake request digest.", "performance.bake.request")
711
+ }
712
+ if (request.engine.engineId !== receipt.engine.engineId || request.engine.profile !== receipt.engine.profile) {
713
+ throw new SpatialSceneError("invalid-data", "Bake engine/profile identity mismatch.", "performance.bake.engine")
714
+ }
715
+ if (request.input.inputSha256 !== receipt.input.inputSha256 || request.input.inputProfile !== receipt.input.inputProfile) {
716
+ throw new SpatialSceneError("invalid-data", "Bake input identity mismatch.", "performance.bake.input")
717
+ }
718
+ if (receipt.outputs.length !== request.outputs.length) {
719
+ throw new SpatialSceneError("invalid-data", "Bake output count mismatch.", "performance.bake.outputs")
720
+ }
721
+ const requestProfiles = new Set(request.outputs.map((output) => output.outputProfile))
722
+ for (const output of receipt.outputs) {
723
+ if (!requestProfiles.has(output.outputProfile)) {
724
+ throw new SpatialSceneError("invalid-data", `Bake output profile ${output.outputProfile} is not in the request.`, "performance.bake.outputs")
725
+ }
726
+ }
727
+ }
728
+
729
+ function vec3Add(a: Vec3, b: Vec3): Vec3 {
730
+ return Object.freeze([a[0] + b[0], a[1] + b[1], a[2] + b[2]])
731
+ }
732
+
733
+ function vec3Sub(a: Vec3, b: Vec3): Vec3 {
734
+ return Object.freeze([a[0] - b[0], a[1] - b[1], a[2] - b[2]])
735
+ }
736
+
737
+ function vec3Length(a: Vec3): number {
738
+ return Math.hypot(a[0], a[1], a[2])
739
+ }
740
+
741
+ function vec3Normalize(a: Vec3): Vec3 {
742
+ const length = vec3Length(a)
743
+ if (length === 0) return Object.freeze([0, 0, 1])
744
+ return Object.freeze([a[0] / length, a[1] / length, a[2] / length])
745
+ }
746
+
747
+ function vec3Dot(a: Vec3, b: Vec3): number {
748
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
749
+ }
750
+
751
+ function vec3Cross(a: Vec3, b: Vec3): Vec3 {
752
+ return Object.freeze([
753
+ a[1] * b[2] - a[2] * b[1],
754
+ a[2] * b[0] - a[0] * b[2],
755
+ a[0] * b[1] - a[1] * b[0],
756
+ ])
757
+ }
758
+
759
+ function multiplyQuaternionUnnormalized(a: Quaternion, b: Quaternion): Quaternion {
760
+ return Object.freeze([
761
+ a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
762
+ a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
763
+ a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
764
+ a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
765
+ ]) as unknown as Quaternion
766
+ }
767
+
768
+ function multiplyQuaternion(a: Quaternion, b: Quaternion): Quaternion {
769
+ return normalizeQuaternion(multiplyQuaternionUnnormalized(a, b))
770
+ }
771
+
772
+ function conjugateQuaternion(q: Quaternion): Quaternion {
773
+ return Object.freeze([-q[0], -q[1], -q[2], q[3]])
774
+ }
775
+
776
+ function rotateVectorByQuaternion(q: Quaternion, v: Vec3): Vec3 {
777
+ if (vec3Length(v) === 0) return Object.freeze([0, 0, 0])
778
+ const pure = multiplyQuaternionUnnormalized(q, [v[0], v[1], v[2], 0])
779
+ const rotated = multiplyQuaternionUnnormalized(pure, conjugateQuaternion(q))
780
+ return Object.freeze([rotated[0], rotated[1], rotated[2]])
781
+ }
782
+
783
+ function quaternionFromVectors(from: Vec3, to: Vec3): Quaternion {
784
+ const f = vec3Normalize(from)
785
+ const t = vec3Normalize(to)
786
+ const d = vec3Dot(f, t)
787
+ if (d > 0.999_999) return Object.freeze([0, 0, 0, 1])
788
+ if (d < -0.999_999) {
789
+ const axis = vec3Cross([0, 1, 0], f)
790
+ const n = vec3Length(axis)
791
+ const up = n < 0.000_001 ? ([1, 0, 0] as Vec3) : axis
792
+ return normalizeQuaternion([up[0], up[1], up[2], 0])
793
+ }
794
+ const cross = vec3Cross(f, t)
795
+ return normalizeQuaternion([cross[0], cross[1], cross[2], 1 + d])
796
+ }
797
+
798
+ function worldFromLocal(parentPosition: Vec3, parentRotation: Quaternion, localPosition: Vec3, localRotation: Quaternion): { position: Vec3; rotation: Quaternion } {
799
+ return {
800
+ position: vec3Add(parentPosition, rotateVectorByQuaternion(parentRotation, localPosition)),
801
+ rotation: multiplyQuaternion(parentRotation, localRotation),
802
+ }
803
+ }
804
+
805
+ function solveTwoBoneIk(
806
+ baseWorld: { position: Vec3; rotation: Quaternion },
807
+ upperLocal: { position: Vec3; rotation: Quaternion },
808
+ lowerLocal: { position: Vec3; rotation: Quaternion },
809
+ endLocal: { position: Vec3; rotation: Quaternion },
810
+ target: Vec3,
811
+ pole: Vec3,
812
+ policy: "stretch" | "preserve",
813
+ ): { upperRotation: Quaternion; lowerRotation: Quaternion } | null {
814
+ const a = vec3Length(lowerLocal.position)
815
+ const b = vec3Length(endLocal.position)
816
+ if (a <= 1e-7) return null
817
+ const targetOffset = vec3Sub(target, baseWorld.position)
818
+ let c = vec3Length(targetOffset)
819
+ const minReach = Math.abs(a - b)
820
+ const maxReach = a + b
821
+ if (c < minReach || c > maxReach) {
822
+ if (policy === "preserve") return null
823
+ c = Math.max(minReach, Math.min(maxReach, c))
824
+ }
825
+ if (c <= 1e-7) return null
826
+ const targetDir = vec3Normalize(targetOffset)
827
+ const poleDir = vec3Normalize(pole)
828
+ let normal = vec3Cross(poleDir, targetDir)
829
+ if (vec3Length(normal) < 1e-7) {
830
+ normal = vec3Cross([0, 1, 0], targetDir)
831
+ if (vec3Length(normal) < 1e-7) normal = vec3Cross([1, 0, 0], targetDir)
832
+ if (vec3Length(normal) < 1e-7) return null
833
+ }
834
+ normal = vec3Normalize(normal)
835
+ const perp = vec3Normalize(vec3Cross(targetDir, normal))
836
+ const cosAlpha = Math.max(-1, Math.min(1, (a * a + c * c - b * b) / (2 * a * c)))
837
+ const sinAlpha = Math.sqrt(Math.max(0, 1 - cosAlpha * cosAlpha))
838
+ const elbowOffset = vec3Add(vec3Scale(targetDir, a * cosAlpha), vec3Scale(perp, a * sinAlpha))
839
+ const elbowWorld = vec3Add(baseWorld.position, elbowOffset)
840
+ const upperRestDir = rotateVectorByQuaternion(upperLocal.rotation, vec3Normalize(lowerLocal.position))
841
+ const upperTargetDir = rotateVectorByQuaternion(conjugateQuaternion(baseWorld.rotation), vec3Normalize(vec3Sub(elbowWorld, baseWorld.position)))
842
+ const qUpper = quaternionFromVectors(upperRestDir, upperTargetDir)
843
+ const solvedUpperWorldRot = multiplyQuaternion(multiplyQuaternion(baseWorld.rotation, qUpper), upperLocal.rotation)
844
+ const lowerRestDir = rotateVectorByQuaternion(lowerLocal.rotation, vec3Normalize(endLocal.position))
845
+ const lowerTargetDir = rotateVectorByQuaternion(conjugateQuaternion(solvedUpperWorldRot), vec3Normalize(vec3Sub(target, elbowWorld)))
846
+ const qLower = quaternionFromVectors(lowerRestDir, lowerTargetDir)
847
+ return {
848
+ upperRotation: multiplyQuaternion(qUpper, upperLocal.rotation),
849
+ lowerRotation: multiplyQuaternion(qLower, lowerLocal.rotation),
850
+ }
851
+ }
852
+
853
+ export function poseFromMatrix(matrix: readonly number[]): { position: Vec3; rotation: Quaternion } {
854
+ const m = matrix
855
+ if (m.length < 16) throw new RangeError("Matrix must have 16 components.")
856
+ const position: Vec3 = Object.freeze([m[12]!, m[13]!, m[14]!])
857
+ const trace = m[0]! + m[5]! + m[10]!
858
+ let rotation: Quaternion
859
+ if (trace > 0) {
860
+ const s = 0.5 / Math.sqrt(trace + 1)
861
+ rotation = [(m[6]! - m[9]!) * s, (m[8]! - m[2]!) * s, (m[1]! - m[4]!) * s, 0.25 / s] as Quaternion
862
+ } else if (m[0]! > m[5]! && m[0]! > m[10]!) {
863
+ const s = 2 * Math.sqrt(1 + m[0]! - m[5]! - m[10]!)
864
+ rotation = [0.25 * s, (m[1]! + m[4]!) / s, (m[8]! + m[2]!) / s, (m[6]! - m[9]!) / s] as Quaternion
865
+ } else if (m[5]! > m[10]!) {
866
+ const s = 2 * Math.sqrt(1 + m[5]! - m[0]! - m[10]!)
867
+ rotation = [(m[1]! + m[4]!) / s, 0.25 * s, (m[6]! + m[9]!) / s, (m[8]! - m[2]!) / s] as Quaternion
868
+ } else {
869
+ const s = 2 * Math.sqrt(1 + m[10]! - m[0]! - m[5]!)
870
+ rotation = [(m[8]! + m[2]!) / s, (m[6]! + m[9]!) / s, 0.25 * s, (m[1]! - m[4]!) / s] as Quaternion
871
+ }
872
+ return { position, rotation: normalizeQuaternion(rotation) }
873
+ }
874
+
875
+ function lerpVec3(a: Vec3, b: Vec3, t: number): Vec3 {
876
+ return Object.freeze([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t])
877
+ }
878
+
879
+ function vec3Scale(a: Vec3, s: number): Vec3 {
880
+ return Object.freeze([a[0] * s, a[1] * s, a[2] * s])
881
+ }
882
+
883
+ function seededLcg(seed: number): { value: number; next: number } {
884
+ const next = (seed * 1_664_525 + 1_013_904_223) >>> 0
885
+ return { value: next / 0x1_0000_0000, next }
886
+ }
887
+
888
+ function springOffset(timeUs: number, seed: number, amplitude: number, frequency: number): { position: Vec3; rotation: Quaternion } {
889
+ let state = seed
890
+ const phases: number[] = []
891
+ for (let i = 0; i < 6; i += 1) {
892
+ const lcg = seededLcg(state)
893
+ state = lcg.next
894
+ phases.push(lcg.value * Math.PI * 2)
895
+ }
896
+ const t = timeUs / 1_000_000
897
+ const envelope = Math.exp(-t)
898
+ const px = Math.sin(2 * Math.PI * frequency * t + phases[0]!) * amplitude * envelope
899
+ const py = Math.sin(2 * Math.PI * frequency * t + phases[1]!) * amplitude * envelope
900
+ const pz = Math.sin(2 * Math.PI * frequency * t + phases[2]!) * amplitude * envelope
901
+ const rx = Math.sin(2 * Math.PI * frequency * t + phases[3]!) * amplitude * envelope * 0.5
902
+ const ry = Math.sin(2 * Math.PI * frequency * t + phases[4]!) * amplitude * envelope * 0.5
903
+ const rz = Math.sin(2 * Math.PI * frequency * t + phases[5]!) * amplitude * envelope * 0.5
904
+ const rw = Math.sqrt(Math.max(0, 1 - (rx * rx + ry * ry + rz * rz)))
905
+ return {
906
+ position: [px, py, pz] as unknown as Vec3,
907
+ rotation: normalizeQuaternion([rx, ry, rz, rw]),
908
+ }
909
+ }
910
+
911
+ function resolveBodyMask(mask: string | readonly string[] | undefined): Set<string> {
912
+ if (mask === undefined) return new Set(HUMANOID_BONE_NAMES)
913
+ if (typeof mask === "string") {
914
+ const bones = SPATIAL_PERFORMANCE_BODY_MASKS[mask as SpatialPerformanceBodyMaskName]
915
+ if (bones === undefined) throw new SpatialSceneError("invalid-data", `Unknown body mask ${mask}.`, "performance.mask")
916
+ return new Set(bones)
917
+ }
918
+ return new Set(mask)
919
+ }
920
+
921
+ function mappedBoneOrder(mapping: SpatialHumanoidMapping): readonly string[] {
922
+ const mapped = new Set<string>(mapping.bones.map((bone) => bone.canonicalName))
923
+ const result: string[] = []
924
+ const pending = ["hips"]
925
+ while (pending.length > 0) {
926
+ const bone = pending.shift()!
927
+ if (!mapped.has(bone)) continue
928
+ result.push(bone)
929
+ for (const [child, parent] of Object.entries(BONE_PARENTS)) {
930
+ if (parent === bone && mapped.has(child)) pending.push(child)
931
+ }
932
+ }
933
+ return Object.freeze(result)
934
+ }
935
+
936
+ function sampleClipChannel(channel: SpatialPerformanceClip["channels"][number], sourceTimeUs: number): { position: Vec3; rotation: Quaternion } | null {
937
+ const keys = channel.keys
938
+ if (sourceTimeUs <= keys[0]!.timeUs) {
939
+ return { position: keys[0]!.position, rotation: keys[0]!.rotation }
940
+ }
941
+ if (sourceTimeUs >= keys[keys.length - 1]!.timeUs) {
942
+ return { position: keys[keys.length - 1]!.position, rotation: keys[keys.length - 1]!.rotation }
943
+ }
944
+ for (let i = 1; i < keys.length; i += 1) {
945
+ const prev = keys[i - 1]!
946
+ const next = keys[i]!
947
+ if (sourceTimeUs >= prev.timeUs && sourceTimeUs <= next.timeUs) {
948
+ const t = (sourceTimeUs - prev.timeUs) / (next.timeUs - prev.timeUs)
949
+ return {
950
+ position: lerpVec3(prev.position, next.position, t),
951
+ rotation: slerpQuaternion(prev.rotation, next.rotation, t),
952
+ }
953
+ }
954
+ }
955
+ return null
956
+ }
957
+
958
+ function collectActiveClips(
959
+ timeUs: number,
960
+ clips: SpatialPerformanceSources["clips"],
961
+ directives: SpatialPerformancePlan["directives"],
962
+ mode?: "override" | "additive",
963
+ ): { directive: Extract<SpatialPerformancePlan["directives"][number], { kind: "clip" }>; sourceTimeUs: number }[] {
964
+ const result: { directive: Extract<SpatialPerformancePlan["directives"][number], { kind: "clip" }>; sourceTimeUs: number }[] = []
965
+ for (const directive of directives) {
966
+ if (directive.kind !== "clip") continue
967
+ if (mode !== undefined && directive.mode !== mode) continue
968
+ const clip = clips[directive.clipDigest]
969
+ if (clip === undefined) continue
970
+ if (timeUs < directive.startUs || timeUs >= directive.endUs) continue
971
+ const trimmedDuration = directive.trimEndUs - directive.trimStartUs
972
+ const sourceCycle = trimmedDuration / directive.timeScale
973
+ const loopCount = directive.loop === "once" ? 1 : directive.loop
974
+ const totalSource = sourceCycle * loopCount
975
+ const outputLocal = timeUs - directive.startUs
976
+ if (outputLocal >= totalSource) continue
977
+ const cycleLocal = outputLocal % sourceCycle
978
+ const sourceTimeUs = directive.trimStartUs + cycleLocal * directive.timeScale
979
+ result.push({ directive, sourceTimeUs })
980
+ }
981
+ return result
982
+ }
983
+
984
+ function activeCrossfade(
985
+ timeUs: number,
986
+ directives: SpatialPerformancePlan["directives"],
987
+ ): { from: string; to: string; alpha: number } | null {
988
+ for (const directive of directives) {
989
+ if (directive.kind !== "crossfade") continue
990
+ if (timeUs >= directive.startUs && timeUs < directive.startUs + directive.durationUs) {
991
+ const alpha = directive.durationUs > 0 ? (timeUs - directive.startUs) / directive.durationUs : 0
992
+ return { from: directive.fromClipId, to: directive.toClipId, alpha }
993
+ }
994
+ }
995
+ return null
996
+ }
997
+
998
+ function clipPoseForBone(
999
+ clips: SpatialPerformanceSources["clips"],
1000
+ directive: Extract<SpatialPerformancePlan["directives"][number], { kind: "clip" }>,
1001
+ sourceTimeUs: number,
1002
+ bone: string,
1003
+ boneToRest: ReadonlyMap<string, { position: Vec3; rotation: Quaternion }>,
1004
+ ): { position: Vec3; rotation: Quaternion } {
1005
+ const clip = clips[directive.clipDigest]
1006
+ if (clip === undefined) {
1007
+ const rest = boneToRest.get(bone)
1008
+ return rest ?? { position: [0, 0, 0] as unknown as Vec3, rotation: [0, 0, 0, 1] as unknown as Quaternion }
1009
+ }
1010
+ const channel = clip.channels.find((c) => c.bone === bone)
1011
+ if (channel === undefined) {
1012
+ const rest = boneToRest.get(bone)
1013
+ return rest ?? { position: [0, 0, 0] as unknown as Vec3, rotation: [0, 0, 0, 1] as unknown as Quaternion }
1014
+ }
1015
+ const sampled = sampleClipChannel(channel, sourceTimeUs)
1016
+ if (sampled !== null) return sampled
1017
+ return { position: [0, 0, 0] as unknown as Vec3, rotation: [0, 0, 0, 1] as unknown as Quaternion }
1018
+ }
1019
+
1020
+ function baseBonePose(
1021
+ clips: SpatialPerformanceSources["clips"],
1022
+ directives: SpatialPerformancePlan["directives"],
1023
+ timeUs: number,
1024
+ bone: string,
1025
+ boneToRest: ReadonlyMap<string, { position: Vec3; rotation: Quaternion }>,
1026
+ ): { position: Vec3; rotation: Quaternion } {
1027
+ const active = collectActiveClips(timeUs, clips, directives, "override")
1028
+ const maskedActive = active.filter((entry) => resolveBodyMask(entry.directive.mask).has(bone))
1029
+ const crossfade = activeCrossfade(timeUs, directives)
1030
+ if (maskedActive.length === 0) {
1031
+ const rest = boneToRest.get(bone)
1032
+ return rest ?? { position: [0, 0, 0] as unknown as Vec3, rotation: [0, 0, 0, 1] as unknown as Quaternion }
1033
+ }
1034
+ if (crossfade !== null) {
1035
+ const fromDirective = maskedActive.find((a) => a.directive.directiveId === crossfade.from)
1036
+ const toDirective = maskedActive.find((a) => a.directive.directiveId === crossfade.to)
1037
+ if (fromDirective !== undefined && toDirective !== undefined) {
1038
+ const fromPose = clipPoseForBone(clips, fromDirective.directive, fromDirective.sourceTimeUs, bone, boneToRest)
1039
+ const toPose = clipPoseForBone(clips, toDirective.directive, toDirective.sourceTimeUs, bone, boneToRest)
1040
+ return {
1041
+ position: lerpVec3(fromPose.position, toPose.position, crossfade.alpha),
1042
+ rotation: slerpQuaternion(fromPose.rotation, toPose.rotation, crossfade.alpha),
1043
+ }
1044
+ }
1045
+ }
1046
+ if (maskedActive.length === 1) return clipPoseForBone(clips, maskedActive[0]!.directive, maskedActive[0]!.sourceTimeUs, bone, boneToRest)
1047
+ let pose = clipPoseForBone(clips, maskedActive[0]!.directive, maskedActive[0]!.sourceTimeUs, bone, boneToRest)
1048
+ for (let i = 1; i < maskedActive.length; i += 1) {
1049
+ const other = clipPoseForBone(clips, maskedActive[i]!.directive, maskedActive[i]!.sourceTimeUs, bone, boneToRest)
1050
+ const t = 1 / (i + 1)
1051
+ pose = {
1052
+ position: lerpVec3(pose.position, other.position, t),
1053
+ rotation: slerpQuaternion(pose.rotation, other.rotation, t),
1054
+ }
1055
+ }
1056
+ return pose
1057
+ }
1058
+
1059
+ function additiveBoneDelta(
1060
+ clips: SpatialPerformanceSources["clips"],
1061
+ directives: SpatialPerformancePlan["directives"],
1062
+ timeUs: number,
1063
+ bone: string,
1064
+ boneToRest: ReadonlyMap<string, { position: Vec3; rotation: Quaternion }>,
1065
+ ): { position: Vec3; rotation: Quaternion } {
1066
+ const active = collectActiveClips(timeUs, clips, directives, "additive")
1067
+ const maskedActive = active.filter((entry) => resolveBodyMask(entry.directive.mask).has(bone))
1068
+ let position: Vec3 = [0, 0, 0] as unknown as Vec3
1069
+ let rotation: Quaternion = [0, 0, 0, 1] as unknown as Quaternion
1070
+ for (const entry of maskedActive) {
1071
+ const pose = clipPoseForBone(clips, entry.directive, entry.sourceTimeUs, bone, boneToRest)
1072
+ position = vec3Add(position, pose.position)
1073
+ rotation = multiplyQuaternion(rotation, pose.rotation)
1074
+ }
1075
+ return { position, rotation }
1076
+ }
1077
+
1078
+ function rootWaypointPosition(waypoints: readonly { timeUs: number; position: Vec3; rotation: Quaternion }[], timeUs: number): { position: Vec3; rotation: Quaternion } {
1079
+ if (timeUs <= waypoints[0]!.timeUs) return { position: waypoints[0]!.position, rotation: waypoints[0]!.rotation }
1080
+ const last = waypoints[waypoints.length - 1]!
1081
+ if (timeUs >= last.timeUs) return { position: last.position, rotation: last.rotation }
1082
+ for (let i = 1; i < waypoints.length; i += 1) {
1083
+ const prev = waypoints[i - 1]!
1084
+ const next = waypoints[i]!
1085
+ if (timeUs >= prev.timeUs && timeUs <= next.timeUs) {
1086
+ const t = (timeUs - prev.timeUs) / (next.timeUs - prev.timeUs)
1087
+ return {
1088
+ position: lerpVec3(prev.position, next.position, t),
1089
+ rotation: slerpQuaternion(prev.rotation, next.rotation, t),
1090
+ }
1091
+ }
1092
+ }
1093
+ return { position: last.position, rotation: last.rotation }
1094
+ }
1095
+
1096
+ function buildBoneToRest(mapping: SpatialHumanoidMapping): ReadonlyMap<string, { position: Vec3; rotation: Quaternion }> {
1097
+ const map = new Map<string, { position: Vec3; rotation: Quaternion }>()
1098
+ for (const bone of mapping.bones) {
1099
+ map.set(bone.canonicalName, { position: bone.restOffset.position, rotation: bone.restOffset.rotation })
1100
+ }
1101
+ return map
1102
+ }
1103
+
1104
+ export function compileSpatialPerformance(planInput: unknown, sourcesInput: unknown): SpatialPerformanceTake {
1105
+ const plan = parseSpatialPerformancePlan(planInput)
1106
+ const sources = parseSpatialPerformanceSources(sourcesInput)
1107
+
1108
+ if (sources.sceneSha256 !== plan.sceneSha256) {
1109
+ throw new SpatialSceneError("invalid-data", "Stale or mismatched scene digest.", "performance.scene")
1110
+ }
1111
+ if (sources.rigSha256 !== plan.rigSha256) {
1112
+ throw new SpatialSceneError("invalid-data", "Stale or mismatched rig digest.", "performance.rig")
1113
+ }
1114
+ const mappingSha = canonicalJsonSha256(sources.mapping)
1115
+ if (mappingSha !== plan.mappingSha256) {
1116
+ throw new SpatialSceneError("invalid-data", "Stale or mismatched mapping digest.", "performance.mapping")
1117
+ }
1118
+
1119
+ validatePerformancePlan(plan, sources)
1120
+
1121
+ for (const directive of plan.directives) {
1122
+ if ("bone" in directive && directive.bone !== undefined) {
1123
+ assertBoneExists(sources.mapping, directive.bone, `performance.directive.${directive.directiveId}`)
1124
+ }
1125
+ if ("endBone" in directive && directive.endBone !== undefined) {
1126
+ assertBoneExists(sources.mapping, directive.endBone, `performance.directive.${directive.directiveId}`)
1127
+ }
1128
+ if (directive.kind === "clip") {
1129
+ const clip = sources.clips[directive.clipDigest]
1130
+ if (clip === undefined) {
1131
+ throw new SpatialSceneError("not-found", `Clip ${directive.clipDigest} is missing.`, `performance.directive.${directive.directiveId}`)
1132
+ }
1133
+ const clipSha = canonicalJsonSha256(clip)
1134
+ if (clipSha !== directive.clipDigest) {
1135
+ throw new SpatialSceneError("invalid-data", `Clip digest mismatch for ${directive.clipDigest}.`, `performance.directive.${directive.directiveId}`)
1136
+ }
1137
+ }
1138
+ if ("propId" in directive && directive.propId !== undefined) {
1139
+ assertPropExists(sources, directive.propId, `performance.directive.${directive.directiveId}`)
1140
+ }
1141
+ }
1142
+
1143
+ const frameRate: SpatialFrameRate = { numerator: plan.frameRate.numerator, denominator: plan.frameRate.denominator }
1144
+ const sampleCount = spatialFrameCount(plan.durationUs, frameRate)
1145
+ if (sampleCount > SPATIAL_PERFORMANCE_LIMITS.samples) {
1146
+ throw new SpatialSceneError("invalid-data", `Sample count ${sampleCount} exceeds the performance limit.`, "performance.samples")
1147
+ }
1148
+
1149
+ const boneToRest = buildBoneToRest(sources.mapping)
1150
+ const order = mappedBoneOrder(sources.mapping)
1151
+
1152
+ const morphDirectives = plan.directives.filter((d) => d.kind === "morph") as Extract<SpatialPerformancePlan["directives"][number], { kind: "morph" }>[]
1153
+ const attachmentDirectives = plan.directives.filter((d) => d.kind === "attach" || d.kind === "release")
1154
+ .sort((a, b) => a.startUs - b.startUs || Number(a.kind === "attach") - Number(b.kind === "attach"))
1155
+ const springDirectives = plan.directives.filter((d) => d.kind === "spring") as Extract<SpatialPerformancePlan["directives"][number], { kind: "spring" }>[]
1156
+ const lookAtDirectives = plan.directives.filter((d) => d.kind === "look-at") as Extract<SpatialPerformancePlan["directives"][number], { kind: "look-at" }>[]
1157
+ const twoBoneDirectives = plan.directives.filter((d) => d.kind === "two-bone-ik") as Extract<SpatialPerformancePlan["directives"][number], { kind: "two-bone-ik" }>[]
1158
+ const footPlantDirectives = plan.directives.filter((d) => d.kind === "foot-plant") as Extract<SpatialPerformancePlan["directives"][number], { kind: "foot-plant" }>[]
1159
+ const rootTrajectory = plan.directives.find((d) => d.kind === "root-trajectory") as Extract<SpatialPerformancePlan["directives"][number], { kind: "root-trajectory" }> | undefined
1160
+
1161
+ const propIds = new Set<string>([...Object.keys(sources.props), ...attachmentDirectives.map((d) => d.propId)])
1162
+
1163
+ type MutableSample = {
1164
+ timeUs: number
1165
+ boneWorld: Record<string, { position: [number, number, number]; rotation: [number, number, number, number] }>
1166
+ attachments: Record<string, { attached: boolean; parentBone?: string; localOffset?: { position: [number, number, number]; rotation: [number, number, number, number]; scale: [number, number, number] }; worldPosition: [number, number, number] }>
1167
+ }
1168
+
1169
+ const samples: MutableSample[] = []
1170
+ const boneKeys: Record<string, { positions: [number, number, number][]; rotations: [number, number, number, number][] }> = {}
1171
+ for (const bone of order) {
1172
+ boneKeys[bone] = { positions: [], rotations: [] }
1173
+ }
1174
+ const morphKeys: Record<number, { timeUs: number; value: number }[]> = {}
1175
+ const attachmentKeys: Record<string, { timeUs: number; attached: boolean; parentBone: string; localOffset: { position: [number, number, number]; rotation: [number, number, number, number]; scale: [number, number, number] } }[]> = {}
1176
+ for (const propId of propIds) {
1177
+ attachmentKeys[propId] = []
1178
+ }
1179
+
1180
+ for (let i = 0; i < sampleCount; i += 1) {
1181
+ const sample = spatialFrameSample(i, plan.durationUs, frameRate)
1182
+ const timeUs = sample.timeUs
1183
+
1184
+ let rootPosition: Vec3 = [0, 0, 0] as unknown as Vec3
1185
+ let rootRotation: Quaternion = [0, 0, 0, 1] as unknown as Quaternion
1186
+ if (rootTrajectory !== undefined) {
1187
+ const waypoint = rootWaypointPosition(rootTrajectory.waypoints, timeUs)
1188
+ rootPosition = waypoint.position
1189
+ rootRotation = waypoint.rotation
1190
+ }
1191
+
1192
+ const localPoses = new Map<string, { position: Vec3; rotation: Quaternion }>()
1193
+ const boneRecord: Record<string, { position: [number, number, number]; rotation: [number, number, number, number] }> = {}
1194
+
1195
+ // Pass 1: base local poses from override clips, respecting canonical body masks.
1196
+ for (const bone of order) {
1197
+ localPoses.set(bone, baseBonePose(sources.clips, plan.directives, timeUs, bone, boneToRest))
1198
+ }
1199
+
1200
+ // Pass 2: forward-kinematics the base pose so lookAt/IK/footPlant see consistent worlds.
1201
+ const baseWorld = new Map<string, { position: Vec3; rotation: Quaternion }>()
1202
+ for (const bone of order) {
1203
+ const parentName = BONE_PARENTS[bone]!
1204
+ const parent = parentName === null ? { position: rootPosition, rotation: rootRotation } : baseWorld.get(parentName)
1205
+ if (parent === undefined) throw new SpatialSceneError("invalid-data", `Missing parent for bone ${bone}.`, "performance.fk")
1206
+ const local = localPoses.get(bone)!
1207
+ baseWorld.set(bone, worldFromLocal(parent.position, parent.rotation, local.position, local.rotation))
1208
+ }
1209
+
1210
+ // Pass 3: apply deterministic additive clip layers in directive order.
1211
+ for (const bone of order) {
1212
+ const delta = additiveBoneDelta(sources.clips, plan.directives, timeUs, bone, boneToRest)
1213
+ const base = localPoses.get(bone)!
1214
+ localPoses.set(bone, { position: vec3Add(base.position, delta.position), rotation: multiplyQuaternion(base.rotation, delta.rotation) })
1215
+ }
1216
+
1217
+ // Pass 4: apply deterministic spring offsets.
1218
+ for (const spring of springDirectives) {
1219
+ const offset = springOffset(timeUs, plan.seed + spring.seed, spring.amplitude, spring.frequency)
1220
+ const base = localPoses.get(spring.bone)!
1221
+ localPoses.set(spring.bone, { position: vec3Add(base.position, offset.position), rotation: multiplyQuaternion(base.rotation, offset.rotation) })
1222
+ }
1223
+
1224
+ // Pass 5: forward-kinematics the working pose for constraint solvers.
1225
+ const workingWorld = new Map<string, { position: Vec3; rotation: Quaternion }>()
1226
+ for (const bone of order) {
1227
+ const parentName = BONE_PARENTS[bone]!
1228
+ const parent = parentName === null ? { position: rootPosition, rotation: rootRotation } : workingWorld.get(parentName)
1229
+ if (parent === undefined) throw new SpatialSceneError("invalid-data", `Missing parent for bone ${bone}.`, "performance.fk")
1230
+ const local = localPoses.get(bone)!
1231
+ workingWorld.set(bone, worldFromLocal(parent.position, parent.rotation, local.position, local.rotation))
1232
+ }
1233
+
1234
+ // Pass 6: solve lookAt, bounded analytic two-bone IK, and footPlant into local poses.
1235
+ for (const lookAt of lookAtDirectives) {
1236
+ const world = workingWorld.get(lookAt.bone)
1237
+ const parentName = BONE_PARENTS[lookAt.bone]!
1238
+ const parent = parentName === null ? { position: rootPosition, rotation: rootRotation } : workingWorld.get(parentName)
1239
+ if (world === undefined || parent === undefined) continue
1240
+ const parentConj = conjugateQuaternion(parent.rotation)
1241
+ const targetWorldDir = vec3Normalize(vec3Sub(lookAt.target, world.position))
1242
+ const targetParentDir = rotateVectorByQuaternion(parentConj, targetWorldDir)
1243
+ const lookLocal = quaternionFromVectors([0, 0, 1], targetParentDir)
1244
+ const local = localPoses.get(lookAt.bone)!
1245
+ localPoses.set(lookAt.bone, { position: local.position, rotation: lookLocal })
1246
+ }
1247
+
1248
+ for (const ik of twoBoneDirectives) {
1249
+ const endBone = ik.endBone
1250
+ const lowerBone = BONE_PARENTS[endBone]!
1251
+ const upperBone = BONE_PARENTS[lowerBone]!
1252
+ const baseWorld = workingWorld.get(upperBone)
1253
+ const upperLocal = localPoses.get(upperBone)
1254
+ const lowerLocal = localPoses.get(lowerBone)
1255
+ const endLocal = localPoses.get(endBone)
1256
+ if (baseWorld === undefined || upperLocal === undefined || lowerLocal === undefined || endLocal === undefined) continue
1257
+ const solved = solveTwoBoneIk(baseWorld, upperLocal, lowerLocal, endLocal, ik.target, ik.pole, ik.policy)
1258
+ if (solved !== null) {
1259
+ localPoses.set(upperBone, { position: upperLocal.position, rotation: solved.upperRotation })
1260
+ localPoses.set(lowerBone, { position: lowerLocal.position, rotation: solved.lowerRotation })
1261
+ }
1262
+ }
1263
+
1264
+ for (const plant of footPlantDirectives) {
1265
+ const world = workingWorld.get(plant.bone)
1266
+ const parentName = BONE_PARENTS[plant.bone]!
1267
+ const parent = parentName === null ? { position: rootPosition, rotation: rootRotation } : workingWorld.get(parentName)
1268
+ if (world === undefined || parent === undefined) continue
1269
+ const parentConj = conjugateQuaternion(parent.rotation)
1270
+ const plantedWorld = { position: [world.position[0], plant.groundY, world.position[2]] as unknown as Vec3, rotation: [0, 0, 0, 1] as unknown as Quaternion }
1271
+ const targetParent = vec3Sub(plantedWorld.position, parent.position)
1272
+ const plantLocalPos = rotateVectorByQuaternion(parentConj, targetParent)
1273
+ const plantLocal = { position: plantLocalPos, rotation: multiplyQuaternion(parentConj, plantedWorld.rotation) }
1274
+ localPoses.set(plant.bone, plantLocal)
1275
+ }
1276
+
1277
+ // Pass 7: final forward-kinematics pass to record world positions.
1278
+ const boneWorld = new Map<string, { position: Vec3; rotation: Quaternion }>()
1279
+ for (const bone of order) {
1280
+ const parentName = BONE_PARENTS[bone]!
1281
+ const parent = parentName === null ? { position: rootPosition, rotation: rootRotation } : boneWorld.get(parentName)
1282
+ if (parent === undefined) throw new SpatialSceneError("invalid-data", `Missing parent for bone ${bone}.`, "performance.fk")
1283
+ const local = localPoses.get(bone)!
1284
+ const world = worldFromLocal(parent.position, parent.rotation, local.position, local.rotation)
1285
+ boneWorld.set(bone, world)
1286
+ boneKeys[bone]!.positions.push([world.position[0], world.position[1], world.position[2]])
1287
+ boneKeys[bone]!.rotations.push([world.rotation[0], world.rotation[1], world.rotation[2], world.rotation[3]])
1288
+ boneRecord[bone] = {
1289
+ position: [world.position[0], world.position[1], world.position[2]],
1290
+ rotation: [world.rotation[0], world.rotation[1], world.rotation[2], world.rotation[3]],
1291
+ }
1292
+ }
1293
+
1294
+ const morphValues: Record<number, number> = {}
1295
+ for (const directive of morphDirectives) {
1296
+ morphValues[directive.index] = directive.weight
1297
+ }
1298
+ for (const [index, weight] of Object.entries(morphValues)) {
1299
+ const idx = Number(index)
1300
+ const channel = morphKeys[idx] ?? []
1301
+ channel.push({ timeUs, value: weight })
1302
+ morphKeys[idx] = channel
1303
+ }
1304
+
1305
+ const attachmentRecord: MutableSample["attachments"] = {}
1306
+ for (const propId of propIds) {
1307
+ let attached = false
1308
+ let parentBone: string | undefined
1309
+ let localOffset: { position: [number, number, number]; rotation: [number, number, number, number]; scale: [number, number, number] } | undefined
1310
+ // Ownership follows the latest event, including expired attaches so an
1311
+ // older interval cannot resume. At one instant, release closes the old
1312
+ // binding before attach opens the new one. Stable sorting preserves
1313
+ // authored precedence among simultaneous attaches.
1314
+ let latest: (typeof attachmentDirectives)[number] | undefined
1315
+ for (const directive of attachmentDirectives) {
1316
+ if (directive.startUs > timeUs) break
1317
+ if (directive.propId === propId) latest = directive
1318
+ }
1319
+ if (latest?.kind === "attach" && timeUs < latest.endUs) {
1320
+ attached = true
1321
+ parentBone = latest.bone
1322
+ localOffset = {
1323
+ position: [latest.localOffset.position[0], latest.localOffset.position[1], latest.localOffset.position[2]],
1324
+ rotation: [latest.localOffset.rotation[0], latest.localOffset.rotation[1], latest.localOffset.rotation[2], latest.localOffset.rotation[3]],
1325
+ scale: [latest.localOffset.scale[0], latest.localOffset.scale[1], latest.localOffset.scale[2]],
1326
+ }
1327
+ }
1328
+ let worldPosition: [number, number, number] = [0, 0, 0]
1329
+ if (attached && parentBone !== undefined && localOffset !== undefined) {
1330
+ const parentWorld = boneWorld.get(parentBone)
1331
+ if (parentWorld !== undefined) {
1332
+ const offsetMatrix = multiplyTransforms(
1333
+ composeTransform({ position: parentWorld.position, rotation: parentWorld.rotation, scale: [1, 1, 1] as unknown as Vec3 }),
1334
+ composeTransform({ position: localOffset.position as unknown as Vec3, rotation: localOffset.rotation as unknown as Quaternion, scale: localOffset.scale as unknown as Vec3 }),
1335
+ )
1336
+ const offsetPose = poseFromMatrix(offsetMatrix)
1337
+ worldPosition = [offsetPose.position[0], offsetPose.position[1], offsetPose.position[2]]
1338
+ }
1339
+ } else {
1340
+ const prop = sources.props[propId]
1341
+ if (prop !== undefined) {
1342
+ worldPosition = [prop.localOffset.position[0], prop.localOffset.position[1], prop.localOffset.position[2]]
1343
+ }
1344
+ }
1345
+ attachmentRecord[propId] = { attached, worldPosition }
1346
+ if (attached && parentBone !== undefined && localOffset !== undefined) {
1347
+ attachmentRecord[propId]!.parentBone = parentBone
1348
+ attachmentRecord[propId]!.localOffset = localOffset
1349
+ }
1350
+ attachmentKeys[propId]!.push({ timeUs, attached, parentBone: parentBone ?? "hips", localOffset: localOffset ?? { position: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] } })
1351
+ }
1352
+
1353
+ samples.push({ timeUs, boneWorld: boneRecord, attachments: attachmentRecord })
1354
+ }
1355
+
1356
+ const channels: { kind: string }[] = []
1357
+ for (const bone of order) {
1358
+ const data = boneKeys[bone]!
1359
+ const keys: { timeUs: number; position: [number, number, number]; rotation: [number, number, number, number] }[] = []
1360
+ for (let i = 0; i < sampleCount; i += 1) {
1361
+ const sample = spatialFrameSample(i, plan.durationUs, frameRate)
1362
+ keys.push({ timeUs: sample.timeUs, position: data.positions[i]!, rotation: data.rotations[i]! })
1363
+ }
1364
+ channels.push({ kind: "bone-pose", bone, keys } as unknown as { kind: string })
1365
+ }
1366
+ for (const [index, values] of Object.entries(morphKeys)) {
1367
+ const idx = Number(index)
1368
+ channels.push({ kind: "morph", index: idx, keys: values } as unknown as { kind: string })
1369
+ }
1370
+ for (const [propId, data] of Object.entries(attachmentKeys)) {
1371
+ channels.push({ kind: "attachment", propId, keys: data } as unknown as { kind: string })
1372
+ }
1373
+
1374
+ const planSha = canonicalJsonSha256(plan)
1375
+ const directivesSha = canonicalJsonSha256(plan.directives)
1376
+ const compilerSha = canonicalJsonSha256(SPATIAL_PERFORMANCE_COMPILER_ID)
1377
+ const outputSha = canonicalJsonSha256({ planSha256: planSha, durationUs: plan.durationUs, samples, channels })
1378
+
1379
+ const receipt = {
1380
+ kind: "slopcamera.spatial-performance-receipt",
1381
+ schemaVersion: 1,
1382
+ compiler: SPATIAL_PERFORMANCE_COMPILER_ID,
1383
+ compilerSha256: compilerSha,
1384
+ sceneSha256: plan.sceneSha256,
1385
+ rigSha256: plan.rigSha256,
1386
+ mappingSha256: plan.mappingSha256,
1387
+ directivesSha256: directivesSha,
1388
+ seed: plan.seed,
1389
+ outputSha256: outputSha,
1390
+ durationUs: plan.durationUs,
1391
+ sampleCount,
1392
+ }
1393
+
1394
+ const take = {
1395
+ kind: "slopcamera.spatial-performance-take",
1396
+ schemaVersion: 1,
1397
+ planSha256: planSha,
1398
+ durationUs: plan.durationUs,
1399
+ samples,
1400
+ channels,
1401
+ receipt,
1402
+ }
1403
+
1404
+ return deepFreezeJson(take) as unknown as SpatialPerformanceTake
1405
+ }
1406
+
1407
+ export function auditSpatialPerformance(take: SpatialPerformanceTake, optionsInput: unknown): SpatialPerformanceAuditReport {
1408
+ const options = parseSpatialPerformanceAuditOptions(optionsInput)
1409
+ const takeSha = canonicalJsonSha256(take)
1410
+ const findings: SpatialPerformanceFinding[] = []
1411
+
1412
+ const getHeadForward = (headPose: { position: Vec3; rotation: Quaternion }): Vec3 => {
1413
+ return rotateVectorByQuaternion(headPose.rotation, [0, 0, 1])
1414
+ }
1415
+
1416
+ const typedBoneWorld = (sample: (typeof take.samples)[number]) => {
1417
+ return sample.boneWorld as Record<string, { position: Vec3; rotation: Quaternion }>
1418
+ }
1419
+ const typedAttachments = (sample: (typeof take.samples)[number]) => {
1420
+ return sample.attachments as Record<string, { attached: boolean; parentBone?: string; worldPosition: Vec3 }>
1421
+ }
1422
+
1423
+ for (let i = 0; i < take.samples.length; i += 1) {
1424
+ const sample = take.samples[i]!
1425
+ const timeUs = sample.timeUs
1426
+ const bones = typedBoneWorld(sample)
1427
+
1428
+ for (const [bone, pose] of Object.entries(bones)) {
1429
+ if (options.groundY !== undefined && (bone === "leftFoot" || bone === "rightFoot")) {
1430
+ if (pose.position[1] < options.groundY - 0.01) {
1431
+ findings.push({
1432
+ kind: "ground-penetration",
1433
+ severity: "warning",
1434
+ entityId: options.characterId,
1435
+ bone: bone as unknown as z.infer<typeof humanoidBoneName>,
1436
+ timeUs,
1437
+ detail: `Foot ${bone} penetrates ground at y=${pose.position[1].toFixed(4)} (groundY=${options.groundY}).`,
1438
+ })
1439
+ }
1440
+ if (i > 0) {
1441
+ const prev = take.samples[i - 1]!
1442
+ const prevBones = typedBoneWorld(prev)
1443
+ const prevPose = prevBones[bone]
1444
+ if (prevPose !== undefined && Math.abs(prevPose.position[1] - options.groundY) < 0.02 && Math.abs(pose.position[1] - options.groundY) < 0.02) {
1445
+ const dx = pose.position[0] - prevPose.position[0]
1446
+ const dz = pose.position[2] - prevPose.position[2]
1447
+ const horizontal = Math.hypot(dx, dz)
1448
+ if (horizontal > 0.01) {
1449
+ findings.push({
1450
+ kind: "foot-slide",
1451
+ severity: "warning",
1452
+ entityId: options.characterId,
1453
+ bone: bone as unknown as z.infer<typeof humanoidBoneName>,
1454
+ timeUs,
1455
+ detail: `Planted foot ${bone} slid ${horizontal.toFixed(4)} meters.`,
1456
+ })
1457
+ }
1458
+ }
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ if (options.gazeTarget !== undefined) {
1464
+ const head = bones["head"]
1465
+ if (head !== undefined) {
1466
+ const forward = getHeadForward(head)
1467
+ const toTarget = vec3Normalize(vec3Sub(options.gazeTarget, head.position))
1468
+ const dot = Math.max(-1, Math.min(1, vec3Dot(forward, toTarget)))
1469
+ const angle = Math.acos(dot)
1470
+ if (angle > 0.1) {
1471
+ findings.push({
1472
+ kind: "gaze-error",
1473
+ severity: "info",
1474
+ entityId: options.characterId,
1475
+ bone: "head",
1476
+ timeUs,
1477
+ detail: `Head is ${angle.toFixed(4)} radians off gaze target.`,
1478
+ })
1479
+ }
1480
+ }
1481
+ }
1482
+
1483
+ const attachments = typedAttachments(sample)
1484
+ for (const [propId, attachment] of Object.entries(attachments)) {
1485
+ if (attachment.attached && attachment.parentBone !== undefined) {
1486
+ const hand = bones[attachment.parentBone]
1487
+ if (hand !== undefined) {
1488
+ const drift = vec3Length(vec3Sub(attachment.worldPosition, hand.position))
1489
+ if (drift > 0.01) {
1490
+ findings.push({
1491
+ kind: "attachment-drift",
1492
+ severity: "warning",
1493
+ entityId: options.characterId,
1494
+ bone: attachment.parentBone as unknown as z.infer<typeof humanoidBoneName>,
1495
+ timeUs,
1496
+ detail: `Prop ${propId} drifts ${drift.toFixed(4)} meters from ${attachment.parentBone}.`,
1497
+ })
1498
+ }
1499
+ }
1500
+ }
1501
+ }
1502
+
1503
+ const upperLegs = ["leftUpperLeg", "rightUpperLeg"] as const
1504
+ const lowerLegs = ["leftLowerLeg", "rightLowerLeg"] as const
1505
+ const feet = ["leftFoot", "rightFoot"] as const
1506
+ for (let side = 0; side < 2; side += 1) {
1507
+ const upper = bones[upperLegs[side]!]
1508
+ const lower = bones[lowerLegs[side]!]
1509
+ const foot = bones[feet[side]!]
1510
+ if (upper !== undefined && lower !== undefined && foot !== undefined) {
1511
+ const thigh = vec3Normalize(vec3Sub(lower.position, upper.position))
1512
+ const calf = vec3Normalize(vec3Sub(foot.position, lower.position))
1513
+ const dot = vec3Dot(thigh, calf)
1514
+ if (dot > 0.95) {
1515
+ findings.push({
1516
+ kind: "joint-limit",
1517
+ severity: "warning",
1518
+ entityId: options.characterId,
1519
+ bone: lowerLegs[side]! as unknown as z.infer<typeof humanoidBoneName>,
1520
+ timeUs,
1521
+ detail: `Knee ${lowerLegs[side]!} overextended (dot=${dot.toFixed(4)}).`,
1522
+ })
1523
+ }
1524
+ }
1525
+ const upperArm = bones[side === 0 ? "leftUpperArm" : "rightUpperArm"]
1526
+ const lowerArm = bones[side === 0 ? "leftLowerArm" : "rightLowerArm"]
1527
+ const hand = bones[side === 0 ? "leftHand" : "rightHand"]
1528
+ if (upperArm !== undefined && lowerArm !== undefined && hand !== undefined) {
1529
+ const upper = vec3Normalize(vec3Sub(lowerArm.position, upperArm.position))
1530
+ const lower = vec3Normalize(vec3Sub(hand.position, lowerArm.position))
1531
+ const dot = vec3Dot(upper, lower)
1532
+ if (dot > 0.95) {
1533
+ findings.push({
1534
+ kind: "joint-limit",
1535
+ severity: "warning",
1536
+ entityId: options.characterId,
1537
+ bone: (side === 0 ? "leftLowerArm" : "rightLowerArm") as unknown as z.infer<typeof humanoidBoneName>,
1538
+ timeUs,
1539
+ detail: `Elbow ${side === 0 ? "leftLowerArm" : "rightLowerArm"} overextended (dot=${dot.toFixed(4)}).`,
1540
+ })
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ if (i > 0 && options.clipBoundaries !== undefined && options.clipBoundaries.includes(timeUs)) {
1546
+ const prev = take.samples[i - 1]!
1547
+ const prevBones = typedBoneWorld(prev)
1548
+ let jump = 0
1549
+ for (const [bone, pose] of Object.entries(bones)) {
1550
+ const prevPose = prevBones[bone]
1551
+ if (prevPose === undefined) continue
1552
+ const p = vec3Length(vec3Sub(pose.position, prevPose.position))
1553
+ if (p > jump) jump = p
1554
+ }
1555
+ if (jump > 0.1) {
1556
+ findings.push({
1557
+ kind: "clip-discontinuity",
1558
+ severity: "warning",
1559
+ entityId: options.characterId,
1560
+ timeUs,
1561
+ detail: `Transform jump of ${jump.toFixed(4)} meters across clip boundary.`,
1562
+ })
1563
+ }
1564
+ }
1565
+
1566
+ const hips = bones["hips"]
1567
+ if (hips !== undefined) {
1568
+ if (options.cameras !== undefined) {
1569
+ for (const camera of options.cameras) {
1570
+ const distance = vec3Length(vec3Sub(camera.position, hips.position))
1571
+ if (distance < camera.near) {
1572
+ findings.push({
1573
+ kind: "character-camera-collision",
1574
+ severity: "warning",
1575
+ entityId: options.characterId,
1576
+ timeUs,
1577
+ detail: `Character is ${distance.toFixed(4)} meters inside camera ${camera.cameraId} near plane.`,
1578
+ })
1579
+ }
1580
+ }
1581
+ }
1582
+ if (options.otherCharacters !== undefined) {
1583
+ for (const other of options.otherCharacters) {
1584
+ const distance = vec3Length(vec3Sub(other.position, hips.position))
1585
+ if (distance < other.radius) {
1586
+ findings.push({
1587
+ kind: "character-character-collision",
1588
+ severity: "warning",
1589
+ entityId: options.characterId,
1590
+ timeUs,
1591
+ detail: `Character collides with ${other.characterId} (distance=${distance.toFixed(4)}).`,
1592
+ })
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+ }
1598
+
1599
+ const sorted = sortSpatialBy(findings, (f) => {
1600
+ const key = `${String(f.timeUs).padStart(16, "0")}|${f.bone ?? ""}|${f.kind}|${f.severity}|${f.entityId ?? ""}|${f.detail}`
1601
+ return key
1602
+ })
1603
+ const omitted = Math.max(0, sorted.length - SPATIAL_PERFORMANCE_LIMITS.findings)
1604
+ const trimmed = sorted.slice(0, SPATIAL_PERFORMANCE_LIMITS.findings)
1605
+
1606
+ return deepFreezeJson({
1607
+ kind: "slopcamera.spatial-performance-audit",
1608
+ schemaVersion: 1,
1609
+ takeSha256: takeSha,
1610
+ findings: trimmed,
1611
+ omittedFindings: omitted,
1612
+ }) as unknown as SpatialPerformanceAuditReport
1613
+ }