@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,206 @@
1
+ import { z } from "zod"
2
+
3
+ import { canonicalJson } from "../code/canonical-json.js"
4
+ import { deepFreezeJson } from "../code/json-snapshot.js"
5
+ import {
6
+ SpatialDigestSchema,
7
+ SpatialEntityIdSchema,
8
+ SpatialQuaternionSchema,
9
+ SpatialTimeUsSchema,
10
+ SpatialVec3Schema,
11
+ } from "./contracts.js"
12
+ import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
13
+ import {
14
+ SPATIAL_SIMULATION_LIMITS,
15
+ createSpatialSimulationBakeReceipt,
16
+ parseSpatialSimulationPlan,
17
+ spatialSimulationPlanSha256,
18
+ type SpatialSimulationBakeReceipt,
19
+ } from "./simulation.js"
20
+
21
+ /**
22
+ * Deterministic fixed-step reference bake for `slopcamera-rigid-body-reference-v1`.
23
+ * Semi-implicit Euler with analytic forces only — gravity, declared springs,
24
+ * and fixed-constraint projection — plus free rigid rotation from the initial
25
+ * angular velocity. The integrator is deliberately narrow: undeclared world
26
+ * colliders, contact, and hinge constraints are outside this qualification and
27
+ * reject rather than approximate. All arithmetic is IEEE-754 binary64 in
28
+ * declared plan order, so identical plans produce identical bytes everywhere.
29
+ * The native secondary-motion profile stays fail-closed here.
30
+ */
31
+
32
+ export const SPATIAL_SIMULATION_BAKE_LIMITS = Object.freeze({
33
+ channels: SPATIAL_SIMULATION_LIMITS.bodies,
34
+ keysPerChannel: SPATIAL_SIMULATION_LIMITS.steps,
35
+ })
36
+
37
+ const transformKeySchema = z.strictObject({
38
+ orientation: SpatialQuaternionSchema,
39
+ position: SpatialVec3Schema,
40
+ timeUs: SpatialTimeUsSchema,
41
+ })
42
+
43
+ export const SpatialSimulationBakeChannelSchema = z.strictObject({
44
+ entityId: SpatialEntityIdSchema,
45
+ keys: z.array(transformKeySchema).min(1).max(SPATIAL_SIMULATION_BAKE_LIMITS.keysPerChannel),
46
+ kind: z.literal("transform"),
47
+ })
48
+
49
+ export const SpatialSimulationBakeDocumentSchema = z.strictObject({
50
+ cacheId: z.string().min(1).max(128).regex(/^cache_[a-z0-9][a-z0-9_-]*$/u),
51
+ channels: z.array(SpatialSimulationBakeChannelSchema).min(1).max(SPATIAL_SIMULATION_BAKE_LIMITS.channels),
52
+ engineProfile: z.literal("slopcamera-rigid-body-reference-v1"),
53
+ kind: z.literal("slopcamera.spatial-simulation-bake"),
54
+ planSha256: SpatialDigestSchema,
55
+ schemaVersion: z.literal(1),
56
+ stepCount: z.number().int().min(1).max(SPATIAL_SIMULATION_LIMITS.steps),
57
+ timeStepUs: SpatialTimeUsSchema.min(1).max(1_000_000),
58
+ }).superRefine((document, context) => {
59
+ const entityIds = new Set<string>()
60
+ for (const [index, channel] of document.channels.entries()) {
61
+ if (entityIds.has(channel.entityId)) context.addIssue({ code: "custom", path: ["channels", index, "entityId"], message: `Duplicate bake channel for ${channel.entityId}.` })
62
+ entityIds.add(channel.entityId)
63
+ let previous = -1
64
+ for (const [keyIndex, key] of channel.keys.entries()) {
65
+ if (key.timeUs <= previous) context.addIssue({ code: "custom", path: ["channels", index, "keys", keyIndex, "timeUs"], message: "Bake keys must be strictly ordered by timeUs." })
66
+ previous = key.timeUs
67
+ }
68
+ if (channel.keys.length !== document.stepCount) {
69
+ context.addIssue({ code: "custom", path: ["channels", index, "keys"], message: `Bake channel must carry exactly ${document.stepCount} ordered keys.` })
70
+ }
71
+ }
72
+ })
73
+
74
+ export type SpatialSimulationBakeDocument = Readonly<z.infer<typeof SpatialSimulationBakeDocumentSchema>>
75
+
76
+ export function parseSpatialSimulationBakeDocument(input: unknown): SpatialSimulationBakeDocument {
77
+ return deepFreezeJson(parseSpatialValue(SpatialSimulationBakeDocumentSchema, input, "simulation bake"))
78
+ }
79
+
80
+ export function spatialSimulationBakeSha256(document: SpatialSimulationBakeDocument): string {
81
+ return spatialValueSha256(document)
82
+ }
83
+
84
+ const round = (value: number): number => (Object.is(value, -0) ? 0 : value)
85
+
86
+ export interface SpatialSimulationBakeResult {
87
+ readonly document: SpatialSimulationBakeDocument
88
+ readonly receipt: SpatialSimulationBakeReceipt
89
+ }
90
+
91
+ /**
92
+ * Executes one admitted plan and returns the canonical bake document plus its
93
+ * reconciled receipt. Only the reference profile executes; every other engine
94
+ * identity, including the native secondary-motion profile, rejects here even
95
+ * when the plan parses.
96
+ */
97
+ export function bakeSpatialSimulation(planInput: unknown): SpatialSimulationBakeResult {
98
+ const plan = parseSpatialSimulationPlan(planInput)
99
+ if (plan.engine.profile !== "slopcamera-rigid-body-reference-v1" || plan.simulationKind !== "rigid-body") {
100
+ throw new SpatialSceneError("invalid-data", `Simulation engine ${plan.engine.profile} is not qualified for portable deterministic baking.`, "simulation-bake")
101
+ }
102
+ for (const constraint of plan.constraints) {
103
+ if (constraint.kind === "hinge") {
104
+ throw new SpatialSceneError("invalid-data", `Hinge constraint ${constraint.constraintId} is not qualified in the reference integrator.`, "simulation-bake")
105
+ }
106
+ }
107
+ const dt = plan.timeStepUs / 1_000_000
108
+ interface BodyState { position: [number, number, number]; velocity: [number, number, number]; orientation: [number, number, number, number]; angular: [number, number, number] }
109
+ const states: BodyState[] = plan.bodies.map(body => ({
110
+ position: [...body.initialPosition],
111
+ velocity: [...body.initialVelocity],
112
+ orientation: [...body.initialOrientation],
113
+ angular: [...body.initialAngularVelocity],
114
+ }))
115
+ const indexByBodyId = new Map(plan.bodies.map((body, index) => [body.id, index]))
116
+ const springs = plan.constraints.filter((constraint): constraint is Extract<typeof constraint, { kind: "spring" }> => constraint.kind === "spring")
117
+ .map(constraint => {
118
+ const a = states[indexByBodyId.get(constraint.bodyA)!]!, b = states[indexByBodyId.get(constraint.bodyB)!]!
119
+ const dx = b.position[0] - a.position[0], dy = b.position[1] - a.position[1], dz = b.position[2] - a.position[2]
120
+ return { a: indexByBodyId.get(constraint.bodyA)!, b: indexByBodyId.get(constraint.bodyB)!, damping: constraint.damping, restLength: Math.sqrt(dx * dx + dy * dy + dz * dz), stiffness: constraint.stiffness }
121
+ })
122
+ const fixed = plan.constraints.filter((constraint): constraint is Extract<typeof constraint, { kind: "fixed" }> => constraint.kind === "fixed")
123
+ .map(constraint => {
124
+ const a = states[indexByBodyId.get(constraint.bodyA)!]!, b = states[indexByBodyId.get(constraint.bodyB)!]!
125
+ return { a: indexByBodyId.get(constraint.bodyA)!, b: indexByBodyId.get(constraint.bodyB)!,
126
+ offset: [b.position[0] - a.position[0], b.position[1] - a.position[1], b.position[2] - a.position[2]] as const }
127
+ })
128
+ const keys: { orientation: [number, number, number, number]; position: [number, number, number]; timeUs: number }[][] = plan.bodies.map(() => [])
129
+ for (let step = 0; step < plan.stepCount; step++) {
130
+ const forces = plan.bodies.map(body => [body.pinned || body.mass <= 0 ? 0 : plan.gravity[0] * body.mass,
131
+ body.pinned || body.mass <= 0 ? 0 : plan.gravity[1] * body.mass,
132
+ body.pinned || body.mass <= 0 ? 0 : plan.gravity[2] * body.mass] as [number, number, number])
133
+ for (const spring of springs) {
134
+ const a = states[spring.a]!, b = states[spring.b]!
135
+ const dx = b.position[0] - a.position[0], dy = b.position[1] - a.position[1], dz = b.position[2] - a.position[2]
136
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz)
137
+ const nx = distance > 0 ? dx / distance : 0, ny = distance > 0 ? dy / distance : 0, nz = distance > 0 ? dz / distance : 0
138
+ const stretch = distance - spring.restLength
139
+ const relative = (b.velocity[0] - a.velocity[0]) * nx + (b.velocity[1] - a.velocity[1]) * ny + (b.velocity[2] - a.velocity[2]) * nz
140
+ const magnitude = spring.stiffness * stretch + spring.damping * relative
141
+ const fa = forces[spring.a]!, fb = forces[spring.b]!
142
+ fa[0] += magnitude * nx; fa[1] += magnitude * ny; fa[2] += magnitude * nz
143
+ fb[0] -= magnitude * nx; fb[1] -= magnitude * ny; fb[2] -= magnitude * nz
144
+ }
145
+ for (const [index, body] of plan.bodies.entries()) {
146
+ if (body.pinned || body.mass <= 0) continue
147
+ const state = states[index]!, force = forces[index]!
148
+ state.velocity[0] += force[0] / body.mass * dt
149
+ state.velocity[1] += force[1] / body.mass * dt
150
+ state.velocity[2] += force[2] / body.mass * dt
151
+ state.position[0] += state.velocity[0] * dt
152
+ state.position[1] += state.velocity[1] * dt
153
+ state.position[2] += state.velocity[2] * dt
154
+ const [wx, wy, wz] = state.angular
155
+ if (wx !== 0 || wy !== 0 || wz !== 0) {
156
+ const [qx, qy, qz, qw] = state.orientation
157
+ const hx = 0.5 * dt * (wx * qw + wy * qz - wz * qy)
158
+ const hy = 0.5 * dt * (wy * qw + wz * qx - wx * qz)
159
+ const hz = 0.5 * dt * (wz * qw + wx * qy - wy * qx)
160
+ const hw = 0.5 * dt * (-wx * qx - wy * qy - wz * qz)
161
+ const nx = qx + hx, ny = qy + hy, nz = qz + hz, nw = qw + hw
162
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz + nw * nw)
163
+ state.orientation = [nx / length, ny / length, nz / length, nw / length]
164
+ }
165
+ }
166
+ for (const constraint of fixed) {
167
+ const a = states[constraint.a]!, b = states[constraint.b]!
168
+ b.position[0] = a.position[0] + constraint.offset[0]
169
+ b.position[1] = a.position[1] + constraint.offset[1]
170
+ b.position[2] = a.position[2] + constraint.offset[2]
171
+ b.velocity[0] = a.velocity[0]; b.velocity[1] = a.velocity[1]; b.velocity[2] = a.velocity[2]
172
+ }
173
+ const timeUs = step * plan.timeStepUs
174
+ for (const [index, state] of states.entries()) {
175
+ keys[index]!.push({ orientation: [round(state.orientation[0]), round(state.orientation[1]), round(state.orientation[2]), round(state.orientation[3])],
176
+ position: [round(state.position[0]), round(state.position[1]), round(state.position[2])], timeUs })
177
+ }
178
+ }
179
+ const document = parseSpatialSimulationBakeDocument({
180
+ cacheId: plan.cacheId,
181
+ channels: plan.bodies.map((body, index) => ({ entityId: body.entityId, keys: keys[index]!, kind: "transform" })),
182
+ engineProfile: "slopcamera-rigid-body-reference-v1",
183
+ kind: "slopcamera.spatial-simulation-bake",
184
+ planSha256: spatialSimulationPlanSha256(plan),
185
+ schemaVersion: 1,
186
+ stepCount: plan.stepCount,
187
+ timeStepUs: plan.timeStepUs,
188
+ })
189
+ const bytes = new TextEncoder().encode(`${canonicalJson(document)}\n`).byteLength
190
+ if (bytes > plan.maximumOutputBytes) {
191
+ throw new SpatialSceneError("invalid-data", `Simulation bake output (${bytes} bytes) exceeds the plan output-byte budget (${plan.maximumOutputBytes}).`, "simulation-bake")
192
+ }
193
+ const receipt = createSpatialSimulationBakeReceipt({
194
+ cacheId: plan.cacheId,
195
+ engine: plan.engine,
196
+ kind: "slopcamera.spatial-simulation-bake-receipt",
197
+ output: { animationSha256: spatialSimulationBakeSha256(document), bytes, channelCount: document.channels.length, keyCount: document.channels.reduce((sum, channel) => sum + channel.keys.length, 0) },
198
+ planSha256: spatialSimulationPlanSha256(plan),
199
+ schemaVersion: 1,
200
+ seed: plan.seed,
201
+ sourceDigest: plan.sourceDigest,
202
+ stepCount: plan.stepCount,
203
+ timeStepUs: plan.timeStepUs,
204
+ })
205
+ return { document, receipt }
206
+ }
@@ -0,0 +1,187 @@
1
+ import { z } from "zod"
2
+
3
+ import { canonicalJsonSha256 } from "../code/canonical-json.js"
4
+ import { deepFreezeJson } from "../code/json-snapshot.js"
5
+ import {
6
+ SpatialDigestSchema,
7
+ SpatialEntityIdSchema,
8
+ SpatialQuaternionSchema,
9
+ SpatialTimeUsSchema,
10
+ SpatialVec3Schema,
11
+ } from "./contracts.js"
12
+ import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
13
+
14
+ /**
15
+ * Deterministic fixed-step physics and secondary-motion bakes.
16
+ * No live mutable physics — these are cache-bound, authored plans that emit
17
+ * ordinary deterministic animation curves for renderer lowering and audit.
18
+ */
19
+
20
+ export const SPATIAL_SIMULATION_LIMITS = Object.freeze({
21
+ bodies: 256,
22
+ constraints: 256,
23
+ durationUs: 3_600_000_000,
24
+ outputBytes: 1_000_000_000,
25
+ steps: 100_000,
26
+ substeps: 128,
27
+ })
28
+
29
+ const positiveDimension = z.number().finite().positive().max(1_000_000)
30
+ const unit = z.number().finite().min(0).max(1)
31
+ const bodyId = z.string().min(1).max(64).regex(/^[a-z][a-z0-9_-]*$/u)
32
+ const constraintId = z.string().min(1).max(64).regex(/^[a-z][a-z0-9_-]*$/u)
33
+
34
+ export const SpatialSimulationEngineSchema = z.strictObject({
35
+ identitySha256: SpatialDigestSchema,
36
+ profile: z.enum(["slopcamera-rigid-body-reference-v1", "slopcamera-native-secondary-motion-v1"]),
37
+ })
38
+
39
+ export const SpatialRigidBodySchema = z.strictObject({
40
+ entityId: SpatialEntityIdSchema,
41
+ friction: unit,
42
+ id: bodyId,
43
+ initialAngularVelocity: SpatialVec3Schema,
44
+ initialOrientation: SpatialQuaternionSchema,
45
+ initialPosition: SpatialVec3Schema,
46
+ initialVelocity: SpatialVec3Schema,
47
+ mass: z.number().finite().min(0).max(1_000_000_000),
48
+ pinned: z.boolean().default(false),
49
+ restitution: unit,
50
+ shape: z.discriminatedUnion("kind", [
51
+ z.strictObject({ kind: z.literal("sphere"), radius: positiveDimension }),
52
+ z.strictObject({ kind: z.literal("box"), size: z.tuple([positiveDimension, positiveDimension, positiveDimension]) }),
53
+ z.strictObject({ height: positiveDimension, kind: z.literal("capsule"), radius: positiveDimension }),
54
+ ]),
55
+ }).superRefine((body, context) => {
56
+ if (!body.pinned && body.mass <= 0) {
57
+ context.addIssue({ code: "custom", path: ["mass"], message: "Unpinned rigid bodies require positive mass." })
58
+ }
59
+ })
60
+
61
+ const constraintBase = {
62
+ bodyA: bodyId,
63
+ bodyB: bodyId,
64
+ constraintId,
65
+ }
66
+
67
+ export const SpatialConstraintSchema = z.discriminatedUnion("kind", [
68
+ z.strictObject({ ...constraintBase, axis: SpatialVec3Schema, kind: z.literal("hinge"), limits: z.tuple([z.number().finite(), z.number().finite()]).optional() }),
69
+ z.strictObject({ ...constraintBase, damping: z.number().finite().min(0).max(1_000_000), kind: z.literal("spring"), stiffness: positiveDimension }),
70
+ z.strictObject({ ...constraintBase, kind: z.literal("fixed"), localA: SpatialVec3Schema, localB: SpatialVec3Schema }),
71
+ ])
72
+
73
+ export const SpatialSimulationPlanSchema = z.strictObject({
74
+ bodies: z.array(SpatialRigidBodySchema).min(1).max(SPATIAL_SIMULATION_LIMITS.bodies),
75
+ cacheId: z.string().min(1).max(128).regex(/^cache_[a-z0-9][a-z0-9_-]*$/u),
76
+ constraints: z.array(SpatialConstraintSchema).max(SPATIAL_SIMULATION_LIMITS.constraints),
77
+ engine: SpatialSimulationEngineSchema,
78
+ entityId: SpatialEntityIdSchema,
79
+ gravity: SpatialVec3Schema,
80
+ kind: z.literal("slopcamera.spatial-simulation-plan"),
81
+ maxSubsteps: z.number().int().min(1).max(SPATIAL_SIMULATION_LIMITS.substeps),
82
+ maximumOutputBytes: z.number().int().safe().positive().max(SPATIAL_SIMULATION_LIMITS.outputBytes).default(SPATIAL_SIMULATION_LIMITS.outputBytes),
83
+ schemaVersion: z.literal(1),
84
+ seed: z.number().int().min(0).max(2_147_483_647),
85
+ simulationKind: z.enum(["rigid-body", "secondary-motion"]).default("rigid-body"),
86
+ sourceDigest: SpatialDigestSchema,
87
+ stepCount: z.number().int().min(1).max(SPATIAL_SIMULATION_LIMITS.steps),
88
+ timeStepUs: SpatialTimeUsSchema.min(1).max(1_000_000),
89
+ }).superRefine((plan, context) => {
90
+ if ((plan.simulationKind === "rigid-body" && plan.engine.profile !== "slopcamera-rigid-body-reference-v1")
91
+ || (plan.simulationKind === "secondary-motion" && plan.engine.profile !== "slopcamera-native-secondary-motion-v1")) {
92
+ context.addIssue({ code: "custom", path: ["engine", "profile"], message: "Simulation kind must match its closed engine profile." })
93
+ }
94
+ if (plan.timeStepUs * plan.stepCount > SPATIAL_SIMULATION_LIMITS.durationUs) {
95
+ context.addIssue({ code: "custom", path: ["stepCount"], message: "Simulation duration exceeds one hour." })
96
+ }
97
+ const bodyIds = new Set<string>()
98
+ const entityIds = new Set<string>()
99
+ for (const [index, body] of plan.bodies.entries()) {
100
+ if (bodyIds.has(body.id)) context.addIssue({ code: "custom", path: ["bodies", index, "id"], message: `Duplicate rigid-body id ${body.id}.` })
101
+ if (entityIds.has(body.entityId)) context.addIssue({ code: "custom", path: ["bodies", index, "entityId"], message: `Entity ${body.entityId} has more than one rigid body.` })
102
+ bodyIds.add(body.id)
103
+ entityIds.add(body.entityId)
104
+ }
105
+ const constraintIds = new Set<string>()
106
+ for (const [index, constraint] of plan.constraints.entries()) {
107
+ if (constraintIds.has(constraint.constraintId)) context.addIssue({ code: "custom", path: ["constraints", index, "constraintId"], message: `Duplicate constraint id ${constraint.constraintId}.` })
108
+ constraintIds.add(constraint.constraintId)
109
+ const missingBodies = [constraint.bodyA, constraint.bodyB].filter(id => !bodyIds.has(id))
110
+ if (missingBodies.length > 0) {
111
+ context.addIssue({ code: "custom", path: ["constraints", index], message: `Constraint ${constraint.constraintId} references unknown body ${missingBodies.join(", ")}.` })
112
+ }
113
+ if (constraint.bodyA === constraint.bodyB) {
114
+ context.addIssue({ code: "custom", path: ["constraints", index], message: `Constraint ${constraint.constraintId} cannot connect a body to itself.` })
115
+ }
116
+ if (constraint.kind === "hinge") {
117
+ if (constraint.axis.every(value => value === 0)) context.addIssue({ code: "custom", path: ["constraints", index, "axis"], message: "Hinge axes must be nonzero." })
118
+ if (constraint.limits !== undefined && constraint.limits[1] < constraint.limits[0]) context.addIssue({ code: "custom", path: ["constraints", index, "limits"], message: "Hinge limits must be ordered." })
119
+ }
120
+ }
121
+ const estimatedBytes = plan.bodies.length * plan.stepCount * 10 * Float64Array.BYTES_PER_ELEMENT
122
+ if (estimatedBytes > plan.maximumOutputBytes) {
123
+ context.addIssue({ code: "custom", path: ["maximumOutputBytes"], message: `Estimated ordinary-animation bytes (${estimatedBytes}) exceed maximumOutputBytes (${plan.maximumOutputBytes}).` })
124
+ }
125
+ })
126
+
127
+ const simulationBakeReceiptBodySchema = z.strictObject({
128
+ cacheId: z.string().min(1).max(128).regex(/^cache_[a-z0-9][a-z0-9_-]*$/u),
129
+ engine: SpatialSimulationEngineSchema,
130
+ kind: z.literal("slopcamera.spatial-simulation-bake-receipt"),
131
+ output: z.strictObject({
132
+ animationSha256: SpatialDigestSchema,
133
+ bytes: z.number().int().safe().positive().max(SPATIAL_SIMULATION_LIMITS.outputBytes),
134
+ channelCount: z.number().int().min(1).max(SPATIAL_SIMULATION_LIMITS.bodies * 2),
135
+ keyCount: z.number().int().min(1).max(SPATIAL_SIMULATION_LIMITS.bodies * SPATIAL_SIMULATION_LIMITS.steps * 2),
136
+ }),
137
+ planSha256: SpatialDigestSchema,
138
+ schemaVersion: z.literal(1),
139
+ seed: z.number().int().min(0).max(2_147_483_647),
140
+ sourceDigest: SpatialDigestSchema,
141
+ stepCount: z.number().int().min(1).max(SPATIAL_SIMULATION_LIMITS.steps),
142
+ timeStepUs: SpatialTimeUsSchema.min(1).max(1_000_000),
143
+ })
144
+
145
+ export const SpatialSimulationBakeReceiptSchema = simulationBakeReceiptBodySchema.extend({
146
+ receiptSha256: SpatialDigestSchema,
147
+ })
148
+
149
+ export type SpatialSimulationPlan = Readonly<z.infer<typeof SpatialSimulationPlanSchema>>
150
+ export type SpatialSimulationBakeReceipt = Readonly<z.infer<typeof SpatialSimulationBakeReceiptSchema>>
151
+
152
+ export function parseSpatialSimulationPlan(input: unknown): SpatialSimulationPlan {
153
+ return deepFreezeJson(parseSpatialValue(SpatialSimulationPlanSchema, input, "simulation plan"))
154
+ }
155
+
156
+ export function spatialSimulationPlanSha256(plan: SpatialSimulationPlan): string {
157
+ return spatialValueSha256(plan)
158
+ }
159
+
160
+ export function createSpatialSimulationBakeReceipt(input: z.input<typeof simulationBakeReceiptBodySchema>): SpatialSimulationBakeReceipt {
161
+ const body = simulationBakeReceiptBodySchema.parse(input)
162
+ return deepFreezeJson(SpatialSimulationBakeReceiptSchema.parse({ ...body, receiptSha256: canonicalJsonSha256(body) }))
163
+ }
164
+
165
+ export function parseSpatialSimulationBakeReceipt(input: unknown): SpatialSimulationBakeReceipt {
166
+ const receipt = parseSpatialValue(SpatialSimulationBakeReceiptSchema, input, "simulation bake receipt")
167
+ const { receiptSha256, ...body } = receipt
168
+ if (canonicalJsonSha256(body) !== receiptSha256) {
169
+ throw new SpatialSceneError("conflict", "Simulation bake receipt digest does not match its body.", "simulation-bake")
170
+ }
171
+ return deepFreezeJson(receipt)
172
+ }
173
+
174
+ export function reconcileSpatialSimulationBakeReceipt(planInput: unknown, receiptInput: unknown): SpatialSimulationBakeReceipt {
175
+ const plan = parseSpatialSimulationPlan(planInput)
176
+ const receipt = parseSpatialSimulationBakeReceipt(receiptInput)
177
+ const planSha256 = spatialSimulationPlanSha256(plan)
178
+ if (receipt.planSha256 !== planSha256 || receipt.cacheId !== plan.cacheId || receipt.sourceDigest !== plan.sourceDigest
179
+ || receipt.engine.profile !== plan.engine.profile || receipt.engine.identitySha256 !== plan.engine.identitySha256
180
+ || receipt.seed !== plan.seed || receipt.stepCount !== plan.stepCount || receipt.timeStepUs !== plan.timeStepUs) {
181
+ throw new SpatialSceneError("conflict", "Simulation bake receipt is stale or belongs to a different plan, cache, source, engine, seed, or clock.", "simulation-bake")
182
+ }
183
+ if (receipt.output.bytes > plan.maximumOutputBytes) {
184
+ throw new SpatialSceneError("invalid-data", "Simulation bake output exceeds the plan output-byte budget.", "simulation-bake")
185
+ }
186
+ return deepFreezeJson(receipt)
187
+ }