@hraness/slopcamera 3.2.8 → 3.3.1

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 (150) 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 +268 -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-cgpkyjqa.js +3 -0
  59. package/dist/index-f0ptv8dm.js +7 -0
  60. package/dist/index-grk47tgs.js +3 -0
  61. package/dist/index-qry58nj2.js +10 -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/package.json +7 -5
  69. package/skills/slopcamera/SKILL.md +6 -1
  70. package/skills/slopcamera/references/directed-scenes.md +22 -2
  71. package/skills/slopcamera/references/gateway-media.md +15 -1
  72. package/skills/slopcamera/references/image-galleries.md +166 -0
  73. package/skills/slopcamera/references/install.md +1 -1
  74. package/skills/slopcamera/references/parametric-design.md +14 -0
  75. package/skills/slopcamera/references/scene-building.md +32 -0
  76. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  77. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  78. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  79. package/src/capability-manifest.ts +361 -0
  80. package/src/cli.ts +222 -4
  81. package/src/code/canonical-json.ts +9 -0
  82. package/src/generate.ts +2 -2
  83. package/src/icon.ts +1111 -0
  84. package/src/image-gallery.ts +1049 -0
  85. package/src/index.ts +11 -0
  86. package/src/mcp/boundary.ts +11 -11
  87. package/src/mcp/index.ts +3 -0
  88. package/src/mcp/server.ts +2 -1
  89. package/src/mcp/tools.ts +1642 -130
  90. package/src/mcp/types.ts +19 -0
  91. package/src/operations.ts +412 -0
  92. package/src/portable-capability-manifest.ts +67 -0
  93. package/src/scene-gallery.ts +184 -0
  94. package/src/spatial-scene/asset-admission.ts +183 -0
  95. package/src/spatial-scene/audit-rendered.ts +692 -0
  96. package/src/spatial-scene/audit.ts +617 -0
  97. package/src/spatial-scene/behavior-audit.ts +252 -0
  98. package/src/spatial-scene/behavior-bake.ts +233 -0
  99. package/src/spatial-scene/behavior-fns.ts +585 -0
  100. package/src/spatial-scene/behavior-gallery.ts +109 -0
  101. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  102. package/src/spatial-scene/behavior-trace.ts +325 -0
  103. package/src/spatial-scene/behavior.ts +618 -0
  104. package/src/spatial-scene/build.ts +632 -0
  105. package/src/spatial-scene/camera-rig.ts +125 -0
  106. package/src/spatial-scene/camera-track.ts +4 -2
  107. package/src/spatial-scene/character.ts +124 -0
  108. package/src/spatial-scene/contracts.ts +75 -7
  109. package/src/spatial-scene/design-templates.ts +351 -0
  110. package/src/spatial-scene/design.ts +327 -0
  111. package/src/spatial-scene/direction-compile.ts +584 -0
  112. package/src/spatial-scene/direction.ts +122 -0
  113. package/src/spatial-scene/effects.ts +149 -0
  114. package/src/spatial-scene/evaluate.ts +109 -29
  115. package/src/spatial-scene/gallery.ts +277 -0
  116. package/src/spatial-scene/generate.ts +239 -0
  117. package/src/spatial-scene/geometry-native.ts +165 -0
  118. package/src/spatial-scene/geometry.ts +1216 -0
  119. package/src/spatial-scene/gltf.ts +600 -77
  120. package/src/spatial-scene/identity.ts +55 -2
  121. package/src/spatial-scene/index.ts +36 -0
  122. package/src/spatial-scene/inspect.ts +35 -6
  123. package/src/spatial-scene/material-lighting.ts +609 -0
  124. package/src/spatial-scene/math.ts +21 -4
  125. package/src/spatial-scene/motion-evidence.ts +88 -0
  126. package/src/spatial-scene/parametric.ts +765 -0
  127. package/src/spatial-scene/particle-preparation.ts +183 -0
  128. package/src/spatial-scene/particle.ts +195 -0
  129. package/src/spatial-scene/patch.ts +109 -15
  130. package/src/spatial-scene/performance.ts +1613 -0
  131. package/src/spatial-scene/probe.ts +137 -0
  132. package/src/spatial-scene/recipe-pack.ts +86 -0
  133. package/src/spatial-scene/render-effects.ts +240 -0
  134. package/src/spatial-scene/review.ts +639 -0
  135. package/src/spatial-scene/simulation-bake.ts +206 -0
  136. package/src/spatial-scene/simulation.ts +187 -0
  137. package/src/spatial-scene/solve.ts +366 -0
  138. package/src/spatial-scene/temporal-audit.ts +354 -0
  139. package/src/version.ts +1 -1
  140. package/dist/index-42zsesc1.js +0 -2257
  141. package/dist/index-7131pg1c.js +0 -1820
  142. package/dist/index-7308egqr.js +0 -48
  143. package/dist/index-8txs6fkn.js +0 -712
  144. package/dist/index-fava6pge.js +0 -192
  145. package/dist/index-h1k0fnjq.js +0 -2363
  146. package/dist/index-r7gdhmsp.js +0 -424
  147. package/dist/index-sh6xbav6.js +0 -1219
  148. package/dist/index-z1w83f81.js +0 -4
  149. package/dist/index-zfnddgay.js +0 -2029
  150. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -0,0 +1,584 @@
