@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,137 @@
1
+ import type { SpatialAssetManifest, SpatialSceneV1 } from "./contracts.js"
2
+ import { parseSpatialScene } from "./identity.js"
3
+
4
+ /**
5
+ * The gallery probe scene is a fixed checked-in template — never
6
+ * caller-authored — that substitutes one admitted candidate asset so a
7
+ * rendered still proves how it behaves in a scene: on lit geometry for a
8
+ * texture, as `scene.background`/`scene.environment` lighting for a skybox,
9
+ * or as a flat backdrop surface behind a lit subject.
10
+ */
11
+ export type SlopcameraGalleryProbeMode = "texture" | "skybox" | "backdrop"
12
+
13
+ const transform = (position: readonly [number, number, number]) => ({
14
+ position,
15
+ rotation: [0, 0, 0, 1] as const,
16
+ scale: [1, 1, 1] as const,
17
+ })
18
+
19
+ const entityBase = {
20
+ parentId: null,
21
+ placement: { kind: "world" as const },
22
+ origin: { kind: "authored" as const },
23
+ visible: true,
24
+ }
25
+
26
+ const neutral = {
27
+ kind: "standard" as const,
28
+ color: "#8b95a7",
29
+ opacity: 1,
30
+ roughness: 0.55,
31
+ metalness: 0.05,
32
+ }
33
+
34
+ export function galleryProbeScene(
35
+ mode: SlopcameraGalleryProbeMode,
36
+ asset: SpatialAssetManifest,
37
+ ): SpatialSceneV1 {
38
+ const subjectMaterial = mode === "texture"
39
+ ? { kind: "standard" as const, color: "#ffffff", opacity: 1, roughness: 0.75, metalness: 0, map: asset.assetId }
40
+ : mode === "skybox"
41
+ // A reflective sphere shows what the environment actually lights.
42
+ ? { kind: "standard" as const, color: "#cfd6e4", opacity: 1, roughness: 0.18, metalness: 0.85 }
43
+ : neutral
44
+ const entities: unknown[] = [
45
+ {
46
+ ...entityBase, entityId: "entity_subject", name: "Subject", kind: "mesh",
47
+ transform: transform([0, 0.15, 0]),
48
+ geometry: { kind: "sphere", radius: 0.9 },
49
+ material: subjectMaterial,
50
+ },
51
+ {
52
+ ...entityBase, entityId: "entity_ground", name: "Ground", kind: "mesh",
53
+ transform: {
54
+ position: [0, -0.75, 0],
55
+ rotation: [-0.7071067811865476, 0, 0, 0.7071067811865476],
56
+ scale: [1, 1, 1],
57
+ },
58
+ geometry: { kind: "plane", width: 10, height: 10 },
59
+ material: { ...neutral, roughness: 0.9 },
60
+ },
61
+ {
62
+ ...entityBase, entityId: "entity_fill", name: "Fill", kind: "light",
63
+ transform: transform([0, 0, 0]),
64
+ light: "ambient", color: "#ffffff", intensity: 1.6,
65
+ },
66
+ {
67
+ ...entityBase, entityId: "entity_key", name: "Key", kind: "light",
68
+ transform: transform([3, 4, 5]),
69
+ light: "directional", color: "#ffffff", intensity: 3,
70
+ },
71
+ ]
72
+ if (mode === "texture") {
73
+ const wallTiles = [
74
+ [-2.5, 0.2, -3],
75
+ [2.5, 0.2, -3],
76
+ [-2.5, 3, -3],
77
+ [2.5, 3, -3],
78
+ ] as const
79
+ for (const [index, position] of wallTiles.entries()) {
80
+ entities.push({
81
+ ...entityBase,
82
+ entityId: `entity_wall_${String(index + 1)}`,
83
+ name: `Wall tile ${String(index + 1)}`,
84
+ kind: "mesh",
85
+ transform: transform(position),
86
+ geometry: { kind: "plane", width: 5, height: 2.8 },
87
+ material: {
88
+ kind: "standard", color: "#ffffff", opacity: 1, roughness: 0.85,
89
+ metalness: 0, map: asset.assetId,
90
+ },
91
+ })
92
+ }
93
+ }
94
+ if (mode === "skybox") {
95
+ entities.push({
96
+ ...entityBase, entityId: "entity_environment", name: "Environment",
97
+ kind: "environment", transform: transform([0, 0, 0]),
98
+ assetId: asset.assetId, role: "both", intensity: 1,
99
+ })
100
+ }
101
+ if (mode === "backdrop") {
102
+ entities.push({
103
+ ...entityBase, entityId: "entity_backdrop", name: "Backdrop", kind: "image",
104
+ transform: transform([0, 1.6, -3]),
105
+ assetId: asset.assetId, width: 12, height: 6.75, fit: "cover", opacity: 1,
106
+ })
107
+ }
108
+ return parseSpatialScene({
109
+ kind: "slopcamera.spatial-scene",
110
+ schemaVersion: 1,
111
+ sceneId: "scene_gallery_probe",
112
+ coordinates: "right-handed-y-up-meters",
113
+ durationUs: 4_000_000,
114
+ entities,
115
+ cameras: [{
116
+ cameraId: "camera_probe",
117
+ name: "Probe",
118
+ pose: { position: [0, 0.5, 4.4], rotation: [0, 0, 0, 1] },
119
+ projection: {
120
+ kind: "perspective", width: 960, height: 540,
121
+ fx: 650, fy: 650, cx: 480, cy: 270, near: 0.1, far: 100,
122
+ },
123
+ }],
124
+ assets: [asset],
125
+ animations: [],
126
+ generators: [],
127
+ overrides: [],
128
+ })
129
+ }
130
+
131
+ export function galleryProbeRenderRequest() {
132
+ return {
133
+ cameraId: "camera_probe",
134
+ mode: { kind: "beauty" as const },
135
+ selection: { kind: "frame" as const, timeUs: 0 },
136
+ }
137
+ }
@@ -0,0 +1,86 @@
1
+ import { z } from "zod"
2
+
3
+ import {
4
+ SpatialCameraIdSchema,
5
+ SpatialDigestSchema,
6
+ SpatialTimeUsSchema,
7
+ } from "./contracts.js"
8
+ import { SpatialDirectionSchema } from "./direction.js"
9
+ import { SpatialRenderPlanSchema } from "./effects.js"
10
+ import { SpatialGalleryAxisSchema } from "./gallery.js"
11
+ import { deepFreezeJson } from "../code/json-snapshot.js"
12
+ import { parseSpatialValue, spatialValueSha256 } from "./identity.js"
13
+ import { SpatialParticleSystemSchema } from "./particle.js"
14
+ import { SpatialSimulationBakeReceiptSchema } from "./simulation.js"
15
+ import { SpatialTemporalContactSchema } from "./temporal-audit.js"
16
+
17
+ /**
18
+ * Declarative recipe packs for Phase 13.
19
+ *
20
+ * A recipe pack is the bounded, inert, content-addressed document a coding
21
+ * agent authors to parameterize the built-in `cinematic-world` workflow. It
22
+ * names an admitted scene by digest, one authored direction document, the
23
+ * gallery axes to explore, preview render requests, optional effect systems to
24
+ * bind, and optional sampled temporal-audit settings. A pack is data, never
25
+ * code: admission parses it from `unknown`, and the workflow independently
26
+ * revalidates every embedded render request before planning.
27
+ */
28
+ export const SPATIAL_RECIPE_PACK_LIMITS = Object.freeze({
29
+ axes: 6,
30
+ previews: 4,
31
+ timesUs: 64,
32
+ })
33
+
34
+ export const SpatialRecipePackPreviewSchema = z.strictObject({
35
+ name: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/u),
36
+ /** Validated again by the host render-request schema at workflow admission. */
37
+ request: z.unknown(),
38
+ })
39
+ export type SpatialRecipePackPreview = Readonly<z.infer<typeof SpatialRecipePackPreviewSchema>>
40
+
41
+ export const SpatialRecipePackEffectsSchema = z.strictObject({
42
+ particleSystems: z.array(SpatialParticleSystemSchema).max(64).default([]),
43
+ renderPlan: SpatialRenderPlanSchema,
44
+ simulationBakes: z.array(SpatialSimulationBakeReceiptSchema).max(64).default([]),
45
+ })
46
+ export type SpatialRecipePackEffects = Readonly<z.infer<typeof SpatialRecipePackEffectsSchema>>
47
+
48
+ export const SpatialRecipePackAuditSchema = z.strictObject({
49
+ contacts: z.array(SpatialTemporalContactSchema).max(64).default([]),
50
+ cutBeforeUs: z.array(SpatialTimeUsSchema).max(64).default([]),
51
+ timesUs: z.array(SpatialTimeUsSchema).min(2).max(SPATIAL_RECIPE_PACK_LIMITS.timesUs).optional(),
52
+ })
53
+ export type SpatialRecipePackAudit = Readonly<z.infer<typeof SpatialRecipePackAuditSchema>>
54
+
55
+ export const SpatialRecipePackSchema = z.strictObject({
56
+ kind: z.literal("slopcamera.spatial-recipe-pack"),
57
+ schemaVersion: z.literal(1),
58
+ packId: z.string().regex(/^recipe_[a-zA-Z0-9_-]{1,64}$/u),
59
+ sceneSha256: SpatialDigestSchema,
60
+ direction: SpatialDirectionSchema,
61
+ cameraId: SpatialCameraIdSchema.optional(),
62
+ axes: z.array(SpatialGalleryAxisSchema).min(1).max(SPATIAL_RECIPE_PACK_LIMITS.axes),
63
+ previews: z.array(SpatialRecipePackPreviewSchema).max(SPATIAL_RECIPE_PACK_LIMITS.previews).default([]),
64
+ effects: SpatialRecipePackEffectsSchema.optional(),
65
+ temporalAudit: SpatialRecipePackAuditSchema.optional(),
66
+ }).superRefine((pack, context) => {
67
+ if (new Set(pack.axes).size !== pack.axes.length) {
68
+ context.addIssue({ code: "custom", message: "Recipe pack gallery axes must be unique.", path: ["axes"] })
69
+ }
70
+ const names = pack.previews.map(preview => preview.name)
71
+ if (new Set(names).size !== names.length) {
72
+ context.addIssue({ code: "custom", message: "Recipe pack preview names must be unique.", path: ["previews"] })
73
+ }
74
+ if (pack.temporalAudit !== undefined && pack.cameraId === undefined) {
75
+ context.addIssue({ code: "custom", message: "A temporal audit requires the pack cameraId.", path: ["temporalAudit"] })
76
+ }
77
+ })
78
+ export type SpatialRecipePack = Readonly<z.infer<typeof SpatialRecipePackSchema>>
79
+
80
+ export function parseSpatialRecipePack(input: unknown): SpatialRecipePack {
81
+ return deepFreezeJson(parseSpatialValue(SpatialRecipePackSchema, input, "spatial recipe pack"))
82
+ }
83
+
84
+ export function spatialRecipePackSha256(pack: SpatialRecipePack): string {
85
+ return spatialValueSha256(pack)
86
+ }
@@ -0,0 +1,240 @@
1
+ import { z } from "zod"
2
+
3
+ import { canonicalJsonSha256 } from "../code/canonical-json.js"
4
+ import { deepFreezeJson } from "../code/json-snapshot.js"
5
+ import { SpatialDigestSchema, SpatialSceneV1Schema } from "./contracts.js"
6
+ import { spatialRenderPlanAssetIds, SpatialRenderPlanSchema, spatialRenderPlanSha256 } from "./effects.js"
7
+ import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
8
+ import { spatialParticleAssetIds, SpatialParticleSystemSchema, spatialParticleSystemSha256 } from "./particle.js"
9
+ import { SpatialSimulationBakeReceiptSchema } from "./simulation.js"
10
+
11
+ export const SPATIAL_RENDER_EFFECTS_LIMITS = Object.freeze({
12
+ particleSystems: 64,
13
+ simulationBakes: 64,
14
+ })
15
+
16
+ const particleBindingSchema = z.strictObject({
17
+ system: SpatialParticleSystemSchema,
18
+ systemSha256: SpatialDigestSchema,
19
+ })
20
+
21
+ export const SpatialRenderEffectsDocumentSchema = z.strictObject({
22
+ kind: z.literal("slopcamera.spatial-render-effects"),
23
+ particleSystems: z.array(particleBindingSchema).max(SPATIAL_RENDER_EFFECTS_LIMITS.particleSystems),
24
+ renderPlan: SpatialRenderPlanSchema,
25
+ renderPlanSha256: SpatialDigestSchema,
26
+ sceneSha256: SpatialDigestSchema,
27
+ schemaVersion: z.literal(1),
28
+ simulationBakes: z.array(SpatialSimulationBakeReceiptSchema).max(SPATIAL_RENDER_EFFECTS_LIMITS.simulationBakes),
29
+ }).superRefine((document, context) => {
30
+ if (spatialRenderPlanSha256(document.renderPlan) !== document.renderPlanSha256) {
31
+ context.addIssue({ code: "custom", path: ["renderPlanSha256"], message: "Render-plan digest does not match the canonical render plan." })
32
+ }
33
+ const particleEntities = new Set<string>()
34
+ for (const [index, binding] of document.particleSystems.entries()) {
35
+ if (spatialParticleSystemSha256(binding.system) !== binding.systemSha256) {
36
+ context.addIssue({ code: "custom", path: ["particleSystems", index, "systemSha256"], message: "Particle-system digest does not match its canonical system." })
37
+ }
38
+ if (particleEntities.has(binding.system.entityId)) {
39
+ context.addIssue({ code: "custom", path: ["particleSystems", index, "system", "entityId"], message: `Duplicate particle-system entity ${binding.system.entityId}.` })
40
+ }
41
+ particleEntities.add(binding.system.entityId)
42
+ }
43
+ const caches = new Set<string>()
44
+ for (const [index, receipt] of document.simulationBakes.entries()) {
45
+ const { receiptSha256, ...body } = receipt
46
+ if (canonicalJsonSha256(body) !== receiptSha256) {
47
+ context.addIssue({ code: "custom", path: ["simulationBakes", index, "receiptSha256"], message: "Simulation bake receipt digest does not match its canonical body." })
48
+ }
49
+ if (receipt.sourceDigest !== document.sceneSha256) {
50
+ context.addIssue({ code: "custom", path: ["simulationBakes", index, "sourceDigest"], message: "Simulation bake receipt belongs to a different scene source." })
51
+ }
52
+ if (caches.has(receipt.cacheId)) {
53
+ context.addIssue({ code: "custom", path: ["simulationBakes", index, "cacheId"], message: `Duplicate simulation cache ${receipt.cacheId}.` })
54
+ }
55
+ caches.add(receipt.cacheId)
56
+ }
57
+ const particleCount = document.particleSystems.reduce((sum, binding) => sum + binding.system.maxCount, 0)
58
+ if (document.renderPlan.quality.particleCount !== particleCount) {
59
+ context.addIssue({ code: "custom", path: ["renderPlan", "quality", "particleCount"], message: "Render quality must declare the exact aggregate particle-state count." })
60
+ }
61
+ const simulationSteps = document.simulationBakes.reduce((sum, receipt) => sum + receipt.stepCount, 0)
62
+ if (document.renderPlan.quality.simulationSteps !== simulationSteps) {
63
+ context.addIssue({ code: "custom", path: ["renderPlan", "quality", "simulationSteps"], message: "Render quality must declare the exact aggregate simulation-step count." })
64
+ }
65
+ })
66
+
67
+ export const SpatialRenderEffectsBindingSchema = z.strictObject({
68
+ document: SpatialRenderEffectsDocumentSchema,
69
+ documentSha256: SpatialDigestSchema,
70
+ }).superRefine((binding, context) => {
71
+ if (spatialRenderEffectsSha256(binding.document) !== binding.documentSha256) {
72
+ context.addIssue({ code: "custom", path: ["documentSha256"], message: "Render-effects digest does not match the canonical document." })
73
+ }
74
+ })
75
+
76
+ export type SpatialRenderEffectsDocument = Readonly<z.infer<typeof SpatialRenderEffectsDocumentSchema>>
77
+ export type SpatialRenderEffectsBinding = Readonly<z.infer<typeof SpatialRenderEffectsBindingSchema>>
78
+
79
+ export function parseSpatialRenderEffectsDocument(input: unknown): SpatialRenderEffectsDocument {
80
+ return deepFreezeJson(parseSpatialValue(SpatialRenderEffectsDocumentSchema, input, "render effects"))
81
+ }
82
+
83
+ export function spatialRenderEffectsAssetIds(document: SpatialRenderEffectsDocument): readonly string[] {
84
+ return Object.freeze([...new Set([
85
+ ...spatialRenderPlanAssetIds(document.renderPlan),
86
+ ...document.particleSystems.flatMap(binding => spatialParticleAssetIds(binding.system)),
87
+ ])].sort())
88
+ }
89
+
90
+ export function spatialRenderEffectsSha256(document: SpatialRenderEffectsDocument): string {
91
+ return spatialValueSha256(document)
92
+ }
93
+
94
+ export const SPATIAL_EFFECTS_CHECK_LIMITS = Object.freeze({
95
+ findings: 256,
96
+ })
97
+
98
+ export const SpatialEffectsCheckFindingSchema = z.strictObject({
99
+ code: z.enum(["stale-scene", "unresolved-entity", "unresolved-asset"]),
100
+ severity: z.enum(["error", "warning"]),
101
+ referenceId: z.string().min(1).max(128).optional(),
102
+ detail: z.string().min(1).max(240),
103
+ })
104
+ export type SpatialEffectsCheckFinding = Readonly<z.infer<typeof SpatialEffectsCheckFindingSchema>>
105
+
106
+ export const SpatialEffectsCheckReportSchema = z.strictObject({
107
+ kind: z.literal("slopcamera.spatial-render-effects-check"),
108
+ schemaVersion: z.literal(1),
109
+ documentSha256: SpatialDigestSchema,
110
+ sceneSha256: SpatialDigestSchema,
111
+ findings: z.array(SpatialEffectsCheckFindingSchema).max(SPATIAL_EFFECTS_CHECK_LIMITS.findings),
112
+ counts: z.strictObject({
113
+ particleSystems: z.number().int().min(0),
114
+ simulationBakes: z.number().int().min(0),
115
+ postProcessSteps: z.number().int().min(0),
116
+ errors: z.number().int().min(0),
117
+ warnings: z.number().int().min(0),
118
+ }),
119
+ })
120
+ export type SpatialEffectsCheckReport = Readonly<z.infer<typeof SpatialEffectsCheckReportSchema>>
121
+
122
+ const effectsCheckInputSchema = z.strictObject({
123
+ effects: z.unknown(),
124
+ scene: z.unknown(),
125
+ })
126
+
127
+ const parseEffectsInput = (input: unknown): { document: SpatialRenderEffectsDocument; documentSha256: string } => {
128
+ if (input !== null && typeof input === "object" && "document" in input) {
129
+ const binding = parseSpatialValue(SpatialRenderEffectsBindingSchema, input, "render effects binding")
130
+ return { document: binding.document, documentSha256: binding.documentSha256 }
131
+ }
132
+ const document = parseSpatialRenderEffectsDocument(input)
133
+ return { document, documentSha256: spatialRenderEffectsSha256(document) }
134
+ }
135
+
136
+ /**
137
+ * Validates an effects document or {document, documentSha256} binding against
138
+ * one scene: stale scene digests and unresolved entity/asset references report
139
+ * as errors before any host work begins.
140
+ */
141
+ export function checkSpatialRenderEffects(input: unknown): SpatialEffectsCheckReport {
142
+ const options = parseSpatialValue(effectsCheckInputSchema, input, "render effects check")
143
+ const scene = parseSpatialValue(SpatialSceneV1Schema, options.scene, "scene")
144
+ const sceneSha256 = spatialValueSha256(scene)
145
+ const { document, documentSha256 } = parseEffectsInput(options.effects)
146
+ const findings: SpatialEffectsCheckFinding[] = []
147
+ const finding = (code: SpatialEffectsCheckFinding["code"], detail: string, referenceId?: string): void => {
148
+ findings.push(deepFreezeJson(referenceId === undefined
149
+ ? { code, severity: "error", detail }
150
+ : { code, severity: "error", referenceId, detail }))
151
+ }
152
+
153
+ if (document.sceneSha256 !== sceneSha256) {
154
+ finding("stale-scene", `Effects document binds scene ${document.sceneSha256} but the checked scene digests to ${sceneSha256}.`)
155
+ }
156
+ const entityIds = new Set(scene.entities.map((entity) => entity.entityId))
157
+ for (const binding of document.particleSystems) {
158
+ if (!entityIds.has(binding.system.entityId)) {
159
+ finding("unresolved-entity", `Particle system on ${binding.system.entityId} does not resolve to a scene entity.`, binding.system.entityId)
160
+ }
161
+ }
162
+ const assetIds = new Set(scene.assets.map((asset) => asset.assetId))
163
+ for (const assetId of spatialRenderEffectsAssetIds(document)) {
164
+ if (!assetIds.has(assetId)) {
165
+ finding("unresolved-asset", `Effects document references asset ${assetId} absent from the scene manifest.`, assetId)
166
+ }
167
+ }
168
+
169
+ const errors = findings.filter((item) => item.severity === "error").length
170
+ return deepFreezeJson(SpatialEffectsCheckReportSchema.parse({
171
+ kind: "slopcamera.spatial-render-effects-check",
172
+ schemaVersion: 1,
173
+ documentSha256,
174
+ sceneSha256,
175
+ findings,
176
+ counts: {
177
+ particleSystems: document.particleSystems.length,
178
+ simulationBakes: document.simulationBakes.length,
179
+ postProcessSteps: document.renderPlan.postProcess?.steps.length ?? 0,
180
+ errors,
181
+ warnings: findings.length - errors,
182
+ },
183
+ }))
184
+ }
185
+
186
+ const effectsPlanInputSchema = z.strictObject({
187
+ scene: z.unknown(),
188
+ renderPlan: z.unknown(),
189
+ particleSystems: z.array(z.unknown()).max(SPATIAL_RENDER_EFFECTS_LIMITS.particleSystems).default([]),
190
+ simulationBakes: z.array(z.unknown()).max(SPATIAL_RENDER_EFFECTS_LIMITS.simulationBakes).default([]),
191
+ })
192
+
193
+ /**
194
+ * Assembles a canonical {document, documentSha256} effects binding for one
195
+ * scene. Entity and asset closure reject before the document is formed; the
196
+ * scene digest binds here, never by caller assertion.
197
+ */
198
+ export function planSpatialRenderEffects(input: unknown): SpatialRenderEffectsBinding {
199
+ const options = parseSpatialValue(effectsPlanInputSchema, input, "render effects plan")
200
+ const scene = parseSpatialValue(SpatialSceneV1Schema, options.scene, "scene")
201
+ const sceneSha256 = spatialValueSha256(scene)
202
+ const renderPlan = parseSpatialValue(SpatialRenderPlanSchema, options.renderPlan, "render plan")
203
+ const particleSystems = options.particleSystems.map((system) =>
204
+ parseSpatialValue(SpatialParticleSystemSchema, system, "particle system"))
205
+ const simulationBakes = options.simulationBakes.map((receipt) =>
206
+ parseSpatialValue(SpatialSimulationBakeReceiptSchema, receipt, "simulation bake receipt"))
207
+
208
+ const entityIds = new Set(scene.entities.map((entity) => entity.entityId))
209
+ for (const system of particleSystems) {
210
+ if (!entityIds.has(system.entityId)) {
211
+ throw new SpatialSceneError("invalid-data", `Particle system on ${system.entityId} does not resolve to a scene entity.`, "particleSystems")
212
+ }
213
+ }
214
+ const assetIds = new Set(scene.assets.map((asset) => asset.assetId))
215
+ const referenced = [...spatialRenderPlanAssetIds(renderPlan), ...particleSystems.flatMap((system) => spatialParticleAssetIds(system))]
216
+ for (const assetId of new Set(referenced)) {
217
+ if (!assetIds.has(assetId)) {
218
+ throw new SpatialSceneError("invalid-data", `Effects input references asset ${assetId} absent from the scene manifest.`, "effects")
219
+ }
220
+ }
221
+ for (const receipt of simulationBakes) {
222
+ if (receipt.sourceDigest !== sceneSha256) {
223
+ throw new SpatialSceneError("invalid-data", `Simulation bake ${receipt.cacheId} belongs to scene ${receipt.sourceDigest}, not ${sceneSha256}.`, "simulationBakes")
224
+ }
225
+ }
226
+
227
+ const document = parseSpatialRenderEffectsDocument({
228
+ kind: "slopcamera.spatial-render-effects",
229
+ schemaVersion: 1,
230
+ sceneSha256,
231
+ renderPlan,
232
+ renderPlanSha256: spatialRenderPlanSha256(renderPlan),
233
+ particleSystems: particleSystems.map((system) => ({ system, systemSha256: spatialParticleSystemSha256(system) })),
234
+ simulationBakes,
235
+ })
236
+ return deepFreezeJson(SpatialRenderEffectsBindingSchema.parse({
237
+ document,
238
+ documentSha256: spatialRenderEffectsSha256(document),
239
+ }))
240
+ }