@hraness/slopcamera 3.2.7 → 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 (188) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +65 -20
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +6 -9
  6. package/apps/desktop/application/default-registry.ts +26 -10
  7. package/apps/desktop/application/operation.ts +14 -5
  8. package/apps/desktop/application/operations/index.ts +4 -1
  9. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  10. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  11. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  12. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  13. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  14. package/apps/desktop/application/registry.ts +2 -2
  15. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  16. package/apps/desktop/application/spatial-assets.ts +160 -31
  17. package/apps/desktop/application/spatial-render.ts +217 -20
  18. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  19. package/apps/desktop/application/spatial-review.ts +361 -0
  20. package/apps/desktop/application/spatial-spz.ts +48 -3
  21. package/apps/desktop/application/spatial-world-import.ts +26 -5
  22. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  23. package/apps/desktop/cli/args.ts +528 -97
  24. package/apps/desktop/cli/capability-manifest.ts +374 -0
  25. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  26. package/apps/desktop/cli/command-host-resources.ts +31 -2
  27. package/apps/desktop/cli/commands.ts +1215 -70
  28. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  29. package/apps/desktop/cli/help.ts +183 -23
  30. package/apps/desktop/cli/html-scene.ts +139 -28
  31. package/apps/desktop/cli/io.ts +2 -1
  32. package/apps/desktop/cli/main.ts +26 -50
  33. package/apps/desktop/cli/menubar.ts +244 -0
  34. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  35. package/apps/desktop/cli/portable-surface.ts +18 -5
  36. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  37. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  38. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  39. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  40. package/apps/desktop/cli/workflow-code.ts +1 -1
  41. package/apps/desktop/code/application-node-planner.ts +0 -47
  42. package/apps/desktop/code/host-source-layout.ts +46 -0
  43. package/apps/desktop/code/public.ts +0 -1
  44. package/apps/desktop/code/runtime-identity.ts +50 -8
  45. package/apps/desktop/code/semantic-builder.ts +24 -43
  46. package/apps/desktop/code/source-bundle.ts +23 -6
  47. package/apps/desktop/code/source-typecheck.ts +131 -43
  48. package/apps/desktop/code/worker-client.ts +14 -2
  49. package/apps/desktop/contracts/cinema.ts +736 -0
  50. package/apps/desktop/contracts/index.ts +1 -0
  51. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  52. package/apps/desktop/contracts/spatial-world.ts +26 -3
  53. package/apps/desktop/core/cinema-plan.ts +1320 -0
  54. package/apps/desktop/core/index.ts +1 -0
  55. package/apps/desktop/core/music-analysis.ts +124 -1
  56. package/apps/desktop/core/storage.ts +75 -0
  57. package/apps/desktop/dist/cli/main.js +737 -291
  58. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  59. package/apps/desktop/html-overlay/index.ts +9 -0
  60. package/apps/desktop/html-overlay/runtime.ts +2 -0
  61. package/apps/desktop/html-overlay/scene.ts +29 -0
  62. package/apps/desktop/html-overlay/spatial.ts +619 -57
  63. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  64. package/apps/desktop/workflows/index.ts +6 -0
  65. package/dist/cli.js +32 -434
  66. package/dist/code/advanced.js +1 -269
  67. package/dist/code/index.js +16 -2239
  68. package/dist/generate.js +1 -23
  69. package/dist/host-resources.js +1 -25
  70. package/dist/index-0bbesn4m.js +35 -0
  71. package/dist/index-296qhpf2.js +3 -0
  72. package/dist/index-3cxf2c58.js +3 -0
  73. package/dist/index-77fjfg9f.js +3 -0
  74. package/dist/index-80a36bc0.js +3 -0
  75. package/dist/index-a3vvs3rf.js +3 -0
  76. package/dist/index-b3geqczn.js +5 -0
  77. package/dist/index-cgpkyjqa.js +3 -0
  78. package/dist/index-f0ptv8dm.js +7 -0
  79. package/dist/index-grk47tgs.js +3 -0
  80. package/dist/index-qry58nj2.js +10 -0
  81. package/dist/index.js +1 -240
  82. package/dist/operations.js +1 -28
  83. package/dist/skill-install-gx7afxsd.js +2 -0
  84. package/dist/vectorize/worker.js +1 -141
  85. package/dist/workflow.js +1 -15
  86. package/docs/README.md +5 -1
  87. package/docs/studio.md +1 -1
  88. package/package.json +11 -24
  89. package/skills/slopcamera/SKILL.md +14 -1
  90. package/skills/slopcamera/references/diagrams.md +5 -14
  91. package/skills/slopcamera/references/directed-scenes.md +22 -2
  92. package/skills/slopcamera/references/gateway-media.md +15 -1
  93. package/skills/slopcamera/references/image-galleries.md +166 -0
  94. package/skills/slopcamera/references/install.md +2 -2
  95. package/skills/slopcamera/references/music-video.md +23 -3
  96. package/skills/slopcamera/references/parametric-design.md +14 -0
  97. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  98. package/skills/slopcamera/references/scene-building.md +32 -0
  99. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  100. package/skills/slopcamera/references/support.md +25 -0
  101. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  102. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  103. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  104. package/src/capability-manifest.ts +361 -0
  105. package/src/cli.ts +250 -102
  106. package/src/code/canonical-json.ts +9 -0
  107. package/src/generate.ts +2 -2
  108. package/src/icon.ts +1111 -0
  109. package/src/image-gallery.ts +1049 -0
  110. package/src/index.ts +11 -23
  111. package/src/mcp/boundary.ts +11 -11
  112. package/src/mcp/index.ts +3 -0
  113. package/src/mcp/server.ts +2 -1
  114. package/src/mcp/tools.ts +1642 -130
  115. package/src/mcp/types.ts +19 -0
  116. package/src/operations.ts +412 -0
  117. package/src/portable-capability-manifest.ts +67 -0
  118. package/src/scene-gallery.ts +184 -0
  119. package/src/spatial-scene/asset-admission.ts +183 -0
  120. package/src/spatial-scene/audit-rendered.ts +692 -0
  121. package/src/spatial-scene/audit.ts +617 -0
  122. package/src/spatial-scene/behavior-audit.ts +252 -0
  123. package/src/spatial-scene/behavior-bake.ts +233 -0
  124. package/src/spatial-scene/behavior-fns.ts +585 -0
  125. package/src/spatial-scene/behavior-gallery.ts +109 -0
  126. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  127. package/src/spatial-scene/behavior-trace.ts +325 -0
  128. package/src/spatial-scene/behavior.ts +618 -0
  129. package/src/spatial-scene/build.ts +632 -0
  130. package/src/spatial-scene/camera-rig.ts +125 -0
  131. package/src/spatial-scene/camera-track.ts +4 -2
  132. package/src/spatial-scene/character.ts +124 -0
  133. package/src/spatial-scene/contracts.ts +75 -7
  134. package/src/spatial-scene/design-templates.ts +351 -0
  135. package/src/spatial-scene/design.ts +327 -0
  136. package/src/spatial-scene/direction-compile.ts +584 -0
  137. package/src/spatial-scene/direction.ts +122 -0
  138. package/src/spatial-scene/effects.ts +149 -0
  139. package/src/spatial-scene/evaluate.ts +109 -29
  140. package/src/spatial-scene/gallery.ts +277 -0
  141. package/src/spatial-scene/generate.ts +239 -0
  142. package/src/spatial-scene/geometry-native.ts +165 -0
  143. package/src/spatial-scene/geometry.ts +1216 -0
  144. package/src/spatial-scene/gltf.ts +600 -77
  145. package/src/spatial-scene/identity.ts +55 -2
  146. package/src/spatial-scene/index.ts +36 -0
  147. package/src/spatial-scene/inspect.ts +35 -6
  148. package/src/spatial-scene/material-lighting.ts +609 -0
  149. package/src/spatial-scene/math.ts +21 -4
  150. package/src/spatial-scene/motion-evidence.ts +88 -0
  151. package/src/spatial-scene/parametric.ts +765 -0
  152. package/src/spatial-scene/particle-preparation.ts +183 -0
  153. package/src/spatial-scene/particle.ts +195 -0
  154. package/src/spatial-scene/patch.ts +109 -15
  155. package/src/spatial-scene/performance.ts +1613 -0
  156. package/src/spatial-scene/probe.ts +137 -0
  157. package/src/spatial-scene/recipe-pack.ts +86 -0
  158. package/src/spatial-scene/render-effects.ts +240 -0
  159. package/src/spatial-scene/review.ts +639 -0
  160. package/src/spatial-scene/simulation-bake.ts +206 -0
  161. package/src/spatial-scene/simulation.ts +187 -0
  162. package/src/spatial-scene/solve.ts +366 -0
  163. package/src/spatial-scene/temporal-audit.ts +354 -0
  164. package/src/support-completion.ts +16 -0
  165. package/src/support.ts +42 -0
  166. package/src/version.ts +1 -1
  167. package/apps/desktop/application/operations/recording/index.ts +0 -5
  168. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  169. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  170. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  171. package/apps/desktop/application/operations/recording/start.ts +0 -169
  172. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  173. package/apps/desktop/capture/protocol.ts +0 -689
  174. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  175. package/apps/desktop/cli/recording-controller.ts +0 -1567
  176. package/apps/desktop/cli/recording-daemon.ts +0 -931
  177. package/dist/index-42zsesc1.js +0 -2257
  178. package/dist/index-7308egqr.js +0 -48
  179. package/dist/index-8txs6fkn.js +0 -712
  180. package/dist/index-mcy8z0br.js +0 -192
  181. package/dist/index-p63wavx0.js +0 -2029
  182. package/dist/index-r7gdhmsp.js +0 -424
  183. package/dist/index-rvghzt7b.js +0 -2013
  184. package/dist/index-sh6xbav6.js +0 -1219
  185. package/dist/index-z1w83f81.js +0 -4
  186. package/dist/index-z7239b4h.js +0 -2363
  187. package/dist/skill-install-dh0ntqba.js +0 -10
  188. package/src/desktop.ts +0 -253
