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