@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.
- package/NOTICE.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +65 -20
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +6 -9
- package/apps/desktop/application/default-registry.ts +26 -10
- package/apps/desktop/application/operation.ts +14 -5
- package/apps/desktop/application/operations/index.ts +4 -1
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +528 -97
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +31 -2
- package/apps/desktop/cli/commands.ts +1215 -70
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +183 -23
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +26 -50
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +18 -5
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +24 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +737 -291
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +32 -434
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -240
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +1 -1
- package/package.json +11 -24
- package/skills/slopcamera/SKILL.md +14 -1
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +250 -102
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -23
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-mcy8z0br.js +0 -192
- package/dist/index-p63wavx0.js +0 -2029
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-rvghzt7b.js +0 -2013
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-z7239b4h.js +0 -2363
- package/dist/skill-install-dh0ntqba.js +0 -10
- 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
|
|
109
|
+
const context = createSpatialEvaluationContext(sceneInput)
|
|
110
|
+
const scene = context.scene
|
|
78
111
|
const shot = parseSpatialValue(SpatialShotV1Schema, shotInput, "shot")
|
|
79
|
-
if (shot.sceneSha256 !==
|
|
80
|
-
if (!
|
|
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 &&
|
|
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
|
-
/**
|
|
89
|
-
|
|
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 =
|
|
172
|
+
let camera = context.camerasById.get(capturedOptions.cameraId)
|
|
99
173
|
if (!camera) throw new SpatialSceneError("not-found", `Unknown camera ${capturedOptions.cameraId}.`, "cameraId")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
+
}
|