@@ -0,0 +1,122 @@
1
+ import { z } from "zod"
2
+
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import { SpatialDigestSchema, SpatialTimeUsSchema } from "./contracts.js"
5
+ import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
6
+
7
+ /**
8
+ * Strict agent direction documents for Phase 8.
9
+ * Every proposal is marked and never auto-applied; it emits inspectable
10
+ * scene/cinema patches and receipts for human or agent review.
11
+ */
12
+
13
+ export const SPATIAL_DIRECTION_LIMITS = Object.freeze({
14
+ actions: 256,
15
+ beats: 64,
16
+ coverage: 64,
17
+ durationUs: 3_600_000_000,
18
+ looks: 32,
19
+ })
20
+
21
+ const directionId = z.string().min(1).max(64).regex(/^[a-z][a-z0-9_-]*$/u)
22
+ const referenceId = z.string().min(1).max(128).regex(/^[a-z][a-z0-9_-]*$/u)
23
+ const intervalShape = {
24
+ endUs: SpatialTimeUsSchema.max(SPATIAL_DIRECTION_LIMITS.durationUs),
25
+ startUs: SpatialTimeUsSchema.max(SPATIAL_DIRECTION_LIMITS.durationUs),
26
+ }
27
+
28
+ export const SpatialDramaticBeatSchema = z.strictObject({
29
+ id: directionId,
30
+ ...intervalShape,
31
+ intent: z.string().trim().min(1).max(256),
32
+ emotion: z.string().trim().min(1).max(64),
33
+ verified: z.boolean().default(false),
34
+ })
35
+
36
+ export const SpatialCharacterActionSchema = z.strictObject({
37
+ id: directionId,
38
+ characterId: referenceId,
39
+ ...intervalShape,
40
+ action: z.enum(["idle", "walk", "run", "turn", "gesture", "interact", "morph"]),
41
+ targetId: referenceId.optional(),
42
+ verified: z.boolean().default(false),
43
+ })
44
+
45
+ export const SpatialCameraCoverageSchema = z.strictObject({
46
+ id: directionId,
47
+ ...intervalShape,
48
+ rigKind: z.enum(["chase", "crane", "dolly", "handheld", "orbit", "rail", "target-tracking", "tripod"]),
49
+ framing: z.enum(["extreme-close-up", "close-up", "medium-close-up", "medium", "medium-wide", "wide", "extreme-wide", "over-shoulder", "insert"]),
50
+ screenDirection: z.enum(["left", "right", "neutral"]).optional(),
51
+ subjectId: referenceId.optional(),
52
+ verified: z.boolean().default(false),
53
+ })
54
+
55
+ export const SpatialLookIntentSchema = z.strictObject({
56
+ id: directionId,
57
+ ...intervalShape,
58
+ lighting: z.string().trim().min(1).max(128),
59
+ atmosphere: z.string().trim().min(1).max(128),
60
+ verified: z.boolean().default(false),
61
+ })
62
+
63
+ export const SpatialDirectionSchema = z.strictObject({
64
+ kind: z.literal("slopcamera.spatial-direction"),
65
+ schemaVersion: z.literal(1),
66
+ entityId: referenceId,
67
+ projectDigest: SpatialDigestSchema,
68
+ beats: z.array(SpatialDramaticBeatSchema).max(SPATIAL_DIRECTION_LIMITS.beats),
69
+ actions: z.array(SpatialCharacterActionSchema).max(SPATIAL_DIRECTION_LIMITS.actions),
70
+ coverage: z.array(SpatialCameraCoverageSchema).max(SPATIAL_DIRECTION_LIMITS.coverage),
71
+ looks: z.array(SpatialLookIntentSchema).max(SPATIAL_DIRECTION_LIMITS.looks),
72
+ }).superRefine((direction, context) => {
73
+ const ids = new Set<string>()
74
+ for (const [collectionName, values] of [
75
+ ["beats", direction.beats],
76
+ ["actions", direction.actions],
77
+ ["coverage", direction.coverage],
78
+ ["looks", direction.looks],
79
+ ] as const) {
80
+ let previousStartUs = -1
81
+ let previousId = ""
82
+ for (const [index, value] of values.entries()) {
83
+ if (value.endUs <= value.startUs) {
84
+ context.addIssue({ code: "custom", path: [collectionName, index, "endUs"], message: "Direction intervals must have positive duration." })
85
+ }
86
+ if (ids.has(value.id)) {
87
+ context.addIssue({ code: "custom", path: [collectionName, index, "id"], message: `Direction id ${value.id} must be globally unique.` })
88
+ }
89
+ ids.add(value.id)
90
+ if (value.startUs < previousStartUs || value.startUs === previousStartUs && value.id.localeCompare(previousId) <= 0) {
91
+ context.addIssue({ code: "custom", path: [collectionName, index], message: "Direction entries must be ordered by startUs then id." })
92
+ }
93
+ previousStartUs = value.startUs
94
+ previousId = value.id
95
+ }
96
+ }
97
+ for (const [index, action] of direction.actions.entries()) {
98
+ if ((action.action === "interact") !== (action.targetId !== undefined)) {
99
+ context.addIssue({ code: "custom", path: ["actions", index, "targetId"], message: "Only interact actions require a targetId." })
100
+ }
101
+ }
102
+ for (let index = 1; index < direction.coverage.length; index += 1) {
103
+ if (direction.coverage[index]!.startUs < direction.coverage[index - 1]!.endUs) {
104
+ context.addIssue({ code: "custom", path: ["coverage", index], message: "Camera coverage intervals must not overlap." })
105
+ }
106
+ }
107
+ })
108
+
109
+ export type SpatialDirection = Readonly<z.infer<typeof SpatialDirectionSchema>>
110
+
111
+ export function parseSpatialDirection(input: unknown): SpatialDirection {
112
+ try {
113
+ return deepFreezeJson(parseSpatialValue(SpatialDirectionSchema, input, "direction"))
114
+ } catch (error) {
115
+ if (error instanceof SpatialSceneError) throw error
116
+ throw new SpatialSceneError("invalid-data", error instanceof Error ? error.message : String(error), "direction")
117
+ }
118
+ }
119
+
120
+ export function spatialDirectionSha256(direction: SpatialDirection): string {
121
+ return spatialValueSha256(direction)
122
+ }
@@ -0,0 +1,149 @@
1
+ import { z } from "zod"
2
+
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import { SpatialAssetIdSchema } from "./contracts.js"
5
+ import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
6
+
7
+ export const positiveDimension = z.number().finite().positive().max(1_000_000)
8
+ export const unit = z.number().finite().min(0).max(1)
9
+
10
+ /**
11
+ * Bounded post-processing and render-quality contracts for Phase 7.
12
+ * Beauty finishing passes are lowered behind the desktop host; diagnostic
13
+ * AOVs (object-ID, depth, motion) never pass through these operators.
14
+ */
15
+
16
+ export const SPATIAL_EFFECT_LIMITS = Object.freeze({
17
+ dust: 500_000,
18
+ embers: 1_000_000,
19
+ luts: 4,
20
+ outputBytes: 8_000_000_000,
21
+ postProcessStack: 8,
22
+ previewParticles: 100_000,
23
+ rain: 100_000,
24
+ renderPixels: 67_108_864,
25
+ simulationFrames: 10_000,
26
+ simulationSteps: 10_000,
27
+ texturePixels: 268_435_456,
28
+ })
29
+
30
+ const boundedPixels = z.number().int().safe().positive()
31
+
32
+ export const SpatialRenderQualitySchema = z.strictObject({
33
+ outputBytes: z.number().int().safe().positive().max(SPATIAL_EFFECT_LIMITS.outputBytes),
34
+ particleCount: z.number().int().safe().min(0).max(SPATIAL_EFFECT_LIMITS.embers),
35
+ pixelBudget: boundedPixels.max(SPATIAL_EFFECT_LIMITS.renderPixels),
36
+ simulationSteps: z.number().int().safe().min(0).max(SPATIAL_EFFECT_LIMITS.simulationSteps),
37
+ texturePixelBudget: boundedPixels.max(SPATIAL_EFFECT_LIMITS.texturePixels),
38
+ tier: z.enum(["preview", "final"]),
39
+ })
40
+
41
+ export const SpatialBloomSchema = z.strictObject({
42
+ intensity: unit,
43
+ kind: z.literal("bloom"),
44
+ radius: z.number().finite().min(0).max(64),
45
+ threshold: unit,
46
+ })
47
+
48
+ export const SpatialDepthOfFieldSchema = z.strictObject({
49
+ aperture: z.number().finite().positive().max(256),
50
+ focalLength: z.number().finite().positive().max(10_000),
51
+ focusDistance: positiveDimension,
52
+ kind: z.literal("depth-of-field"),
53
+ })
54
+
55
+ export const SpatialMotionBlurSchema = z.strictObject({
56
+ kind: z.literal("motion-blur"),
57
+ samples: z.number().int().min(1).max(64),
58
+ shutterAngle: z.number().finite().min(0).max(360),
59
+ })
60
+
61
+ export const SpatialToneMapSchema = z.strictObject({
62
+ exposure: z.number().finite().min(-20).max(20),
63
+ kind: z.literal("tone-map"),
64
+ whitePoint: z.number().finite().positive().max(1_000_000),
65
+ })
66
+
67
+ export const SpatialVignetteSchema = z.strictObject({
68
+ intensity: unit,
69
+ kind: z.literal("vignette"),
70
+ radius: unit,
71
+ })
72
+
73
+ export const SpatialChromaticAberrationSchema = z.strictObject({
74
+ kind: z.literal("chromatic-aberration"),
75
+ offsetPixels: z.number().finite().min(0).max(64),
76
+ radialFalloff: unit,
77
+ })
78
+
79
+ export const SpatialGrainSchema = z.strictObject({
80
+ intensity: unit,
81
+ kind: z.literal("grain"),
82
+ seed: z.number().int().min(0).max(2_147_483_647),
83
+ })
84
+
85
+ export const SpatialFlareSchema = z.strictObject({
86
+ ghosts: z.number().int().min(0).max(16),
87
+ haloWidth: unit,
88
+ intensity: unit,
89
+ kind: z.literal("flare"),
90
+ threshold: unit,
91
+ })
92
+
93
+ export const SpatialLutGradeSchema = z.strictObject({
94
+ assetId: SpatialAssetIdSchema,
95
+ intensity: unit,
96
+ kind: z.literal("lut-grade"),
97
+ })
98
+
99
+ export const SpatialPostProcessStepSchema = z.discriminatedUnion("kind", [
100
+ SpatialBloomSchema,
101
+ SpatialDepthOfFieldSchema,
102
+ SpatialMotionBlurSchema,
103
+ SpatialToneMapSchema,
104
+ SpatialVignetteSchema,
105
+ SpatialChromaticAberrationSchema,
106
+ SpatialGrainSchema,
107
+ SpatialFlareSchema,
108
+ SpatialLutGradeSchema,
109
+ ])
110
+
111
+ export const SpatialPostProcessStackSchema = z.strictObject({
112
+ kind: z.literal("slopcamera.spatial-post-process"),
113
+ schemaVersion: z.literal(1),
114
+ steps: z.array(SpatialPostProcessStepSchema).min(1).max(SPATIAL_EFFECT_LIMITS.postProcessStack),
115
+ }).superRefine((stack, context) => {
116
+ if (stack.steps.filter(step => step.kind === "lut-grade").length > SPATIAL_EFFECT_LIMITS.luts) {
117
+ context.addIssue({ code: "custom", path: ["steps"], message: `Post-process stacks support at most ${SPATIAL_EFFECT_LIMITS.luts} LUT grades.` })
118
+ }
119
+ })
120
+
121
+ export const SpatialRenderPlanSchema = z.strictObject({
122
+ kind: z.literal("slopcamera.spatial-render-plan"),
123
+ postProcess: SpatialPostProcessStackSchema.optional(),
124
+ quality: SpatialRenderQualitySchema,
125
+ schemaVersion: z.literal(1),
126
+ })
127
+
128
+ export type SpatialRenderQuality = Readonly<z.infer<typeof SpatialRenderQualitySchema>>
129
+ export type SpatialPostProcessStack = Readonly<z.infer<typeof SpatialPostProcessStackSchema>>
130
+ export type SpatialRenderPlan = Readonly<z.infer<typeof SpatialRenderPlanSchema>>
131
+
132
+ export function parseSpatialRenderPlan(input: unknown): SpatialRenderPlan {
133
+ const plan = parseSpatialValue(SpatialRenderPlanSchema, input, "render plan")
134
+ if (plan.quality.tier === "preview" && plan.quality.particleCount > SPATIAL_EFFECT_LIMITS.previewParticles) {
135
+ throw new SpatialSceneError("invalid-data", `Preview tier cannot request more than ${SPATIAL_EFFECT_LIMITS.previewParticles.toLocaleString("en-US")} particles.`, "render-plan.quality.particleCount")
136
+ }
137
+ if (plan.quality.outputBytes < plan.quality.particleCount * 64) {
138
+ throw new SpatialSceneError("invalid-data", "Output byte budget is too small for declared particle count.", "render-plan.quality.outputBytes")
139
+ }
140
+ return deepFreezeJson(plan)
141
+ }
142
+
143
+ export function spatialRenderPlanAssetIds(plan: SpatialRenderPlan): readonly string[] {
144
+ return Object.freeze([...new Set(plan.postProcess?.steps.flatMap(step => step.kind === "lut-grade" ? [step.assetId] : []) ?? [])].sort())
145
+ }
146
+
147
+ export function spatialRenderPlanSha256(plan: SpatialRenderPlan): string {
148
+ return spatialValueSha256(plan)
149
+ }
@@ -4,7 +4,7 @@ import {
4
4
  EvaluatedSpatialSceneSchema, SPATIAL_SCENE_LIMITS, SpatialCameraIdSchema, SpatialOverrideSchema, SpatialPoseSchema,
5
5
  SpatialShotV1Schema, SpatialTimeUsSchema,
6
6
  type EvaluatedSpatialScene, type SpatialAnimation, type SpatialCamera, type SpatialEntity,
7
- type SpatialOverride, type SpatialPose, type SpatialShotV1,
7
+ type SpatialOverride, type SpatialPose, type SpatialSceneV1, type SpatialShotV1,
8
8
  } from "./contracts.js"
