@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
@@ -12,18 +12,23 @@ import {
12
12
  SpatialTimeUsSchema, SpatialShotV1Schema, type SpatialSceneV1,
13
13
  } from "../../../src/spatial-scene/contracts";
14
14
  import { evaluateSpatialScene } from "../../../src/spatial-scene/evaluate";
15
+ import { parseSpatialMotionEvidence, spatialMotionEvidenceSha256, SPATIAL_MOTION_EVIDENCE_LIMITS } from "../../../src/spatial-scene/motion-evidence";
16
+ import { SpatialRenderEffectsBindingSchema, spatialRenderEffectsAssetIds } from "../../../src/spatial-scene/render-effects";
17
+ import { prepareSpatialParticleInstances } from "../../../src/spatial-scene/particle-preparation";
18
+ import type { SpatialParticleSystem } from "../../../src/spatial-scene/particle";
15
19
  import { parseSpatialScene, parseSpatialValue, spatialAssetClosureDigests, spatialSceneSha256 } from "../../../src/spatial-scene/identity";
16
20
  import { reduceSpatialFrameRate, spatialFrameCount, spatialFrameSample, spatialOutputDuration, type SpatialRational } from "../../../src/spatial-scene/time";
17
21
  import { canonicalJson, canonicalJsonSha256 } from "../core/canonical-json";
18
22
  import { createNodeSpatialDurability, type SpatialDurabilityPort } from "../core/spatial-durability";
19
23
  import { SpatialShotRenderClockSchema, spatialShotSceneTime } from "../core/spatial-shot-clock";
20
24
  import { createSpatialOverlayBatch, SpatialRenderModeSchema, SPATIAL_OVERLAY_LIMITS, type SpatialOverlayBatchInput, type PreparedSpatialAsset } from "../html-overlay/spatial";
21
- import { HTML_OVERLAY_MAX_HTML_BYTES } from "../html-overlay/contracts";
25
+ import { HTML_OVERLAY_MAX_HTML_BYTES, HTML_OVERLAY_MAX_RESOURCES, HTML_OVERLAY_MAX_TOTAL_RESOURCE_BYTES } from "../html-overlay/contracts";
22
26
  import { assertHtmlOverlayGpuEvidenceProfile, HtmlOverlayExecutionProfileSchema, HtmlOverlayGpuEvidenceSchema, type HtmlOverlayExecutionProfile, type HtmlOverlayGpuEvidence } from "../html-overlay/execution-profile";
23
27
  import { exactCapabilityByName } from "./capability-binding";
24
28
  import { ApplicationError } from "./errors";
25
29
  import { bindHtmlOverlayBrowserRuntime, HtmlOverlayBrowserRuntimeBindingSchema, type HtmlOverlayBrowserRuntimeBinding } from "./html-overlay-browser-runtime";
26
30
  import { createHtmlOverlayExecutionBundle, HtmlOverlayExecutionIntegritySchema } from "./html-overlay-integrity";
31
+ import type { BoundHtmlOverlayResource } from "./html-overlay-renderer";
27
32
  import type { OperationExecutionContext } from "./operation";
28
33
  import { assertMediaCapabilities, bindExpectedMediaCapabilities, MediaCapabilityBindingsSchema, mediaCapabilityCommand, mediaCapabilityRunner, type MediaCapabilityName } from "./operations/media/capabilities";
29
34
  import { AbortBoundApplicationRunner, createMediaOperationWorkspace, MediaArtifactReferenceSchema, publishContentAddressedMedia, publishContentAddressedReceipt, type MediaArtifactReference } from "./operations/media/shared";
@@ -41,9 +46,14 @@ export const SPATIAL_RENDER_LIMITS = Object.freeze({
41
46
  });
42
47
 
43
48
  export function spatialBatchSerializationLimit(error: unknown): boolean {
44
- if (error instanceof z.ZodError) return error.issues.length > 0 && error.issues.every(issue => issue.path.length === 1 && issue.path[0] === "html"
45
- && (issue.code === "too_big" && issue.maximum === HTML_OVERLAY_MAX_HTML_BYTES
46
- || issue.code === "custom" && issue.message === `HTML overlay documents may not exceed ${HTML_OVERLAY_MAX_HTML_BYTES} UTF-8 bytes.`));
49
+ if (error instanceof z.ZodError) return error.issues.length > 0 && error.issues.every(issue => (
50
+ issue.path.length === 1 && issue.path[0] === "html"
51
+ && (issue.code === "too_big" && issue.maximum === HTML_OVERLAY_MAX_HTML_BYTES
52
+ || issue.code === "custom" && issue.message === `HTML overlay documents may not exceed ${HTML_OVERLAY_MAX_HTML_BYTES} UTF-8 bytes.`)
53
+ || issue.path.length === 1 && issue.path[0] === "resources"
54
+ && (issue.code === "too_big" && issue.maximum === HTML_OVERLAY_MAX_RESOURCES
55
+ || issue.code === "custom" && issue.message === `HTML overlay resources exceed ${HTML_OVERLAY_MAX_TOTAL_RESOURCE_BYTES} total bytes.`)
56
+ ));
47
57
  return error instanceof Error && ["Spatial overlay request", "Spatial overlay metadata"].some(name => error.message === `${name} contains more than ${SPATIAL_OVERLAY_LIMITS.requestBytes} bytes.`);
48
58
  }