1
+ import { z } from "zod"
2
+
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import { spatialEntityLocalBounds } from "./audit.js"
5
+ import { SpatialCameraRigSchema, type SpatialCameraRig } from "./camera-rig.js"
6
+ import {
7
+ SpatialCameraIdSchema,
8
+ SpatialDigestSchema,
9
+ SpatialEntityIdSchema,
10
+ SpatialSceneV1Schema,
11
+ SpatialShotV1Schema,
12
+ SpatialTimeUsSchema,
13
+ type SpatialSceneV1,
14
+ type SpatialShotV1,
15
+ } from "./contracts.js"
16
+ import {
17
+ SPATIAL_DIRECTION_LIMITS,
18
+ parseSpatialDirection,
19
+ SpatialDirectionSchema,
20
+ spatialDirectionSha256,
21
+ type SpatialDirection,
22
+ } from "./direction.js"
23
+ import { SpatialPostProcessStepSchema } from "./effects.js"
24
+
25
+ type SpatialCameraCoverage = SpatialDirection["coverage"][number]
26
+ import { createSpatialEvaluationContext, evaluateSpatialSceneInContext } from "./evaluate.js"
27
+ import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
28
+
29
+ /**
30
+ * Deterministic direction compiler for Phase 12.
31
+ *
32
+ * Semantic direction compiles into inspectable proposals: complete camera rigs
33
+ * and spatial shots where world state resolves them, plus explicitly unbound
34
+ * performance and look descriptors. The compilation never applies itself:
35
+ * `verified` is always false, every unbound slot is named in
36
+ * `unresolvedIntents`, and derived identities stay separate from sources.
37
+ */
38
+
39
+ export const SPATIAL_DIRECTION_COMPILER_ID = "slopcamera.spatial-direction-compiler@v1"
40
+
41
+ export const SPATIAL_DIRECTION_COMPILATION_LIMITS = Object.freeze({
42
+ advisories: 256,
43
+ unresolvedIntents: 256,
44
+ checks: 512,
45
+ })
46
+
47
+ export const SpatialDirectionAdvisoryCodeSchema = z.enum([
48
+ "unresolved-reference",
49
+ "stale-digest",
50
+ "defaulted-camera",
51
+ "missing-camera",
52
+ "bounds-unknown",
53
+ "interval-outside-scene",
54
+ "coverage-ungrouped",
55
+ ])
56
+ export type SpatialDirectionAdvisoryCode = z.infer<typeof SpatialDirectionAdvisoryCodeSchema>
57
+
58
+ export const SpatialDirectionAdvisorySchema = z.strictObject({
59
+ code: SpatialDirectionAdvisoryCodeSchema,
60
+ referenceId: z.string().min(1).max(128).optional(),
61
+ detail: z.string().min(1).max(240),
62
+ })
63
+ export type SpatialDirectionAdvisory = Readonly<z.infer<typeof SpatialDirectionAdvisorySchema>>
64
+
65
+ export const SpatialUnresolvedIntentDomainSchema = z.enum(["performance", "camera", "shot", "look"])
66
+ export const SpatialUnresolvedIntentSchema = z.strictObject({
67
+ intentId: z.string().min(1).max(96).regex(/^prop_[a-f0-9]{16}$/u),
68
+ domain: SpatialUnresolvedIntentDomainSchema,
69
+ referenceId: z.string().min(1).max(128),
70
+ slot: z.string().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/u),
71
+ detail: z.string().min(1).max(240),
72
+ })
73
+ export type SpatialUnresolvedIntent = Readonly<z.infer<typeof SpatialUnresolvedIntentSchema>>
74
+
75
+ const unresolvedSlot = (intentId: string, domain: SpatialUnresolvedIntent["domain"], referenceId: string, slot: string, detail: string): SpatialUnresolvedIntent =>
76
+ deepFreezeJson({ intentId, domain, referenceId, slot, detail })
77
+
78
+ export const SpatialPerformanceClipKindSchema = z.enum([
79
+ "idle",
80
+ "walk-cycle",
81
+ "run-cycle",
82
+ "turn",
83
+ "gesture",
84
+ "interact",
85
+ "expression",
86
+ ])
87
+ export type SpatialPerformanceClipKind = z.infer<typeof SpatialPerformanceClipKindSchema>
88
+
89
+ export const SpatialPerformanceProposalSchema = z.strictObject({
90
+ proposalId: z.string().min(1).max(96).regex(/^prop_[a-f0-9]{16}$/u),
91
+ actionId: z.string().min(1).max(64),
92
+ characterId: z.string().min(1).max(128),
93
+ action: SpatialDirectionSchema.shape.actions.element.shape.action,
94
+ clipKind: SpatialPerformanceClipKindSchema,
95
+ startUs: SpatialTimeUsSchema,
96
+ endUs: SpatialTimeUsSchema,
97
+ characterEntityId: SpatialEntityIdSchema.optional(),
98
+ targetEntityId: SpatialEntityIdSchema.optional(),
99
+ unresolved: z.array(z.string().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/u)).min(1).max(16),
100
+ })
101
+ export type SpatialPerformanceProposal = Readonly<z.infer<typeof SpatialPerformanceProposalSchema>>
102
+
103
+ export const SpatialLookProposalSchema = z.strictObject({
104
+ proposalId: z.string().min(1).max(96).regex(/^prop_[a-f0-9]{16}$/u),
105
+ lookId: z.string().min(1).max(64),
106
+ startUs: SpatialTimeUsSchema,
107
+ endUs: SpatialTimeUsSchema,
108
+ lighting: z.string().min(1).max(128),
109
+ atmosphere: z.string().min(1).max(128),
110
+ unresolved: z.array(z.string().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/u)).min(1).max(16),
111
+ suggestedLightingPreset: z.string().min(1).max(64).optional(),
112
+ suggestedMaterialPalette: z.string().min(1).max(64).optional(),
113
+ suggestedPostProcess: z.array(SpatialPostProcessStepSchema).max(16).optional(),
114
+ })
115
+ export type SpatialLookProposal = Readonly<z.infer<typeof SpatialLookProposalSchema>>
116
+
117
+ export const SpatialDirectionProposalsSchema = z.strictObject({
118
+ performance: z.array(SpatialPerformanceProposalSchema).max(SPATIAL_DIRECTION_LIMITS.actions),
119
+ cameraRigs: z.array(SpatialCameraRigSchema).max(SPATIAL_DIRECTION_LIMITS.coverage),
120
+ shots: z.array(SpatialShotV1Schema).max(SPATIAL_DIRECTION_LIMITS.coverage),
121
+ lookIntents: z.array(SpatialLookProposalSchema).max(SPATIAL_DIRECTION_LIMITS.looks),
122
+ })
123
+
124
+ export const SpatialDirectionCompilationSchema = z.strictObject({
125
+ kind: z.literal("slopcamera.spatial-direction-compilation"),
126
+ schemaVersion: z.literal(1),
127
+ directionSha256: SpatialDigestSchema,
128
+ sceneSha256: SpatialDigestSchema,
129
+ compilerVersion: z.literal(SPATIAL_DIRECTION_COMPILER_ID),
130
+ verified: z.literal(false),
131
+ cameraId: SpatialCameraIdSchema.optional(),
132
+ proposals: SpatialDirectionProposalsSchema,
133
+ advisories: z.array(SpatialDirectionAdvisorySchema).max(SPATIAL_DIRECTION_COMPILATION_LIMITS.advisories),
134
+ unresolvedIntents: z.array(SpatialUnresolvedIntentSchema).max(SPATIAL_DIRECTION_COMPILATION_LIMITS.unresolvedIntents),
135
+ })
136
+ export type SpatialDirectionCompilation = Readonly<z.infer<typeof SpatialDirectionCompilationSchema>>
137
+
138
+ export const SpatialDirectionCheckFindingSchema = z.strictObject({
139
+ code: SpatialDirectionAdvisoryCodeSchema,
140
+ severity: z.enum(["error", "warning"]),
141
+ referenceId: z.string().min(1).max(128).optional(),
142
+ detail: z.string().min(1).max(240),
143
+ })
144
+ export type SpatialDirectionCheckFinding = Readonly<z.infer<typeof SpatialDirectionCheckFindingSchema>>
145
+
146
+ export const SpatialDirectionCheckReportSchema = z.strictObject({
147
+ kind: z.literal("slopcamera.spatial-direction-check"),
148
+ schemaVersion: z.literal(1),
149
+ directionSha256: SpatialDigestSchema,
150
+ sceneSha256: SpatialDigestSchema,
151
+ findings: z.array(SpatialDirectionCheckFindingSchema).max(SPATIAL_DIRECTION_COMPILATION_LIMITS.checks),
152
+ counts: z.strictObject({
153
+ beats: z.number().int().min(0),
154
+ actions: z.number().int().min(0),
155
+ coverage: z.number().int().min(0),
156
+ looks: z.number().int().min(0),
157
+ errors: z.number().int().min(0),
158
+ warnings: z.number().int().min(0),
159
+ }),
160
+ })
161
+ export type SpatialDirectionCheckReport = Readonly<z.infer<typeof SpatialDirectionCheckReportSchema>>
162
+
163
+ export function spatialDirectionCompilationSha256(compilation: SpatialDirectionCompilation): string {
164
+ return spatialValueSha256(compilation)
165
+ }
166
+
167
+ const proposalId = (domain: string, referenceId: string): string =>
168
+ `prop_${spatialValueSha256({ domain: `slopcamera.direction-proposal.v1.${domain}`, referenceId }).slice(0, 16)}`
169
+
170
+ /** Direction references are semantic ids; scene entities are entity_-prefixed. */
171
+ function resolveEntityId(scene: SpatialSceneV1, referenceId: string): string | undefined {
172
+ const ids = new Set(scene.entities.map((entity) => entity.entityId))
173
+ if (ids.has(referenceId)) return referenceId
174
+ const prefixed = `entity_${referenceId}`
175
+ return ids.has(prefixed) ? prefixed : undefined
176
+ }
177
+
178
+ const FRAMING_DISTANCE_M: Readonly<Record<SpatialCameraCoverage["framing"], number>> = {
179
+ "insert": 0.5,
180
+ "extreme-close-up": 0.8,
181
+ "close-up": 1.4,
182
+ "medium-close-up": 2.4,
183
+ "medium": 4,
184
+ "medium-wide": 6.5,
185
+ "wide": 11,
186
+ "extreme-wide": 20,
187
+ "over-shoulder": 1.8,
188
+ }
189
+
190
+ const CLIP_KIND_BY_ACTION: Readonly<Record<SpatialPerformanceProposal["action"], SpatialPerformanceClipKind>> = {
191
+ idle: "idle",
192
+ walk: "walk-cycle",
193
+ run: "run-cycle",
194
+ turn: "turn",
195
+ gesture: "gesture",
196
+ interact: "interact",
197
+ morph: "expression",
198
+ }
199
+
200
+ const normalize = (v: readonly [number, number, number]): [number, number, number] => {
201
+ const n = Math.hypot(v[0], v[1], v[2])
202
+ return n < 1e-9 ? [0, 0, -1] : [v[0] / n, v[1] / n, v[2] / n]
203
+ }
204
+ const cross = (a: readonly number[], b: readonly number[]): [number, number, number] => [
205
+ a[1]! * b[2]! - a[2]! * b[1]!,
206
+ a[2]! * b[0]! - a[0]! * b[2]!,
207
+ a[0]! * b[1]! - a[1]! * b[0]!,
208
+ ]
209
+ function quaternionFromBasis(x: readonly number[], y: readonly number[], z: readonly number[]): [number, number, number, number] {
210
+ const m00 = x[0]!, m01 = y[0]!, m02 = z[0]!, m10 = x[1]!, m11 = y[1]!, m12 = z[1]!, m20 = x[2]!, m21 = y[2]!, m22 = z[2]!, trace = m00 + m11 + m22
211
+ let q: [number, number, number, number]
212
+ if (trace > 0) { const s = 2 * Math.sqrt(trace + 1); q = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, s / 4] }
213
+ else if (m00 > m11 && m00 > m22) { const s = 2 * Math.sqrt(1 + m00 - m11 - m22); q = [s / 4, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s] }
214
+ else if (m11 > m22) { const s = 2 * Math.sqrt(1 + m11 - m00 - m22); q = [(m01 + m10) / s, s / 4, (m12 + m21) / s, (m02 + m20) / s] }
215
+ else { const s = 2 * Math.sqrt(1 + m22 - m11 - m00); q = [(m02 + m20) / s, (m12 + m21) / s, s / 4, (m10 - m01) / s] }
216
+ const n = Math.hypot(...q); return q.map(v => v / n) as [number, number, number, number]
217
+ }
218
+ function lookAtQuaternion(position: readonly number[], target: readonly number[]): [number, number, number, number] {
219
+ const forward = [target[0]! - position[0]!, target[1]! - position[1]!, target[2]! - position[2]!] as const
220
+ if (Math.hypot(...forward) < 1e-9) throw new SpatialSceneError("invalid-data", "Camera and look-at target must not coincide.")
221
+ const f = normalize(forward as [number, number, number]), up = Math.abs(f[1]!) > 0.999999 ? [0, 0, 1] : [0, 1, 0]
222
+ const right = normalize(cross(f, up) as [number, number, number]), correctedUp = cross(right, f)
223
+ return quaternionFromBasis(right, correctedUp, [-f[0], -f[1], -f[2]])
224
+ }
225
+
226
+ const shakeSeed = (coverageId: string): number => {
227
+ let seed = 0
228
+ const digest = spatialValueSha256({ domain: "slopcamera.direction-shake-seed.v1", coverageId })
229
+ for (let index = 0; index < 8; index += 1) seed = (seed * 16 + Number.parseInt(digest[index]!, 16)) >>> 0
230
+ return seed
231
+ }
232
+
233
+ interface ResolvedSubject {
234
+ readonly entityId: string | undefined
235
+ readonly position: [number, number, number]
236
+ readonly positionEnd: [number, number, number]
237
+ readonly radiusM: number
238
+ }
239
+
240
+ interface CompilationContext {
241
+ readonly direction: SpatialDirection
242
+ readonly scene: SpatialSceneV1
243
+ readonly sceneSha256: string
244
+ readonly cameraId: string | undefined
245
+ readonly advisories: SpatialDirectionAdvisory[]
246
+ readonly unresolvedIntents: SpatialUnresolvedIntent[]
247
+ }
248
+
249
+ const advisory = (code: SpatialDirectionAdvisoryCode, detail: string, referenceId?: string): SpatialDirectionAdvisory =>
250
+ deepFreezeJson(referenceId === undefined ? { code, detail } : { code, referenceId, detail })
251
+
252
+ function resolveSubject(
253
+ context: CompilationContext,
254
+ coverage: SpatialCameraCoverage,
255
+ evaluation: ReturnType<typeof createSpatialEvaluationContext> | undefined,
256
+ ): ResolvedSubject | undefined {
257
+ if (coverage.subjectId === undefined || evaluation === undefined || context.cameraId === undefined) return undefined
258
+ const entityId = resolveEntityId(context.scene, coverage.subjectId)
259
+ if (entityId === undefined) {
260
+ context.advisories.push(advisory("unresolved-reference", `Coverage ${coverage.id} subject ${coverage.subjectId} is not a scene entity.`, coverage.id))
261
+ return undefined
262
+ }
263
+ const positionAt = (timeUs: number): [number, number, number] | undefined => {
264
+ const clamped = Math.min(Math.max(timeUs, 0), context.scene.durationUs)
265
+ const snapshot = evaluateSpatialSceneInContext(evaluation, { cameraId: context.cameraId!, timeUs: clamped })
266
+ const evaluated = snapshot.entities.find((item) => item.entity.entityId === entityId)
267
+ if (evaluated === undefined) return undefined
268
+ return [evaluated.worldMatrix[12]!, evaluated.worldMatrix[13]!, evaluated.worldMatrix[14]!]
269
+ }
270
+ const position = positionAt(coverage.startUs)
271
+ const positionEnd = positionAt(coverage.endUs)
272
+ if (position === undefined || positionEnd === undefined) {
273
+ context.advisories.push(advisory("unresolved-reference", `Coverage ${coverage.id} subject ${coverage.subjectId} is absent from evaluated state.`, coverage.id))
274
+ return undefined
275
+ }
276
+ const entity = context.scene.entities.find((item) => item.entityId === entityId)!
277
+ const enclosure = spatialEntityLocalBounds(entity, {})
278
+ let radiusM = 1
279
+ if (enclosure.status === "bounded") {
280
+ const { min, max } = enclosure.bounds
281
+ radiusM = Math.min(Math.max(Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2, 0.000001), 1_000_000)
282
+ } else {
283
+ context.advisories.push(advisory("bounds-unknown", `Coverage ${coverage.id} subject ${entityId} has unbounded geometry (${enclosure.reason}); radius defaulted to 1m.`, coverage.id))
284
+ }
285
+ return { entityId, position, positionEnd, radiusM }
286
+ }
287
+
288
+ function cameraOffset(coverage: SpatialCameraCoverage, subject: ResolvedSubject | undefined): [number, number, number] {
289
+ const distance = FRAMING_DISTANCE_M[coverage.framing]!
290
+ const base = subject?.position ?? [0, 1, 0]
291
+ const lateral = coverage.screenDirection === "left" ? -distance * 0.6 : coverage.screenDirection === "right" ? distance * 0.6 : 0
292
+ const height = subject === undefined ? 1.6 : Math.min(Math.max(subject.radiusM, 0.4), 8)
293
+ return [base[0] + lateral, base[1] + height, base[2] - distance]
294
+ }
295
+
296
+ function compileCoverageRig(context: CompilationContext, coverage: SpatialCameraCoverage, subject: ResolvedSubject | undefined): SpatialCameraRig | undefined {
297
+ const cameraId = context.cameraId
298
+ if (cameraId === undefined) return undefined
299
+ const base = { cameraId, startUs: coverage.startUs, endUs: coverage.endUs } as const
300
+ const targetPosition = subject?.position ?? [0, 1, 0]
301
+ const target = subject === undefined
302
+ ? { position: [0, 1, 0] as [number, number, number], radiusM: 1 }
303
+ : { entityId: subject.entityId, position: targetPosition, radiusM: subject.radiusM }
304
+ const position = cameraOffset(coverage, subject)
305
+ const distance = FRAMING_DISTANCE_M[coverage.framing]!
306
+ try {
307
+ switch (coverage.rigKind) {
308
+ case "tripod":
309
+ return SpatialCameraRigSchema.parse({ ...base, kind: "tripod", pose: { position, rotation: lookAtQuaternion(position, targetPosition) }, target })
310
+ case "handheld":
311
+ return SpatialCameraRigSchema.parse({
312
+ ...base, kind: "handheld",
313
+ pose: { position, rotation: lookAtQuaternion(position, targetPosition) },
314
+ target, shake: { seed: shakeSeed(coverage.id), amplitudeM: 0.05, frequencyHz: 1.5, layers: 2 },
315
+ })
316
+ case "dolly": {
317
+ const toward: [number, number, number] = normalize([targetPosition[0] - position[0], 0, targetPosition[2] - position[2]])
318
+ const from: [number, number, number] = [position[0] + toward[0] * distance * 0.5, position[1], position[2] + toward[2] * distance * 0.5]
319
+ return SpatialCameraRigSchema.parse({ ...base, kind: "dolly", from, to: position, target })
320
+ }
321
+ case "crane":
322
+ return SpatialCameraRigSchema.parse({ ...base, kind: "crane", from: [position[0], position[1] + Math.min(4, distance), position[2]], to: position, target })
323
+ case "orbit":
324
+ return SpatialCameraRigSchema.parse({
325
+ ...base, kind: "orbit",
326
+ center: targetPosition, radiusM: Math.max(distance, subject?.radiusM ?? 1),
327
+ startAngleRad: Math.PI, endAngleRad: Math.PI * 2, heightM: subject === undefined ? 1.6 : Math.min(Math.max(subject.radiusM, 0.4), 8),
328
+ target,
329
+ })
330
+ case "rail": {
331
+ const half = Math.max(distance * 0.5, 0.5)
332
+ return SpatialCameraRigSchema.parse({
333
+ ...base, kind: "rail",
334
+ points: [[position[0] - half, position[1], position[2]], position, [position[0] + half, position[1], position[2]]],
335
+ target,
336
+ })
337
+ }
338
+ case "chase":
339
+ return SpatialCameraRigSchema.parse({
340
+ ...base, kind: "chase", target,
341
+ offset: [0, Math.min(Math.max(subject?.radiusM ?? 1, 0.4), 8), -distance],
342
+ ...(subject === undefined ? {} : { toTarget: subject.positionEnd }),
343
+ shake: { seed: shakeSeed(coverage.id), amplitudeM: 0.03, frequencyHz: 2, layers: 1 },
344
+ })
345
+ case "target-tracking": {
346
+ const half = Math.max(distance * 0.5, 0.5)
347
+ return SpatialCameraRigSchema.parse({
348
+ ...base, kind: "target-tracking",
349
+ from: [position[0] - half, position[1], position[2]], to: [position[0] + half, position[1], position[2]],
350
+ target, ...(subject === undefined ? {} : { targetEnd: subject.positionEnd }),
351
+ })
352
+ }
353
+ }
354
+ throw new SpatialSceneError("invalid-data", `Unsupported coverage rig kind ${coverage.rigKind}.`, "coverage.rigKind")
355
+ } catch (error) {
356
+ context.advisories.push(advisory("unresolved-reference", `Coverage ${coverage.id} rig ${coverage.rigKind} failed deterministic construction: ${error instanceof Error ? error.message : String(error)}`, coverage.id))
357
+ return undefined
358
+ }
359
+ }
360
+
361
+ const compileOptionsSchema = z.strictObject({
362
+ direction: z.unknown(),
363
+ scene: z.unknown(),
364
+ cameraId: z.string().min(1).max(128).optional(),
365
+ })
366
+
367
+ /**
368
+ * Compiles direction into one immutable proposal document. Direction and scene
369
+ * parse from `unknown`; identity derives only from canonical inputs, so equal
370
+ * inputs always compile to equal output.
371
+ */
372
+ export function compileSpatialDirection(input: unknown): SpatialDirectionCompilation {
373
+ const options = parseSpatialValue(compileOptionsSchema, input, "direction compilation")
374
+ const direction = parseSpatialDirection(options.direction)
375
+ const scene = parseSpatialValue(SpatialSceneV1Schema, options.scene, "scene")
376
+ const sceneSha256 = spatialValueSha256(scene)
377
+ const advisories: SpatialDirectionAdvisory[] = []
378
+ const unresolvedIntents: SpatialUnresolvedIntent[] = []
379
+
380
+ if (direction.projectDigest !== sceneSha256) {
381
+ advisories.push(advisory("stale-digest", `Direction projectDigest ${direction.projectDigest} does not match the scene digest ${sceneSha256}.`))
382
+ }
383
+ if (resolveEntityId(scene, direction.entityId) === undefined) {
384
+ advisories.push(advisory("unresolved-reference", `Directed entity ${direction.entityId} is not a scene entity.`, direction.entityId))
385
+ }
386
+
387
+ let cameraId: string | undefined
388
+ if (options.cameraId !== undefined) {
389
+ cameraId = scene.cameras.find((camera) => camera.cameraId === options.cameraId)?.cameraId
390
+ if (cameraId === undefined) throw new SpatialSceneError("not-found", `Unknown camera ${options.cameraId}.`, "cameraId")
391
+ } else if (scene.cameras.length === 1) {
392
+ cameraId = scene.cameras[0]!.cameraId
393
+ } else if (scene.cameras.length > 1) {
394
+ cameraId = [...scene.cameras].map((camera) => camera.cameraId).sort()[0]
395
+ advisories.push(advisory("defaulted-camera", `Scene declares ${scene.cameras.length} cameras; coverage defaulted to ${cameraId}. Pass cameraId explicitly to silence this advisory.`))
396
+ } else if (direction.coverage.length > 0) {
397
+ advisories.push(advisory("missing-camera", "Scene declares no cameras; coverage cannot resolve rigs or shots."))
398
+ }
399
+
400
+ const context: CompilationContext = { direction, scene, sceneSha256, cameraId, advisories, unresolvedIntents }
401
+ const evaluation = cameraId === undefined ? undefined : createSpatialEvaluationContext(scene)
402
+
403
+ const checkInterval = (id: string, endUs: number): void => {
404
+ if (endUs > scene.durationUs) {
405
+ advisories.push(advisory("interval-outside-scene", `Entry ${id} ends at ${endUs}us beyond scene duration ${scene.durationUs}us.`, id))
406
+ }
407
+ }
408
+
409
+ const performance: SpatialPerformanceProposal[] = []
410
+ for (const action of direction.actions) {
411
+ checkInterval(action.id, action.endUs)
412
+ const characterEntityId = resolveEntityId(scene, action.characterId)
413
+ const targetEntityId = action.targetId === undefined ? undefined : resolveEntityId(scene, action.targetId)
414
+ if (characterEntityId === undefined) {
415
+ advisories.push(advisory("unresolved-reference", `Action ${action.id} character ${action.characterId} is not a scene entity.`, action.id))
416
+ }
417
+ if (action.targetId !== undefined && targetEntityId === undefined) {
418
+ advisories.push(advisory("unresolved-reference", `Action ${action.id} target ${action.targetId} is not a scene entity.`, action.id))
419
+ }
420
+ const intentId = proposalId("performance", action.id)
421
+ const unresolved = ["clip-digest", "rig-digest", "mapping-digest"]
422
+ if (characterEntityId === undefined) unresolved.push("character-entity")
423
+ if (action.targetId !== undefined && targetEntityId === undefined) unresolved.push("target-entity")
424
+ for (const slot of unresolved) {
425
+ unresolvedIntents.push(unresolvedSlot(intentId, "performance", action.id, slot, `Action ${action.id} cannot bind ${slot}; compile and admit a real clip, rig, and humanoid mapping first.`))
426
+ }
427
+ performance.push(deepFreezeJson({
428
+ proposalId: intentId,
429
+ actionId: action.id,
430
+ characterId: action.characterId,
431
+ action: action.action,
432
+ clipKind: CLIP_KIND_BY_ACTION[action.action],
433
+ startUs: action.startUs,
434
+ endUs: action.endUs,
435
+ ...(characterEntityId === undefined ? {} : { characterEntityId }),
436
+ ...(targetEntityId === undefined ? {} : { targetEntityId }),
437
+ unresolved,
438
+ }))
439
+ }
440
+
441
+ const beats = direction.beats.map((beat) => ({ id: beat.id, startUs: beat.startUs, endUs: beat.endUs }))
442
+ for (const beat of direction.beats) checkInterval(beat.id, beat.endUs)
443
+
444
+ const cameraRigs: SpatialCameraRig[] = []
445
+ const shots: SpatialShotV1[] = []
446
+ for (const coverage of direction.coverage) {
447
+ checkInterval(coverage.id, coverage.endUs)
448
+ if (beats.length > 0 && !beats.some((beat) => beat.startUs <= coverage.startUs && beat.endUs >= coverage.endUs)) {
449
+ advisories.push(advisory("coverage-ungrouped", `Coverage ${coverage.id} is not contained in any dramatic beat.`, coverage.id))
450
+ }
451
+ const subject = resolveSubject(context, coverage, evaluation)
452
+ const rig = compileCoverageRig(context, coverage, subject)
453
+ if (rig !== undefined) {
454
+ cameraRigs.push(rig)
455
+ unresolvedIntents.push(unresolvedSlot(proposalId("camera", coverage.id), "camera", coverage.id, "camera-track", `Coverage ${coverage.id} rig is a proposal; compile it to a camera track before admission.`))
456
+ } else if (cameraId !== undefined) {
457
+ unresolvedIntents.push(unresolvedSlot(proposalId("camera", coverage.id), "camera", coverage.id, "rig", `Coverage ${coverage.id} could not resolve a deterministic ${coverage.rigKind} rig.`))
458
+ }
459
+ if (cameraId !== undefined) {
460
+ const shotId = `shot_${spatialValueSha256({ domain: "slopcamera.direction-shot.v1", coverageId: coverage.id }).slice(0, 16)}`
461
+ shots.push(deepFreezeJson(SpatialShotV1Schema.parse({
462
+ shotId, sceneSha256, cameraId,
463
+ range: { startUs: coverage.startUs, endUs: coverage.endUs },
464
+ sceneStartUs: coverage.startUs,
465
+ playback: "once",
466
+ overrides: [],
467
+ ...(rig !== undefined && (rig.kind === "tripod" || rig.kind === "handheld") ? { cameraPoseOverride: rig.pose } : {}),
468
+ })))
469
+ unresolvedIntents.push(unresolvedSlot(proposalId("shot", coverage.id), "shot", coverage.id, "render-plan", `Shot ${shotId} is unverified; bind a render plan and audit evidence before admission.`))
470
+ }
471
+ }
472
+
473
+ const lookIntents: SpatialLookProposal[] = []
474
+ for (const look of direction.looks) {
475
+ checkInterval(look.id, look.endUs)
476
+ const intentId = proposalId("look", look.id)
477
+ for (const slot of ["material-lighting", "post-process"] as const) {
478
+ unresolvedIntents.push(unresolvedSlot(intentId, "look", look.id, slot, `Look ${look.id} carries authored text; compile ${slot} documents before admission.`))
479
+ }
480
+ lookIntents.push(deepFreezeJson({
481
+ proposalId: intentId, lookId: look.id, startUs: look.startUs, endUs: look.endUs,
482
+ lighting: look.lighting, atmosphere: look.atmosphere,
483
+ unresolved: ["material-lighting", "post-process"],
484
+ }))
485
+ }
486
+
487
+ return deepFreezeJson(SpatialDirectionCompilationSchema.parse({
488
+ kind: "slopcamera.spatial-direction-compilation",
489
+ schemaVersion: 1,
490
+ directionSha256: spatialDirectionSha256(direction),
491
+ sceneSha256,
492
+ compilerVersion: SPATIAL_DIRECTION_COMPILER_ID,
493
+ verified: false,
494
+ ...(cameraId === undefined ? {} : { cameraId }),
495
+ proposals: { performance, cameraRigs, shots, lookIntents },
496
+ advisories,
497
+ unresolvedIntents,
498
+ }))
499
+ }
500
+
501
+ const checkOptionsSchema = z.strictObject({
502
+ direction: z.unknown(),
503
+ scene: z.unknown(),
504
+ })
505
+
506
+ /**
507
+ * Validates a direction document against one scene without compiling. Reports
508
+ * stale digests, unresolved references, and intervals outside scene duration as
509
+ * errors; weaker structural notes as warnings.
510
+ */
511
+ export function checkSpatialDirection(input: unknown): SpatialDirectionCheckReport {
512
+ const options = parseSpatialValue(checkOptionsSchema, input, "direction check")
513
+ const direction = parseSpatialDirection(options.direction)
514
+ const scene = parseSpatialValue(SpatialSceneV1Schema, options.scene, "scene")
515
+ const sceneSha256 = spatialValueSha256(scene)
516
+ const findings: SpatialDirectionCheckFinding[] = []
517
+ const finding = (code: SpatialDirectionAdvisoryCode, severity: "error" | "warning", detail: string, referenceId?: string): void => {
518
+ findings.push(deepFreezeJson(referenceId === undefined ? { code, severity, detail } : { code, severity, referenceId, detail }))
519
+ }
520
+
521
+ if (direction.projectDigest !== sceneSha256) {
522
+ finding("stale-digest", "error", `Direction projectDigest ${direction.projectDigest} does not match scene digest ${sceneSha256}; re-author or rebind the direction.`)
523
+ }
524
+ if (resolveEntityId(scene, direction.entityId) === undefined) {
525
+ finding("unresolved-reference", "error", `Directed entity ${direction.entityId} is not a scene entity.`, direction.entityId)
526
+ }
527
+ if (scene.cameras.length === 0 && direction.coverage.length > 0) {
528
+ finding("missing-camera", "error", "Direction declares camera coverage but the scene declares no cameras.")
529
+ }
530
+ for (const action of direction.actions) {
531
+ if (resolveEntityId(scene, action.characterId) === undefined) {
532
+ finding("unresolved-reference", "error", `Action ${action.id} character ${action.characterId} is not a scene entity.`, action.id)
533
+ }
534
+ if (action.targetId !== undefined && resolveEntityId(scene, action.targetId) === undefined) {
535
+ finding("unresolved-reference", "error", `Action ${action.id} target ${action.targetId} is not a scene entity.`, action.id)
536
+ }
537
+ if (action.endUs > scene.durationUs) {
538
+ finding("interval-outside-scene", "error", `Action ${action.id} ends at ${action.endUs}us beyond scene duration ${scene.durationUs}us.`, action.id)
539
+ }
540
+ }
541
+ for (const coverage of direction.coverage) {
542
+ if (coverage.subjectId !== undefined && resolveEntityId(scene, coverage.subjectId) === undefined) {
543
+ finding("unresolved-reference", "error", `Coverage ${coverage.id} subject ${coverage.subjectId} is not a scene entity.`, coverage.id)
544
+ }
545
+ if (coverage.endUs > scene.durationUs) {
546
+ finding("interval-outside-scene", "error", `Coverage ${coverage.id} ends at ${coverage.endUs}us beyond scene duration ${scene.durationUs}us.`, coverage.id)
547
+ }
548
+ }
549
+ const beats = direction.beats.map((beat) => ({ id: beat.id, startUs: beat.startUs, endUs: beat.endUs }))
550
+ for (const beat of direction.beats) {
551
+ if (beat.endUs > scene.durationUs) {
552
+ finding("interval-outside-scene", "error", `Beat ${beat.id} ends at ${beat.endUs}us beyond scene duration ${scene.durationUs}us.`, beat.id)
553
+ }
554
+ }
555
+ if (beats.length > 0) {
556
+ for (const coverage of direction.coverage) {
557
+ if (!beats.some((beat) => beat.startUs <= coverage.startUs && beat.endUs >= coverage.endUs)) {
558
+ finding("coverage-ungrouped", "warning", `Coverage ${coverage.id} is not contained in any dramatic beat.`, coverage.id)
559
+ }
560
+ }
561
+ }
562
+ for (const look of direction.looks) {
563
+ if (look.endUs > scene.durationUs) {
564
+ finding("interval-outside-scene", "error", `Look ${look.id} ends at ${look.endUs}us beyond scene duration ${scene.durationUs}us.`, look.id)
565
+ }
566
+ }
567
+
568
+ const errors = findings.filter((item) => item.severity === "error").length
569
+ return deepFreezeJson(SpatialDirectionCheckReportSchema.parse({
570
+ kind: "slopcamera.spatial-direction-check",
571
+ schemaVersion: 1,
572
+ directionSha256: spatialDirectionSha256(direction),
573
+ sceneSha256,
574
+ findings,
575
+ counts: {
576
+ beats: direction.beats.length,
577
+ actions: direction.actions.length,
578
+ coverage: direction.coverage.length,
579
+ looks: direction.looks.length,
580
+ errors,
581
+ warnings: findings.length - errors,
582
+ },
583
+ }))
584
+ }