9
9
  import {
10
10
  parseSpatialScene, parseSpatialValue, sortSpatialBy, spatialAssetClosureDigests,
@@ -19,6 +19,38 @@ export interface EvaluateSpatialSceneOptions {
19
19
  readonly cameraPoseOverride?: SpatialPose
20
20
  }
21
21
 
22
+ /**
23
+ * Immutable per-scene evaluation context: every index, ordering, digest and
24
+ * merged-override value that absolute-time evaluation re-derives identically
25
+ * for each sample. Built once by `createSpatialEvaluationContext`, then shared
26
+ * by any number of `evaluateSpatialSceneInContext` calls — audits, renders and
27
+ * camera tracks sample one scene repeatedly, so the parse and indexing happen
28
+ * once per scene rather than once per sample.
29
+ *
30
+ * The context is the optional memo seam of `evaluateSpatialScene`; both public
31
+ * signatures and snapshot bytes are unchanged whether or not a caller builds
32
+ * one. Treat it as opaque and immutable: it shares the parsed scene's frozen
33
+ * values and must never be mutated between calls.
34
+ */
35
+ export interface SpatialEvaluationContext {
36
+ /** The parsed, normalized, deep-frozen scene this context is bound to. */
37
+ readonly scene: SpatialSceneV1
38
+ readonly sceneSha256: string
39
+ readonly entitiesById: ReadonlyMap<string, SpatialEntity>
40
+ readonly camerasById: ReadonlyMap<string, SpatialCamera>
41
+ /** Entity ids in parent-before-child order; hierarchy is evaluation-invariant. */
42
+ readonly entityOrder: readonly string[]
43
+ /** Effective authored visibility; channels and overrides never write `visible`. */
44
+ readonly visibilityById: ReadonlyMap<string, boolean>
45
+ /** Animation channels targeting entities, in canonical order. */
46
+ readonly entityChannels: readonly SpatialAnimation[]
47
+ /** Animation channels targeting cameras, keyed by camera id. */
48
+ readonly cameraChannelsById: ReadonlyMap<string, readonly SpatialAnimation[]>
49
+ /** Scene-level overrides already merged and validated by the scene parse. */
50
+ readonly baseOverrides: readonly SpatialOverride[]
51
+ readonly assetDigests: Readonly<Record<string, string>>
52
+ }
53
+
22
54
  const EvaluatedOptionsSchema = z.strictObject({
23
55
  timeUs: SpatialTimeUsSchema, cameraId: SpatialCameraIdSchema,
24
56
  overrides: z.array(SpatialOverrideSchema).max(SPATIAL_SCENE_LIMITS.entities).optional(),
@@ -74,20 +106,62 @@ function sampleChannel(channel: SpatialAnimation, timeUs: number): number | read
74
106
  }
75
107
 
76
108
  export function validateSpatialShot(sceneInput: unknown, shotInput: unknown): SpatialShotV1 {
77
- const scene = parseSpatialScene(sceneInput)
109
+ const context = createSpatialEvaluationContext(sceneInput)
110
+ const scene = context.scene
78
111
  const shot = parseSpatialValue(SpatialShotV1Schema, shotInput, "shot")
79
- if (shot.sceneSha256 !== spatialValueSha256(scene)) throw new SpatialSceneError("conflict", "Shot pins another scene revision.", "shot.sceneSha256")
80
- if (!scene.cameras.some(camera => camera.cameraId === shot.cameraId)) throw new SpatialSceneError("invalid-data", "Shot camera is absent from its scene.", "shot.cameraId")
112
+ if (shot.sceneSha256 !== context.sceneSha256) throw new SpatialSceneError("conflict", "Shot pins another scene revision.", "shot.sceneSha256")
113
+ if (!context.camerasById.has(shot.cameraId)) throw new SpatialSceneError("invalid-data", "Shot camera is absent from its scene.", "shot.cameraId")
81
114
  if (shot.sceneStartUs >= scene.durationUs) throw new SpatialSceneError("invalid-data", "Shot scene start must precede scene duration.", "shot.sceneStartUs")
82
115
  if (shot.playback === "once" && shot.sceneStartUs + shot.range.endUs - shot.range.startUs > scene.durationUs) throw new SpatialSceneError("invalid-data", "Once playback exceeds scene duration.", "shot.range")
83
116
  validateSpatialOverrides(scene, mergeSpatialOverrides(scene.overrides, shot.overrides))
84
- if (shot.cameraPoseOverride && scene.animations.some(channel => channel.targetId === shot.cameraId)) throw new SpatialSceneError("conflict", "Camera animation and shot pose override both own camera pose.", "shot.cameraPoseOverride")
117
+ if (shot.cameraPoseOverride && context.cameraChannelsById.has(shot.cameraId)) throw new SpatialSceneError("conflict", "Camera animation and shot pose override both own camera pose.", "shot.cameraPoseOverride")
85
118
  return deepFreezeJson(shot)
86
119
  }
87
120
 
88
- /** Absolute time evaluation produces one immutable snapshot; calls share no mutable state. */
89
- export function evaluateSpatialScene(sceneInput: unknown, options: EvaluateSpatialSceneOptions): EvaluatedSpatialScene {
121
+ /**
122
+ * Parses and indexes a scene once so repeated absolute-time evaluations share
123
+ * the constant work: entity and camera lookup maps, the parent-before-child
124
+ * entity order, effective authored visibility, per-target channel lists, merged
125
+ * scene-level overrides, the asset closure digests and the scene digest.
126
+ * The result is immutable; callers never mutate it and may share it across any
127
+ * number of `evaluateSpatialSceneInContext` calls and audit runs.
128
+ */
129
+ export function createSpatialEvaluationContext(sceneInput: unknown): SpatialEvaluationContext {
90
130
  const scene = parseSpatialScene(sceneInput)
131
+ const entitiesById = new Map(scene.entities.map(entity => [entity.entityId, entity]))
132
+ const camerasById = new Map(scene.cameras.map(camera => [camera.cameraId, camera]))
133
+ const entityOrder = spatialTopologicalIds(new Map(scene.entities.map(entity => [entity.entityId, entity.parentId === null ? [] : [entity.parentId]])), "entity hierarchy")
134
+ const visibilityById = new Map<string, boolean>()
135
+ for (const id of entityOrder) {
136
+ const entity = entitiesById.get(id)!
137
+ visibilityById.set(id, entity.visible && (entity.parentId === null || visibilityById.get(entity.parentId) === true))
138
+ }
139
+ const entityChannels: SpatialAnimation[] = []
140
+ const cameraChannelsById = new Map<string, SpatialAnimation[]>()
141
+ for (const channel of scene.animations) {
142
+ if (entitiesById.has(channel.targetId)) entityChannels.push(channel)
143
+ else {
144
+ const list = cameraChannelsById.get(channel.targetId) ?? []
145
+ list.push(channel)
146
+ cameraChannelsById.set(channel.targetId, list)
147
+ }
148
+ }
149
+ return Object.freeze({
150
+ scene, sceneSha256: spatialValueSha256(scene), entitiesById, camerasById,
151
+ entityOrder, visibilityById, entityChannels,
152
+ cameraChannelsById, baseOverrides: mergeSpatialOverrides(scene.overrides, []),
153
+ assetDigests: spatialAssetClosureDigests(scene.assets),
154
+ })
155
+ }
156
+
157
+ /**
158
+ * Evaluates one immutable snapshot against a shared evaluation context. This is
159
+ * the hot path `evaluateSpatialScene` delegates to after building the context;
160
+ * per-sample work is channel sampling, transform composition and the emitted
161
+ * snapshot's own identity, schema parse and freeze.
162
+ */
163
+ export function evaluateSpatialSceneInContext(context: SpatialEvaluationContext, options: EvaluateSpatialSceneOptions): EvaluatedSpatialScene {
164
+ const scene = context.scene
91
165
  // Capture the complete options object before inspecting optional properties.
92
166
  const capturedOptions = parseSpatialValue(
93
167
  // Explicit object schema also rejects unsupported simulation or executable fields.
@@ -95,29 +169,31 @@ export function evaluateSpatialScene(sceneInput: unknown, options: EvaluateSpati
95
169
  )
96
170
  const timeUs = capturedOptions.timeUs
97
171
  if (timeUs > scene.durationUs) throw new SpatialSceneError("invalid-data", "Evaluation time exceeds scene duration.", "timeUs")
98
- let camera = scene.cameras.find(item => item.cameraId === capturedOptions.cameraId)
172
+ let camera = context.camerasById.get(capturedOptions.cameraId)
99
173
  if (!camera) throw new SpatialSceneError("not-found", `Unknown camera ${capturedOptions.cameraId}.`, "cameraId")
100
- const overrides = mergeSpatialOverrides(scene.overrides, capturedOptions.overrides ?? [])
101
- validateSpatialOverrides(scene, overrides)
102
- if (capturedOptions.cameraPoseOverride && scene.animations.some(channel => channel.targetId === camera!.cameraId)) throw new SpatialSceneError("conflict", "Camera animation conflicts with camera pose override.", "cameraPoseOverride")
103
- const entities = new Map(scene.entities.map(entity => [entity.entityId, entity]))
104
- for (const channel of scene.animations) {
174
+ // Without per-call overrides the merged scene-level set is a context constant
175
+ // already validated by the scene parse; per-call overrides re-merge and
176
+ // re-validate against the parsed scene exactly as a standalone call would.
177
+ const overrides = capturedOptions.overrides === undefined ? context.baseOverrides : mergeSpatialOverrides(scene.overrides, capturedOptions.overrides)
178
+ if (capturedOptions.overrides !== undefined) validateSpatialOverrides(scene, overrides)
179
+ if (capturedOptions.cameraPoseOverride && context.cameraChannelsById.has(camera.cameraId)) throw new SpatialSceneError("conflict", "Camera animation conflicts with camera pose override.", "cameraPoseOverride")
180
+ const entities = new Map(context.entitiesById)
181
+ // Entity and camera channels write disjoint slots, so applying the entity
182
+ // subset first and then this camera's channels matches interleaved order.
183
+ for (const channel of context.entityChannels) {
105
184
  const value = sampleChannel(channel, timeUs)
106
- if (channel.targetId === camera.cameraId) {
107
- camera = { ...camera, pose: { ...camera.pose, [channel.property]: value } } as SpatialCamera
108
- } else {
109
- const entity = entities.get(channel.targetId)
110
- if (!entity) continue // Other cameras do not affect the requested view.
111
- if (channel.property === "opacity") entities.set(entity.entityId, applySpatialEntityOverride(entity, { entityId: entity.entityId, property: "opacity", value: value as number }))
112
- else entities.set(entity.entityId, { ...entity, transform: { ...entity.transform, [channel.property]: value } } as SpatialEntity)
113
- }
185
+ const entity = entities.get(channel.targetId)!
186
+ if (channel.property === "opacity") entities.set(entity.entityId, applySpatialEntityOverride(entity, { entityId: entity.entityId, property: "opacity", value: value as number }))
187
+ else entities.set(entity.entityId, { ...entity, transform: { ...entity.transform, [channel.property]: value } } as SpatialEntity)
188
+ }
189
+ for (const channel of context.cameraChannelsById.get(camera.cameraId) ?? []) {
190
+ const value = sampleChannel(channel, timeUs)
191
+ camera = { ...camera, pose: { ...camera.pose, [channel.property]: value } } as SpatialCamera
114
192
  }
115
193
  for (const override of overrides) entities.set(override.entityId, applySpatialEntityOverride(entities.get(override.entityId)!, override))
116
194
  if (capturedOptions.cameraPoseOverride) camera = { ...camera, pose: capturedOptions.cameraPoseOverride }
117
195
  const matrices = new Map<string, Mat4>()
118
- const visibility = new Map<string, boolean>()
119
- const order = spatialTopologicalIds(new Map(scene.entities.map(entity => [entity.entityId, entity.parentId === null ? [] : [entity.parentId]])), "entity hierarchy")
120
- for (const id of order) {
196
+ for (const id of context.entityOrder) {
121
197
  const entity = entities.get(id)!
122
198
  try {
123
199
  const local = composeTransform(entity.transform)
@@ -125,21 +201,25 @@ export function evaluateSpatialScene(sceneInput: unknown, options: EvaluateSpati
125
201
  } catch (error) {
126
202
  throw new SpatialSceneError("invalid-data", error instanceof Error ? error.message : "Invalid evaluated transform.", `entities.${id}.transform`)
127
203
  }
128
- visibility.set(id, entity.visible && (entity.parentId === null || visibility.get(entity.parentId) === true))
129
204
  }
130
205
  const evaluated = scene.entities.map((source, index) => ({
131
206
  entity: entities.get(source.entityId)!, worldMatrix: matrices.get(source.entityId)!,
132
- visible: visibility.get(source.entityId)!, selectionId: index + 1,
207
+ visible: context.visibilityById.get(source.entityId)!, selectionId: index + 1,
133
208
  }))
134
209
  // The state identity excludes camera choice, camera pose, asset locator and provenance.
135
- const assetDigests = spatialAssetClosureDigests(scene.assets)
136
- const stateSha256 = spatialStateValueSha256({ domain: "slopcamera.spatial-state.v1", timeUs, entities: evaluated, assetDigests })
210
+ const stateSha256 = spatialStateValueSha256({ domain: "slopcamera.spatial-state.v1", timeUs, entities: evaluated, assetDigests: context.assetDigests })
137
211
  const viewSha256 = spatialValueSha256({ domain: "slopcamera.spatial-view.v1", stateSha256, camera })
138
212
  const result = EvaluatedSpatialSceneSchema.parse({
139
- kind: "slopcamera.spatial-snapshot", schemaVersion: 1, sceneSha256: spatialValueSha256(scene),
213
+ kind: "slopcamera.spatial-snapshot", schemaVersion: 1, sceneSha256: context.sceneSha256,
140
214
  stateSha256, viewSha256, timeUs, camera,
141
215
  entities: evaluated.map(item => ({ ...item, visible: item.visible && (item.entity.placement.kind === "world" || item.entity.placement.cameraId === camera.cameraId) })),
142
216
  assets: scene.assets,
217
+ ...(scene.fog === undefined ? {} : { fog: scene.fog }),
143
218
  })
144
219
  return deepFreezeJson(result)
145
220
  }
221
+
222
+ /** Absolute time evaluation produces one immutable snapshot; calls share no mutable state. */
223
+ export function evaluateSpatialScene(sceneInput: unknown, options: EvaluateSpatialSceneOptions): EvaluatedSpatialScene {
224
+ return evaluateSpatialSceneInContext(createSpatialEvaluationContext(sceneInput), options)
225
+ }