49
59
 
@@ -51,13 +61,16 @@ export function spatialBatchSerializationLimit(error: unknown): boolean {
51
61
  export function partitionSpatialRenderWindow(input: SpatialOverlayBatchInput) {
52
62
  type Partition = { readonly offset: number; readonly length: number; readonly preparedAssets: readonly PreparedSpatialAsset[]; readonly batch: ReturnType<typeof createSpatialOverlayBatch> };
53
63
  const partitions: Partition[] = [];
64
+ const needsVelocity = input.mode.kind === "motion"
65
+ || input.effects?.document.renderPlan.postProcess?.steps.some(step => step.kind === "motion-blur") === true;
54
66
  const visit = (offset: number, length: number): void => {
55
67
  const snapshots = input.snapshots.slice(offset, offset + length);
56
68
  const times = new Set(snapshots.map(snapshot => snapshot.timeUs));
57
69
  const preparedAssets = input.executionProfile === undefined ? input.preparedAssets ?? []
58
- : (input.preparedAssets ?? []).filter(asset => asset.kind === "splat" || asset.timeUs === null || times.has(asset.timeUs));
70
+ : (input.preparedAssets ?? []).filter(asset => asset.kind === "splat" || asset.kind === "lut" || asset.timeUs === null || times.has(asset.timeUs));
59
71
  try {
60
- const batch = createSpatialOverlayBatch({ ...input, snapshots, preparedAssets });
72
+ const batch = createSpatialOverlayBatch({ ...input, snapshots, preparedAssets,
73
+ ...(needsVelocity && offset > 0 ? { previousSnapshot: input.snapshots[offset - 1]! } : {}) });
61
74
  partitions.push({ offset, length, preparedAssets, batch });
62
75
  } catch (error) {
63
76
  if (input.executionProfile === undefined || length <= 1 || !spatialBatchSerializationLimit(error)) throw error;
@@ -72,6 +85,7 @@ export function partitionSpatialRenderWindow(input: SpatialOverlayBatchInput) {
72
85
 
73
86
  export const SpatialRenderRequestSchema = z.strictObject({
74
87
  executionProfile: HtmlOverlayExecutionProfileSchema.optional(),
88
+ effects: SpatialRenderEffectsBindingSchema.optional(),
75
89
  cameraId: SpatialCameraIdSchema,
76
90
  overrides: z.array(SpatialOverrideSchema).max(4_096).optional(),
77
91
  cameraPoseOverride: SpatialPoseSchema.optional(),
@@ -124,7 +138,20 @@ export interface SpatialRenderPlan {
124
138
  readonly outputWidth: number;
125
139
  readonly outputHeight: number;
126
140
  readonly outputDurationUs?: SpatialRational;
127
- readonly costs: { readonly sourceBytes: number; readonly renderPixels: number; readonly pngBytesBound: number; readonly outputBytesBound: number; readonly stagingBytesBound: number };
141
+ readonly costs: {
142
+ readonly sourceBytes: number;
143
+ readonly renderPixels: number;
144
+ readonly renderTargetPixels: number;
145
+ readonly renderTargetBytes: number;
146
+ readonly texturePixels: number;
147
+ readonly particleStates: number;
148
+ readonly particleStateBytes: number;
149
+ readonly particleBufferBytesBound: number;
150
+ readonly simulationSteps: number;
151
+ readonly pngBytesBound: number;
152
+ readonly outputBytesBound: number;
153
+ readonly stagingBytesBound: number;
154
+ };
128
155
  }
129
156
 
130
157
  function rational(numerator: bigint, denominator: bigint): SpatialRational {
@@ -137,6 +164,38 @@ function checked(condition: boolean, message: string): asserts condition {
137
164
  }
138
165
  const sha256 = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
139
166
 
167
+ function particleSurfaceInputs(system: SpatialParticleSystem, preparedAssets: readonly PreparedSpatialAsset[]) {
168
+ const surfaceIds = new Set(system.emitters.flatMap(emitter => emitter.shape.kind === "surface" ? [emitter.shape.assetId] : []));
169
+ return [...surfaceIds].map(assetId => {
170
+ const triangles: number[][][] = [];
171
+ for (const asset of preparedAssets) {
172
+ if (asset.kind !== "geometry" || asset.assetId !== assetId) continue;
173
+ for (const primitive of asset.primitives) {
174
+ const indices = primitive.indices ?? Array.from({ length: primitive.positions.length / 3 }, (_, index) => index);
175
+ for (let index = 0; index < indices.length; index += 3) triangles.push([indices[index]!, indices[index + 1]!, indices[index + 2]!].map(vertex => (
176
+ [primitive.positions[vertex * 3]!, primitive.positions[vertex * 3 + 1]!, primitive.positions[vertex * 3 + 2]!]
177
+ )));
178
+ }
179
+ }
180
+ return { assetId, triangles };
181
+ });
182
+ }
183
+
184
+ function postProcessRenderTargets(kind: string): number {
185
+ switch (kind) {
186
+ case "bloom": return 3;
187
+ case "depth-of-field":
188
+ case "motion-blur": return 2;
189
+ case "flare": return 3;
190
+ case "tone-map":
191
+ case "vignette":
192
+ case "chromatic-aberration":
193
+ case "grain":
194
+ case "lut-grade": return 1;
195
+ default: throw new ApplicationError("invalid-data", `Unsupported post-process step ${kind}.`);
196
+ }
197
+ }
198
+
140
199
  /** Source-clock samples are computed independently in BigInt and quantized exactly once. */
141
200
  export function planSpatialRender(sceneInput: unknown, requestInput: unknown): SpatialRenderPlan {
142
201
  const scene = parseSpatialScene(sceneInput);
@@ -145,8 +204,50 @@ export function planSpatialRender(sceneInput: unknown, requestInput: unknown): S
145
204
  checked(request.executionProfile !== "three-spark-webgl2-hardware-v1" || request.mode.kind === "beauty", "The Three/Spark hardware profile supports beauty rendering only; splat selection and depth semantics are not qualified.");
146
205
  const camera = scene.cameras.find(item => item.cameraId === request.cameraId);
147
206
  checked(camera !== undefined, "Spatial render camera is absent from the scene.");
207
+ const requestMode = request.mode;
208
+ if (requestMode.kind === "motion") {
209
+ const entityId = requestMode.entityId;
210
+ checked(scene.entities.some(entity => entity.entityId === entityId), "Motion target is absent from the scene.");
211
+ checked(request.executionProfile === undefined, "Motion evidence renders through the deterministic snapshot renderer only.");
212
+ }
148
213
  const { width, height } = camera.projection;
149
214
  checked(width <= SPATIAL_RENDER_LIMITS.dimension && height <= SPATIAL_RENDER_LIMITS.dimension, "Spatial render dimensions exceed the renderer profile.");
215
+ const sceneDigest = spatialSceneSha256(scene);
216
+ const effects = request.effects?.document;
217
+ const texturePixels = scene.assets.reduce((sum, asset) => (
218
+ asset.interpretation.kind === "image" || asset.interpretation.kind === "video"
219
+ ? sum + asset.interpretation.width * asset.interpretation.height
220
+ : sum
221
+ ), 0);
222
+ let particleStates = 0, simulationSteps = 0, renderTargetCount = request.mode.kind === "motion" ? 3 : 1;
223
+ if (effects !== undefined) {
224
+ checked(effects.sceneSha256 === sceneDigest, "Render effects belong to a different scene source.");
225
+ const assets = new Map(scene.assets.map(asset => [asset.assetId, asset]));
226
+ for (const assetId of spatialRenderEffectsAssetIds(effects)) checked(assets.has(assetId), `Render effects reference missing asset ${assetId}.`);
227
+ for (const step of effects.renderPlan.postProcess?.steps ?? []) {
228
+ if (step.kind === "lut-grade") checked(assets.get(step.assetId)?.interpretation.kind === "image", `LUT ${step.assetId} must bind an image asset.`);
229
+ if (request.mode.kind === "beauty") renderTargetCount += postProcessRenderTargets(step.kind);
230
+ }
231
+ for (const binding of effects.particleSystems) {
232
+ checked(scene.entities.some(entity => entity.entityId === binding.system.entityId), `Particle system target ${binding.system.entityId} is absent from the scene.`);
233
+ const renderer = binding.system.renderer;
234
+ if (renderer.assetId !== undefined) {
235
+ const interpretation = assets.get(renderer.assetId)?.interpretation.kind;
236
+ checked(renderer.kind === "sprite" ? interpretation === "image" : interpretation === "gltf", `Particle renderer asset ${renderer.assetId} has an incompatible interpretation.`);
237
+ }
238
+ for (const emitter of binding.system.emitters) {
239
+ if (emitter.shape.kind === "surface") checked(assets.get(emitter.shape.assetId)?.interpretation.kind === "gltf", `Particle surface ${emitter.shape.assetId} must bind a glTF asset.`);
240
+ }
241
+ }
242
+ particleStates = effects.renderPlan.quality.particleCount;
243
+ simulationSteps = effects.renderPlan.quality.simulationSteps;
244
+ checked(width * height <= effects.renderPlan.quality.pixelBudget, "Render dimensions exceed the effect pixel budget.");
245
+ checked(texturePixels <= effects.renderPlan.quality.texturePixelBudget, "Scene textures exceed the effect texture-pixel budget.");
246
+ }
247
+ const renderTargetPixels = width * height * renderTargetCount;
248
+ const renderTargetBytes = renderTargetPixels * (request.mode.kind === "object-id" || request.mode.kind === "axial-depth" ? 4 : 8) + width * height * 4;
249
+ const particleStateBytes = particleStates * 64;
250
+ checked(renderTargetBytes + particleStateBytes <= SPATIAL_RENDER_LIMITS.stagingBytes, "Effect GPU working-set estimate exceeds its byte budget.");
150
251
  const selection = request.selection;
151
252
  let samples: SpatialRenderSample[], outputDurationUs: SpatialRational | undefined;
152
253
  if (selection.kind === "video") {
@@ -173,6 +274,14 @@ export function planSpatialRender(sceneInput: unknown, requestInput: unknown): S
173
274
  checked(times.every(time => time <= scene.durationUs), "A selected frame is outside the scene clock.");
174
275
  samples = times.map((timeUs, index) => ({ index, timeUs, exactTimeUs: { numerator: String(timeUs), denominator: "1" } }));
175
276
  }
277
+ if (request.mode.kind === "motion") {
278
+ checked(samples.length >= 2, "Motion evidence requires at least two ordered samples.");
279
+ checked(samples.length - 1 <= SPATIAL_MOTION_EVIDENCE_LIMITS.evidenceSamples, "Motion evidence exceeds its published sample budget.");
280
+ for (let index = 1; index < samples.length; index++) {
281
+ const gap = samples[index]!.timeUs - samples[index - 1]!.timeUs;
282
+ checked(gap >= 1 && gap <= 1_000_000, "Motion evidence requires strictly increasing samples within the one-second exposure bound.");
283
+ }
284
+ }
176
285
  const outputWidth = selection.kind === "contact-sheet" ? selection.columns * selection.cellWidth : width;
177
286
  const outputHeight = selection.kind === "contact-sheet" ? Math.ceil(samples.length / selection.columns) * selection.cellHeight : height;
178
287
  checked(outputWidth <= SPATIAL_RENDER_LIMITS.dimension && outputHeight <= SPATIAL_RENDER_LIMITS.dimension
@@ -186,18 +295,21 @@ export function planSpatialRender(sceneInput: unknown, requestInput: unknown): S
186
295
  // not a promise that every admitted scene compresses below it. A truncated
187
296
  // encode cannot publish: complete encoded count/timestamps are verified.
188
297
  const outputBytesBound = selection.kind === "video" ? Math.min(SPATIAL_RENDER_LIMITS.outputBytes, pngBytesBound + 1024 * 1024) : outputWidth * outputHeight * 5 + outputHeight * 8 + 65_536;
189
- const stagingBytesBound = sourceBytes + pngBytesBound + outputBytesBound + SPATIAL_RENDER_LIMITS.metadataBytes;
298
+ const particleBufferBytesBound = particleStateBytes * samples.length;
299
+ const stagingBytesBound = sourceBytes + pngBytesBound + particleBufferBytesBound + outputBytesBound + SPATIAL_RENDER_LIMITS.metadataBytes;
190
300
  checked(sourceBytes <= SPATIAL_RENDER_LIMITS.sourceBytes, "Spatial source closure exceeds its byte budget.");
191
301
  checked(renderPixels <= SPATIAL_RENDER_LIMITS.totalPixels && pngBytesBound <= SPATIAL_RENDER_LIMITS.pngBytes, "Spatial frame staging exceeds its pixel or byte budget; reduce resolution or range.");
192
302
  checked(outputBytesBound <= SPATIAL_RENDER_LIMITS.outputBytes && stagingBytesBound <= SPATIAL_RENDER_LIMITS.stagingBytes, "Spatial output exceeds its conservative byte budget.");
303
+ if (effects !== undefined) checked(outputBytesBound <= effects.renderPlan.quality.outputBytes, "Spatial output exceeds the effect output-byte budget.");
193
304
  // Semantic override validation is pure and precedes capability or resource admission.
194
305
  evaluateSpatialScene(scene, { cameraId: request.cameraId, timeUs: samples[0]!.timeUs,
195
306
  ...(request.overrides === undefined ? {} : { overrides: request.overrides }),
196
307
  ...(request.cameraPoseOverride === undefined ? {} : { cameraPoseOverride: request.cameraPoseOverride }),
197
308
  });
198
- return deepFreezeJson({ scene, sceneSha256: spatialSceneSha256(scene), request, requestSha256: canonicalJsonSha256(request), samples,
309
+ return deepFreezeJson({ scene, sceneSha256: sceneDigest, request, requestSha256: canonicalJsonSha256(request), samples,
199
310
  width, height, outputWidth, outputHeight, ...(outputDurationUs === undefined ? {} : { outputDurationUs }),
200
- costs: { sourceBytes, renderPixels, pngBytesBound, outputBytesBound, stagingBytesBound },
311
+ costs: { sourceBytes, renderPixels, renderTargetPixels, renderTargetBytes, texturePixels, particleStates, particleStateBytes, particleBufferBytesBound, simulationSteps,
312
+ pngBytesBound, outputBytesBound, stagingBytesBound },
201
313
  });
202
314
  }
203
315
 
@@ -272,6 +384,9 @@ export const SpatialRenderReceiptSchema = z.strictObject({
272
384
  source: z.strictObject({ canonicalScene: MediaArtifactReferenceSchema, canonicalization: z.literal("parsed-spatial-scene-v1"),
273
385
  originalSceneArtifact: MediaArtifactReferenceSchema.optional(), retainedAssets: z.array(retainedAssetSchema).max(128), sourceManifests: z.record(SpatialAssetIdSchema, SpatialDigestSchema) }),
274
386
  request: SpatialRenderRequestSchema, requestSha256: SpatialDigestSchema,
387
+ effects: z.strictObject({ appliedToBeauty: z.boolean(), documentSha256: SpatialDigestSchema, renderPlanSha256: SpatialDigestSchema,
388
+ particleSystemSha256s: z.array(SpatialDigestSchema).max(64), simulationBakeReceiptSha256s: z.array(SpatialDigestSchema).max(64) }).optional(),
389
+ motionEvidence: z.strictObject({ artifact: MediaArtifactReferenceSchema, evidenceSha256: SpatialDigestSchema }).optional(),
275
390
  samples: z.array(z.strictObject({ sample: z.strictObject({ index: z.number().int().min(0).max(SPATIAL_RENDER_LIMITS.frames - 1), timeUs: SpatialTimeUsSchema, exactTimeUs: rationalSchema }),
276
391
  stateSha256: SpatialDigestSchema, viewSha256: SpatialDigestSchema, pngSha256: SpatialDigestSchema, pngBytes: byteCountSchema })).min(1).max(SPATIAL_RENDER_LIMITS.frames),
277
392
  render: renderSummarySchema, output: MediaArtifactReferenceSchema,
@@ -287,9 +402,26 @@ export const SpatialRenderReceiptSchema = z.strictObject({
287
402
  contactSheet: z.strictObject({ fit: z.literal("contain"), resampling: z.enum(["lanczos3", "nearest-data"]), calibratedProjectionResized: z.literal(false) }).optional(),
288
403
  encodedProbe: MediaArtifactReferenceSchema.optional(),
289
404
  workflow: z.strictObject({ nodeKey: z.string().min(1).max(255), nodePlanSha256: SpatialDigestSchema, runId: z.string().min(1).max(128) }).optional(),
290
- costs: z.strictObject({ sourceBytes: byteCountSchema, renderPixels: z.number().int().min(1).max(SPATIAL_RENDER_LIMITS.totalPixels), pngBytesBound: byteCountSchema,
405
+ costs: z.strictObject({ sourceBytes: byteCountSchema, renderPixels: z.number().int().min(1).max(SPATIAL_RENDER_LIMITS.totalPixels),
406
+ renderTargetPixels: z.number().int().safe().min(1).max(SPATIAL_RENDER_LIMITS.totalPixels * 32).optional(), renderTargetBytes: byteCountSchema.optional(),
407
+ texturePixels: z.number().int().safe().min(0).max(SPATIAL_RENDER_LIMITS.totalPixels).optional(), particleStates: z.number().int().safe().min(0).max(1_000_000).optional(),
408
+ particleStateBytes: byteCountSchema.optional(), particleBufferBytesBound: byteCountSchema.optional(), simulationSteps: z.number().int().safe().min(0).max(10_000).optional(), pngBytesBound: byteCountSchema,
291
409
  outputBytesBound: byteCountSchema, stagingBytesBound: byteCountSchema, actualPngBytes: byteCountSchema, actualBatchMetadataBytes: byteCountSchema }),
292
410
  }).superRefine((receipt, context) => {
411
+ const expectedEffects = receipt.request.effects === undefined ? undefined : {
412
+ appliedToBeauty: receipt.request.mode.kind === "beauty",
413
+ documentSha256: receipt.request.effects.documentSha256,
414
+ renderPlanSha256: receipt.request.effects.document.renderPlanSha256,
415
+ particleSystemSha256s: receipt.request.effects.document.particleSystems.map(binding => binding.systemSha256),
416
+ simulationBakeReceiptSha256s: receipt.request.effects.document.simulationBakes.map(item => item.receiptSha256),
417
+ };
418
+ if (expectedEffects === undefined ? receipt.effects !== undefined
419
+ : receipt.effects === undefined || canonicalJson(receipt.effects) !== canonicalJson(expectedEffects)) {
420
+ context.addIssue({ code: "custom", path: ["effects"], message: "Spatial receipt must bind the exact requested effects and diagnostic bypass state." });
421
+ }
422
+ if ((receipt.request.mode.kind === "motion") !== (receipt.motionEvidence !== undefined)) {
423
+ context.addIssue({ code: "custom", path: ["motionEvidence"], message: "Spatial receipt must bind motion evidence exactly when its mode is motion." });
424
+ }
293
425
  if (receipt.batches.length > (receipt.request.executionProfile === undefined ? Math.ceil(SPATIAL_RENDER_LIMITS.frames / SPATIAL_RENDER_LIMITS.batchFrames) : receipt.samples.length)) {
294
426
  context.addIssue({ code: "custom", path: ["batches"], message: "Spatial batch count exceeds its selected profile's bounded sample partitions." });
295
427
  }
@@ -336,7 +468,7 @@ export class SpatialRenderFailure extends ApplicationError {
336
468
  }
337
469
 
338
470
  /** Descriptor-bound, bounded read. Parent paths and the named leaf must retain physical identity. */
339
- async function readPhysical(path: string, maximumBytes: number, signal: AbortSignal): Promise<Uint8Array> {
471
+ export async function readPhysical(path: string, maximumBytes: number, signal: AbortSignal): Promise<Uint8Array> {
340
472
  checked(await realpath(path) === path, "Spatial render files must have physical paths.");
341
473
  const parents: { path: string; dev: number; ino: number }[] = [];
342
474
  for (let parent = dirname(path);; parent = dirname(parent)) {
@@ -378,7 +510,8 @@ async function stageJson(path: string, input: unknown, maximumBytes: number): Pr
378
510
  await stageBytes(path, bytes);
379
511
  return bytes;
380
512
  }
381
- async function verifyPng(bytes: Uint8Array, width: number, height: number): Promise<void> {
513
+ /** Returns the fully decoded RGBA8 raster; the rendered audit consumes it as data. */
514
+ export async function verifyPng(bytes: Uint8Array, width: number, height: number): Promise<Uint8Array> {
382
515
  const decoder = sharp(bytes, { failOn: "warning", limitInputPixels: SPATIAL_RENDER_LIMITS.framePixels });
383
516
  const metadata = await decoder.metadata();
384
517
  checked(metadata.format === "png" && metadata.width === width && metadata.height === height && metadata.depth === "uchar"
@@ -387,6 +520,7 @@ async function verifyPng(bytes: Uint8Array, width: number, height: number): Prom
387
520
  // Fully decode before accepting renderer bytes. This preserves data-pass channel values.
388
521
  const decoded = await decoder.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
389
522
  checked(decoded.info.channels === 4 && decoded.data.length === width * height * 4, "Renderer PNG did not decode to its declared RGBA raster.");
523
+ return decoded.data;
390
524
  }
391
525
 
392
526
  const integerText = z.union([z.number().int().safe(), z.string().regex(/^-?(?:0|[1-9]\d{0,23})$/u)]).transform(String);
@@ -482,6 +616,8 @@ export async function renderSpatialScene(context: OperationExecutionContext, inp
482
616
  const ports = { runner, ...(names.includes("ffmpeg") ? { ffmpegCommand: mediaCapabilityCommand(capabilities, "ffmpeg"), ffprobeCommand: mediaCapabilityCommand(capabilities, "ffprobe") } : {}) };
483
617
  const retainedSources: { assetId: string; manifestSha256: string; originalPath: string; stagedPath: string; bytes: number }[] = [];
484
618
  const batchFiles: string[] = [], sampleEvidence: { sample: SpatialRenderSample; stateSha256: string; viewSha256: string; pngSha256: string; pngBytes: number }[] = [];
619
+ const motionSamples: { id: string; entityId: string; exposureUs: number; previousTimeUs: number; sampleTimeUs: number; width: number; height: number;
620
+ viewport: readonly [number, number, number, number]; byteLength: number; encoding: "rg16un"; motionSha256: string; motionScale: number; samplesPerPixel: number }[] = [];
485
621
  let pngBytes = 0, metadataBytes = 0;
486
622
  let gpuEvidence: HtmlOverlayGpuEvidence | undefined;
487
623
  for (let offset = 0; offset < plan.samples.length; offset += SPATIAL_RENDER_LIMITS.batchFrames) {
@@ -492,7 +628,35 @@ export async function renderSpatialScene(context: OperationExecutionContext, inp
492
628
  ...(plan.request.cameraPoseOverride === undefined ? {} : { cameraPoseOverride: plan.request.cameraPoseOverride }),
493
629
  }));
494
630
  stage = "preparation";
495
- await withPreparedSpatialAssets({ snapshots, exactSceneTimesUs: samples.map(sample => sample.exactTimeUs), assetRoot, workspaceParent: directory }, ports, context.abortSignal, async prepared => {
631
+ const lutAssetIds = plan.request.mode.kind === "beauty" && plan.request.effects !== undefined
632
+ ? plan.request.effects.document.renderPlan.postProcess?.steps.flatMap(step => step.kind === "lut-grade" ? [step.assetId] : []) : undefined;
633
+ await withPreparedSpatialAssets({ snapshots, exactSceneTimesUs: samples.map(sample => sample.exactTimeUs), assetRoot, workspaceParent: directory,
634
+ ...(lutAssetIds === undefined || lutAssetIds.length === 0 ? {} : { lutAssetIds }) }, ports, context.abortSignal, async prepared => {
635
+ const particleAssets = new Map<string, PreparedSpatialAsset>();
636
+ const particleResources = new Map<string, BoundHtmlOverlayResource>();
637
+ if (plan.request.mode.kind === "beauty" && plan.request.effects !== undefined) {
638
+ for (const binding of plan.request.effects.document.particleSystems) {
639
+ const surfaces = particleSurfaceInputs(binding.system, prepared.preparedAssets);
640
+ for (const sample of samples) {
641
+ const key = `${binding.systemSha256}:${binding.system.entityId}:${sample.timeUs}`;
642
+ if (particleAssets.has(key)) continue;
643
+ const instances = prepareSpatialParticleInstances({ sampleTimeUs: sample.timeUs, surfaces, system: binding.system });
644
+ const name = `particles-${instances.sha256.slice(0, 40)}`;
645
+ const path = join(directory!, `${name}.bin`);
646
+ if (!particleResources.has(name)) {
647
+ await stageBytes(path, instances.bytes);
648
+ const resource = { name, sha256: instances.sha256, bytes: instances.byteLength, mediaType: "application/octet-stream", transport: "fetch" as const, urlPath: `${name}.bin` };
649
+ particleResources.set(name, { ...resource, absolutePath: path });
650
+ }
651
+ particleAssets.set(key, { kind: "particle-instances", entityId: binding.system.entityId, instanceCount: instances.instanceCount,
652
+ resource: { name, sha256: instances.sha256, bytes: instances.byteLength, mediaType: "application/octet-stream", transport: "fetch", urlPath: `${name}.bin` },
653
+ strideBytes: instances.strideBytes, systemSha256: instances.systemSha256, timeUs: sample.timeUs });
654
+ }
655
+ }
656
+ }
657
+ const allPreparedAssets = [...prepared.preparedAssets, ...particleAssets.values()];
658
+ const allResources = [...prepared.resources, ...particleResources.values()];
659
+ checked(allResources.reduce((sum, resource) => sum + resource.bytes, 0) <= plan.costs.stagingBytesBound, "Prepared particle resources exceeded the admitted staging budget.");
496
660
  checked(canonicalJson(prepared.receipt.sourceManifests) === canonicalJson(sourceManifests), "Asset preparation did not bind the complete source closure.");
497
661
  if (offset === 0) {
498
662
  for (const source of prepared.sources) {
@@ -505,17 +669,23 @@ export async function renderSpatialScene(context: OperationExecutionContext, inp
505
669
  }
506
670
  checked(retainedSources.length === plan.scene.assets.length && new Set(retainedSources.map(source => source.assetId)).size === retainedSources.length, "Retained source closure is incomplete or duplicated.");
507
671
  }
672
+ const needsVelocity = plan.request.mode.kind === "motion"
673
+ || plan.request.effects?.document.renderPlan.postProcess?.steps.some(step => step.kind === "motion-blur") === true;
508
674
  const partitions = partitionSpatialRenderWindow({ snapshots, mode: plan.request.mode,
675
+ ...(plan.request.effects === undefined ? {} : { effects: plan.request.effects }),
509
676
  ...(plan.request.executionProfile === undefined ? {} : { executionProfile: plan.request.executionProfile }),
510
- frameRate: plan.request.selection.kind === "video" ? plan.request.selection.frameRate : { numerator: 1, denominator: 1 }, preparedAssets: prepared.preparedAssets });
677
+ ...(needsVelocity && offset > 0 ? { previousSnapshot: evaluateSpatialScene(plan.scene, { cameraId: plan.request.cameraId, timeUs: plan.samples[offset - 1]!.timeUs,
678
+ ...(plan.request.overrides === undefined ? {} : { overrides: plan.request.overrides }),
679
+ ...(plan.request.cameraPoseOverride === undefined ? {} : { cameraPoseOverride: plan.request.cameraPoseOverride }) }) } : {}),
680
+ frameRate: plan.request.selection.kind === "video" ? plan.request.selection.frameRate : { numerator: 1, denominator: 1 }, preparedAssets: allPreparedAssets });
511
681
  for (const partition of partitions) {
512
682
  const batchOffset = offset + partition.offset;
513
683
  const batchSamples = samples.slice(partition.offset, partition.offset + partition.length);
514
684
  const batchSnapshots = snapshots.slice(partition.offset, partition.offset + partition.length);
515
685
  const batch = partition.batch;
516
686
  const resourceNames = new Set(batch.authoring.resources.map(resource => resource.name));
517
- const batchResources = plan.request.executionProfile === undefined ? prepared.resources : prepared.resources.filter(resource => resourceNames.has(resource.name));
518
- const batchPreparation = plan.request.executionProfile === undefined ? prepared.receipt : { ...prepared.receipt,
687
+ const batchResources = allResources.filter(resource => resourceNames.has(resource.name));
688
+ const batchPreparation = particleAssets.size === 0 && plan.request.executionProfile === undefined ? prepared.receipt : { ...prepared.receipt,
519
689
  preparedSha256: canonicalJsonSha256(partition.preparedAssets), outputBytes: batchResources.reduce((sum, resource) => sum + resource.bytes, 0) };
520
690
  const batchDirectory = join(directory!, `batch-${batchOffset}`);
521
691
  await mkdir(batchDirectory, { mode: 0o700 });
@@ -541,7 +711,15 @@ export async function renderSpatialScene(context: OperationExecutionContext, inp
541
711
  const bytes = await readPhysical(join(batchDirectory, "frames", frameName(index)), plan.costs.pngBytesBound, context.abortSignal);
542
712
  pngBytes += bytes.length;
543
713
  checked(pngBytes <= plan.costs.pngBytesBound, "Rendered PNG bytes exceeded the admitted staging estimate.");
544
- await verifyPng(bytes, plan.width, plan.height);
714
+ const decoded = await verifyPng(bytes, plan.width, plan.height);
715
+ if (plan.request.mode.kind === "motion" && sample.index > 0) {
716
+ const previous = plan.samples[sample.index - 1]!;
717
+ motionSamples.push({ id: `sample_${sample.index}`, entityId: plan.request.mode.entityId,
718
+ exposureUs: sample.timeUs - previous.timeUs, previousTimeUs: previous.timeUs, sampleTimeUs: sample.timeUs,
719
+ width: plan.width, height: plan.height, viewport: [0, 0, plan.width, plan.height],
720
+ byteLength: decoded.length, encoding: "rg16un", motionSha256: sha256(decoded),
721
+ motionScale: plan.request.mode.motionScale, samplesPerPixel: 1 });
722
+ }
545
723
  await stageBytes(join(frameDirectory, frameName(sample.index)), bytes);
546
724
  sampleEvidence.push({ sample, stateSha256: batchSnapshots[index]!.stateSha256, viewSha256: batchSnapshots[index]!.viewSha256, pngSha256: sha256(bytes), pngBytes: bytes.length });
547
725
  }
@@ -595,6 +773,13 @@ export async function renderSpatialScene(context: OperationExecutionContext, inp
595
773
  const scenePath = join(directory, "scene.json"), runtimePath = join(directory, "runtime.json");
596
774
  const sceneBytes = await stageJson(scenePath, plan.scene, 2_097_153);
597
775
  const runtimeBytes = await stageJson(runtimePath, runtime, SPATIAL_RENDER_LIMITS.metadataBytes);
776
+ let motionEvidence: { evidence: ReturnType<typeof parseSpatialMotionEvidence>; path: string } | undefined;
777
+ if (plan.request.mode.kind === "motion") {
778
+ motionEvidence = { evidence: parseSpatialMotionEvidence({ kind: "slopcamera.spatial-motion-evidence", schemaVersion: 1,
779
+ entityId: plan.request.mode.entityId, renderRequestSha256: plan.requestSha256,
780
+ rendererSha256: runtime.manifest.rootSha256, samples: motionSamples }), path: join(directory, "motion-evidence.json") };
781
+ metadataBytes += (await stageJson(motionEvidence.path, motionEvidence.evidence, SPATIAL_RENDER_LIMITS.metadataBytes)).length;
782
+ }
598
783
  checked(metadataBytes + sceneBytes.length + runtimeBytes.length <= SPATIAL_RENDER_LIMITS.metadataBytes, "Combined spatial source/runtime/batch evidence exceeds the admitted metadata budget.");
599
784
  stage = "publication";
600
785
  const publish = async (stagedPath: string, extension: string, maximumBytes: number): Promise<MediaArtifactReference> => {
@@ -621,6 +806,9 @@ export async function renderSpatialScene(context: OperationExecutionContext, inp
621
806
  const batches: MediaArtifactReference[] = [];
622
807
  for (const path of batchFiles) batches.push(await publish(path, ".json", SPATIAL_RENDER_LIMITS.metadataBytes));
623
808
  const encodedProbe = probePath === undefined ? undefined : await publish(probePath, ".json", SPATIAL_RENDER_LIMITS.probeBytes);
809
+ const motionEvidenceArtifact = motionEvidence === undefined ? undefined : {
810
+ artifact: await publish(motionEvidence.path, ".json", SPATIAL_RENDER_LIMITS.metadataBytes),
811
+ evidenceSha256: spatialMotionEvidenceSha256(motionEvidence.evidence) };
624
812
  const artifact = await publish(outputPath, plan.request.selection.kind === "video" ? ".mov" : ".png", plan.costs.outputBytesBound);
625
813
  const frameArtifacts: MediaArtifactReference[] = [];
626
814
  if (plan.request.selection.kind === "frame") frameArtifacts.push(artifact);
@@ -632,7 +820,16 @@ export async function renderSpatialScene(context: OperationExecutionContext, inp
632
820
  const receiptValue = SpatialRenderReceiptSchema.parse({
633
821
  kind: "slopcamera.spatial-render-receipt", schemaVersion: 1, attemptId, sceneSha256: plan.sceneSha256,
634
822
  source: { canonicalScene: sceneSource, canonicalization: "parsed-spatial-scene-v1", ...(originalSceneArtifact === undefined ? {} : { originalSceneArtifact }), retainedAssets, sourceManifests },
635
- request: plan.request, requestSha256: plan.requestSha256, samples: sampleEvidence, render, output: artifact, batches, frameArtifacts,
823
+ request: plan.request, requestSha256: plan.requestSha256,
824
+ ...(plan.request.effects === undefined ? {} : { effects: {
825
+ appliedToBeauty: plan.request.mode.kind === "beauty",
826
+ documentSha256: plan.request.effects.documentSha256,
827
+ renderPlanSha256: plan.request.effects.document.renderPlanSha256,
828
+ particleSystemSha256s: plan.request.effects.document.particleSystems.map(binding => binding.systemSha256),
829
+ simulationBakeReceiptSha256s: plan.request.effects.document.simulationBakes.map(item => item.receiptSha256),
830
+ } }),
831
+ ...(motionEvidenceArtifact === undefined ? {} : { motionEvidence: motionEvidenceArtifact }),
832
+ samples: sampleEvidence, render, output: artifact, batches, frameArtifacts,
636
833
  runtime: { artifact: runtimeArtifact, rootSha256: runtime.manifest.rootSha256, capabilities, renderer: "three-webgl2-snapshot-v1", threeVersion: "0.185.1",
637
834
  ...(gpuEvidence === undefined ? {} : { gpuEvidence }) },
638
835
  color: { output: plan.request.mode.kind === "beauty" ? "srgb" : "rgba8-data", alpha: plan.request.mode.kind === "beauty" ? "straight" : "binary-validity", toneMapping: "none" },
@@ -668,7 +865,7 @@ export async function renderSpatialScene(context: OperationExecutionContext, inp
668
865
  return completed;
669
866
  }
670
867
 
671
- function frameName(index: number): string { return `frame-${String(index).padStart(8, "0")}.png`; }
868
+ export function frameName(index: number): string { return `frame-${String(index).padStart(8, "0")}.png`; }
672
869
  function prospective(context: OperationExecutionContext, category: "outputs" | "receipts", extension: string, bytes: Uint8Array): MediaArtifactReference {
673
870
  const digest = sha256(bytes);
674
871
  return MediaArtifactReferenceSchema.parse({ sha256: digest, bytes: bytes.length,