@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.
- package/README.md +29 -9
- package/apps/desktop/application/context.ts +6 -0
- package/apps/desktop/application/default-registry.ts +26 -0
- package/apps/desktop/application/operation.ts +14 -1
- package/apps/desktop/application/operations/index.ts +4 -0
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +514 -11
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +29 -1
- package/apps/desktop/cli/commands.ts +1188 -4
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +172 -6
- package/apps/desktop/cli/main.ts +3 -3
- package/apps/desktop/cli/portable-surface.ts +6 -1
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +271 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/semantic-builder.ts +24 -0
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +725 -273
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +25 -429
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-gs48afpt.js +7 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index-zhg36tf9.js +3 -0
- package/dist/index.js +1 -226
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +12 -0
- package/package.json +7 -5
- package/skills/slopcamera/SKILL.md +7 -1
- package/skills/slopcamera/references/directed-scenes.md +48 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +4 -0
- package/skills/slopcamera/references/native-studio.md +19 -0
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/scene-building.md +172 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +222 -4
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -0
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1649 -132
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +232 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +108 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +617 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/version.ts +1 -1
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7131pg1c.js +0 -1820
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-fava6pge.js +0 -192
- package/dist/index-h1k0fnjq.js +0 -2363
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-zfnddgay.js +0 -2029
- 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
|
+
}
|