@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,125 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { deepFreezeJson } from "../code/json-snapshot.js"
|
|
3
|
+
import { SpatialCameraIdSchema, SpatialCameraSchema, SpatialEntityIdSchema, SpatialFrameRateSchema, SpatialPoseSchema, SpatialTimeUsSchema, SpatialVec3Schema, type SpatialCamera } from "./contracts.js"
|
|
4
|
+
import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
|
|
5
|
+
import { parseSpatialCameraTrack, SPATIAL_CAMERA_TRACK_MAX_FRAMES, type SpatialCameraTrack } from "./camera-track.js"
|
|
6
|
+
import { reduceSpatialFrameRate, spatialFrameCount, spatialFrameSample } from "./time.js"
|
|
7
|
+
|
|
8
|
+
const scalar = z.number().finite().min(-1_000_000).max(1_000_000)
|
|
9
|
+
const positive = z.number().finite().positive().max(1_000_000)
|
|
10
|
+
const target = z.strictObject({ entityId: SpatialEntityIdSchema.optional(), position: SpatialVec3Schema, radiusM: positive })
|
|
11
|
+
const timing = { startUs: SpatialTimeUsSchema, endUs: SpatialTimeUsSchema }
|
|
12
|
+
const base = { cameraId: SpatialCameraIdSchema, ...timing, easing: z.enum(["linear", "smoothstep", "smootherstep"]).optional() }
|
|
13
|
+
const shake = z.strictObject({ seed: z.number().int().min(0).max(0xffff_ffff), amplitudeM: z.number().finite().min(0).max(10), frequencyHz: z.number().finite().min(0.01).max(100), layers: z.number().int().min(1).max(8) })
|
|
14
|
+
export const SpatialCameraRigSchema = z.discriminatedUnion("kind", [
|
|
15
|
+
z.strictObject({ ...base, kind: z.literal("dolly"), from: SpatialVec3Schema, to: SpatialVec3Schema, target }),
|
|
16
|
+
z.strictObject({ ...base, kind: z.literal("crane"), from: SpatialVec3Schema, to: SpatialVec3Schema, target }),
|
|
17
|
+
z.strictObject({ ...base, kind: z.literal("orbit"), center: SpatialVec3Schema, radiusM: positive, startAngleRad: scalar, endAngleRad: scalar, heightM: scalar, target }),
|
|
18
|
+
z.strictObject({ ...base, kind: z.literal("rail"), points: z.array(SpatialVec3Schema).min(2).max(64), target }),
|
|
19
|
+
z.strictObject({ ...base, kind: z.literal("handheld"), pose: SpatialPoseSchema, target, shake }),
|
|
20
|
+
z.strictObject({ ...base, kind: z.literal("chase"), target, offset: SpatialVec3Schema, fromTarget: SpatialVec3Schema.optional(), toTarget: SpatialVec3Schema.optional(), shake: shake.optional() }),
|
|
21
|
+
z.strictObject({ ...base, kind: z.literal("tripod"), pose: SpatialPoseSchema, target }),
|
|
22
|
+
z.strictObject({ ...base, kind: z.literal("target-tracking"), from: SpatialVec3Schema, to: SpatialVec3Schema, target, targetEnd: SpatialVec3Schema.optional() }),
|
|
23
|
+
]).refine(value => value.endUs > value.startUs, "Camera rig duration must be nonempty.")
|
|
24
|
+
export type SpatialCameraRig = Readonly<z.infer<typeof SpatialCameraRigSchema>>
|
|
25
|
+
|
|
26
|
+
export const SpatialRackFocusSchema = z.strictObject({ startDistanceM: positive, endDistanceM: positive })
|
|
27
|
+
export type SpatialRackFocus = Readonly<z.infer<typeof SpatialRackFocusSchema>>
|
|
28
|
+
const compileSchema = z.strictObject({ camera: SpatialCameraSchema, rig: SpatialCameraRigSchema, frameRate: SpatialFrameRateSchema, frameCount: z.number().int().min(1).max(SPATIAL_CAMERA_TRACK_MAX_FRAMES), rackFocus: SpatialRackFocusSchema.optional() })
|
|
29
|
+
|
|
30
|
+
const mix = (a: number, b: number, t: number) => a + (b - a) * t
|
|
31
|
+
const vec = (a: readonly number[], b: readonly number[], t: number): [number, number, number] => [mix(a[0]!, b[0]!, t), mix(a[1]!, b[1]!, t), mix(a[2]!, b[2]!, t)]
|
|
32
|
+
function eased(t: number, kind: SpatialCameraRig["easing"]): number { return kind === "smoothstep" ? t*t*(3-2*t) : kind === "smootherstep" ? t*t*t*(t*(t*6-15)+10) : t }
|
|
33
|
+
function hash(seed: number, n: number): number { let x = (seed ^ Math.imul(n + 1, 0x9e3779b1)) >>> 0; x ^= x >>> 16; x = Math.imul(x, 0x7feb352d); x ^= x >>> 15; return ((x >>> 0) / 0x1_0000_0000) * 2 - 1 }
|
|
34
|
+
function shakeAt(spec: z.infer<typeof shake> | undefined, seconds: number): [number, number, number] {
|
|
35
|
+
if (!spec) return [0,0,0]
|
|
36
|
+
const out: [number,number,number] = [0,0,0]
|
|
37
|
+
for (let layer=0; layer<spec.layers; layer++) for (let axis=0; axis<3; axis++) out[axis] = out[axis]! + Math.sin(seconds * spec.frequencyHz * (layer+1) * Math.PI*2 + hash(spec.seed, layer*3+axis)*Math.PI) * spec.amplitudeM / (spec.layers * (layer+1))
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
const normalize = (v: readonly number[]): [number,number,number] => { const n=Math.hypot(...v); return [v[0]!/n,v[1]!/n,v[2]!/n] }
|
|
41
|
+
const cross = (a:readonly number[],b:readonly number[]):[number,number,number] => [a[1]!*b[2]!-a[2]!*b[1]!,a[2]!*b[0]!-a[0]!*b[2]!,a[0]!*b[1]!-a[1]!*b[0]!]
|
|
42
|
+
function quaternionFromBasis(x:readonly number[],y:readonly number[],z:readonly number[]): [number,number,number,number] {
|
|
43
|
+
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; let q:[number,number,number,number]
|
|
44
|
+
if(trace>0){const s=2*Math.sqrt(trace+1);q=[(m21-m12)/s,(m02-m20)/s,(m10-m01)/s,s/4]}
|
|
45
|
+
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]}
|
|
46
|
+
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]}
|
|
47
|
+
else {const s=2*Math.sqrt(1+m22-m00-m11);q=[(m02+m20)/s,(m12+m21)/s,s/4,(m10-m01)/s]}
|
|
48
|
+
const n=Math.hypot(...q); return q.map(v=>v/n) as [number,number,number,number]
|
|
49
|
+
}
|
|
50
|
+
function lookAt(position: readonly number[], targetPosition: readonly number[]): [number,number,number,number] {
|
|
51
|
+
const forward=[targetPosition[0]!-position[0]!,targetPosition[1]!-position[1]!,targetPosition[2]!-position[2]!]
|
|
52
|
+
if(Math.hypot(...forward)<1e-9) throw new SpatialSceneError("invalid-data","Camera and look-at target must not coincide.")
|
|
53
|
+
const f=normalize(forward), up=Math.abs(f[1]!)>.999999?[0,0,1]:[0,1,0], right=normalize(cross(f,up)), correctedUp=cross(right,f)
|
|
54
|
+
return quaternionFromBasis(right,correctedUp,[-f[0],-f[1],-f[2]])
|
|
55
|
+
}
|
|
56
|
+
function pointOnRail(points: readonly (readonly number[])[], t: number): [number,number,number] {
|
|
57
|
+
// Chord-length parameterization gives authored segments proportional time and avoids speed jumps caused solely by unequal spacing.
|
|
58
|
+
const lengths=points.slice(1).map((p,i)=>Math.hypot(p[0]!-points[i]![0]!,p[1]!-points[i]![1]!,p[2]!-points[i]![2]!)), total=lengths.reduce((a,b)=>a+b,0)
|
|
59
|
+
if(total===0) return [...points[0]!] as [number,number,number]
|
|
60
|
+
let distance=t*total,index=0; while(index<lengths.length-1&&distance>lengths[index]!){distance-=lengths[index]!;index++}
|
|
61
|
+
return vec(points[index]!,points[index+1]!,lengths[index]===0?0:distance/lengths[index]!)
|
|
62
|
+
}
|
|
63
|
+
function sampleRig(rig: SpatialCameraRig, t: number): { position:[number,number,number], target:[number,number,number] } {
|
|
64
|
+
const u=eased(t,rig.easing); let position:[number,number,number], aim:[number,number,number]=[...rig.target.position]
|
|
65
|
+
if (rig.kind === "orbit") position=[rig.center[0]+Math.sin(mix(rig.startAngleRad,rig.endAngleRad,u))*rig.radiusM,rig.center[1]+rig.heightM,rig.center[2]+Math.cos(mix(rig.startAngleRad,rig.endAngleRad,u))*rig.radiusM]
|
|
66
|
+
else if (rig.kind === "rail") position=pointOnRail(rig.points,u)
|
|
67
|
+
else if (rig.kind === "handheld" || rig.kind === "tripod") position=[...rig.pose.position]
|
|
68
|
+
else if (rig.kind === "chase") { aim=vec(rig.fromTarget??rig.target.position,rig.toTarget??rig.target.position,u); position=[aim[0]+rig.offset[0],aim[1]+rig.offset[1],aim[2]+rig.offset[2]] }
|
|
69
|
+
else position=vec(rig.from,rig.to,u)
|
|
70
|
+
if (rig.kind === "target-tracking" && rig.targetEnd) aim=vec(rig.target.position,rig.targetEnd,u)
|
|
71
|
+
const s=shakeAt(rig.kind === "handheld" || rig.kind === "chase" ? rig.shake : undefined,(rig.endUs-rig.startUs)*t/1e6)
|
|
72
|
+
return { position:[position[0]+s[0],position[1]+s[1],position[2]+s[2]], target:aim }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Compiles semantic motion to an ordinary immutable camera track; no runtime rig or random source remains. */
|
|
76
|
+
export function compileSpatialCameraRig(input: unknown): SpatialCameraTrack {
|
|
77
|
+
const value=parseSpatialValue(compileSchema,input,"camera rig compilation"), rate=reduceSpatialFrameRate(value.frameRate), duration=value.rig.endUs-value.rig.startUs
|
|
78
|
+
if (value.camera.cameraId !== value.rig.cameraId) throw new SpatialSceneError("conflict","Rig and camera identities differ.")
|
|
79
|
+
if (spatialFrameCount(duration,rate)!==value.frameCount) throw new SpatialSceneError("invalid-data","frameCount must exactly cover the rig's half-open duration at frameRate.")
|
|
80
|
+
const clock={startUs:value.rig.startUs,frameRate:rate,frameCount:value.frameCount}
|
|
81
|
+
const samples=Array.from({length:value.frameCount},(_,frameIndex)=>{ const relative=spatialFrameSample(frameIndex,duration,rate), t=Number(relative.exactTimeUs.numerator)/Number(relative.exactTimeUs.denominator)/duration, sampled=sampleRig(value.rig,t), denominator=BigInt(relative.exactTimeUs.denominator); const lens=value.rackFocus ? { ...(value.camera.lens ?? {focalLengthMm:50,sensorWidthMm:36}), focusDistanceM:mix(value.rackFocus.startDistanceM,value.rackFocus.endDistanceM,eased(t,value.rig.easing)) } : value.camera.lens
|
|
82
|
+
return {frameIndex,timeUs:value.rig.startUs+relative.timeUs,exactTimeUs:{numerator:String(BigInt(value.rig.startUs)*denominator+BigInt(relative.exactTimeUs.numerator)),denominator:String(denominator)},camera:{...value.camera,pose:{position:sampled.position,rotation:lookAt(sampled.position,sampled.target)},...(lens===undefined?{}:{lens})}}
|
|
83
|
+
})
|
|
84
|
+
return parseSpatialCameraTrack({kind:"slopcamera.spatial-camera-track",schemaVersion:1,sceneSha256:spatialValueSha256({domain:"slopcamera.camera-rig.v1",camera:value.camera,rig:value.rig,frameRate:rate,frameCount:value.frameCount,rackFocus:value.rackFocus??null}),cameraId:value.camera.cameraId,clock,samples})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const framingTolerance=z.number().finite().min(0).max(.5).default(.05)
|
|
88
|
+
export const SpatialFramingGoalSchema = z.discriminatedUnion("kind", [
|
|
89
|
+
z.strictObject({kind:z.literal("close-up"),targets:z.array(target).min(1).max(3),tolerance:framingTolerance}), z.strictObject({kind:z.literal("medium"),targets:z.array(target).min(1).max(3),tolerance:framingTolerance}), z.strictObject({kind:z.literal("wide"),targets:z.array(target).min(1).max(3),tolerance:framingTolerance}), z.strictObject({kind:z.literal("two-shot"),targets:z.array(target).length(2),tolerance:framingTolerance}), z.strictObject({kind:z.literal("over-shoulder"),targets:z.array(target).min(2).max(3),tolerance:framingTolerance}),
|
|
90
|
+
z.strictObject({kind:z.literal("screen-position"),targets:z.array(target).length(1),position:z.tuple([z.number().finite().min(0).max(1),z.number().finite().min(0).max(1)]),tolerance:framingTolerance}), z.strictObject({kind:z.literal("headroom"),targets:z.array(target).length(1),headroom:z.number().finite().min(0).max(.5),tolerance:framingTolerance}), z.strictObject({kind:z.literal("rule-of-thirds"),targets:z.array(target).length(1),quadrant:z.enum(["upper-left","upper-right","lower-left","lower-right"]),tolerance:framingTolerance}),
|
|
91
|
+
])
|
|
92
|
+
export type SpatialFramingResult = Readonly<{ satisfied:true; camera:SpatialCamera } | { satisfied:false; report:{ code:"missing-bounds"|"impossible"|"unsatisfied"; goal:string; entityIds:readonly string[]; detail:string; deviation?:number } }>
|
|
93
|
+
/** Solves spherical world bounds against the camera's calibrated pixel intrinsics. */
|
|
94
|
+
export function solveSpatialFraming(cameraInput: unknown, goalInput: unknown): SpatialFramingResult {
|
|
95
|
+
const camera=parseSpatialValue(SpatialCameraSchema,cameraInput,"framing camera"), goal=parseSpatialValue(SpatialFramingGoalSchema,goalInput,"framing goal"), ids=goal.targets.flatMap(x=>x.entityId?[x.entityId]:[])
|
|
96
|
+
if (ids.length!==goal.targets.length) return deepFreezeJson({satisfied:false,report:{code:"missing-bounds",goal:goal.kind,entityIds:ids,detail:"Every framing target requires supplied known bounds and identity."}})
|
|
97
|
+
if(camera.projection.kind!=="perspective") return deepFreezeJson({satisfied:false,report:{code:"impossible",goal:goal.kind,entityIds:ids,detail:"Semantic framing requires a perspective camera."}})
|
|
98
|
+
const p=camera.projection, min=goal.targets.reduce((a,x)=>[Math.min(a[0],x.position[0]-x.radiusM),Math.min(a[1],x.position[1]-x.radiusM),Math.min(a[2],x.position[2]-x.radiusM)] as [number,number,number],[Infinity,Infinity,Infinity] as [number,number,number]), max=goal.targets.reduce((a,x)=>[Math.max(a[0],x.position[0]+x.radiusM),Math.max(a[1],x.position[1]+x.radiusM),Math.max(a[2],x.position[2]+x.radiusM)] as [number,number,number],[-Infinity,-Infinity,-Infinity] as [number,number,number]), center=vec(min,max,.5), halfW=(max[0]-min[0])/2, halfH=(max[1]-min[1])/2, depthRadius=(max[2]-min[2])/2
|
|
99
|
+
const fill=goal.kind==="close-up"?.8:goal.kind==="medium"?.55:goal.kind==="wide"?.3:goal.kind==="over-shoulder"?.7:.6
|
|
100
|
+
const depth=Math.max(halfW*p.fx/(p.width*fill/2),halfH*p.fy/(p.height*fill/2),p.near+depthRadius+1e-6)
|
|
101
|
+
if(depth+depthRadius>=p.far) return deepFreezeJson({satisfied:false,report:{code:"impossible",goal:goal.kind,entityIds:ids,detail:"Target bounds cannot fit inside the camera clipping range."}})
|
|
102
|
+
let desiredX=.5,desiredY=.5; if(goal.kind==="screen-position") [desiredX,desiredY]=goal.position; else if(goal.kind==="rule-of-thirds"){desiredX=goal.quadrant.endsWith("left")?1/3:2/3;desiredY=goal.quadrant.startsWith("upper")?1/3:2/3}else if(goal.kind==="headroom") desiredY=goal.headroom+(halfH*p.fy/depth)/p.height
|
|
103
|
+
// For a -Z camera with no roll, pixel = principal point + focal * camera-space coordinate / depth.
|
|
104
|
+
const centerPixelX=desiredX*p.width,centerPixelY=desiredY*p.height, localCenter:[number,number,number]=[(centerPixelX-p.cx)*depth/p.fx,-(centerPixelY-p.cy)*depth/p.fy,-depth]
|
|
105
|
+
const q=camera.pose.rotation, [qx,qy,qz,qw]=q, [lx,ly,lz]=localCenter, tx=2*(qy*lz-qz*ly),ty=2*(qz*lx-qx*lz),tz=2*(qx*ly-qy*lx), worldOffset:[number,number,number]=[lx+qw*tx+(qy*tz-qz*ty),ly+qw*ty+(qz*tx-qx*tz),lz+qw*tz+(qx*ty-qy*tx)], position:[number,number,number]=[center[0]-worldOffset[0],center[1]-worldOffset[1],center[2]-worldOffset[2]], solved={...camera,pose:{position,rotation:q}}
|
|
106
|
+
const actual=worldToCamera([center[0]-position[0],center[1]-position[1],center[2]-position[2]],q), actualDepth=-actual[2], projectedX=p.cx+actual[0]*p.fx/actualDepth, projectedY=p.cy-actual[1]*p.fy/actualDepth, deviation=Math.hypot(projectedX/p.width-desiredX,projectedY/p.height-desiredY)
|
|
107
|
+
return deviation<=goal.tolerance+1e-12 ? deepFreezeJson({satisfied:true,camera:solved}) : deepFreezeJson({satisfied:false,report:{code:"unsatisfied",goal:goal.kind,entityIds:ids,detail:"Calibrated framing exceeds the declared normalized-screen tolerance.",deviation}})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type SpatialCameraAuditFinding = Readonly<{ kind:"clipping"|"collision"|"acceleration"|"framing-deviation"|"focus-error"|"angular-velocity"; cameraId:string; frameIndex:number; timeUs:number; entityId?:string | undefined; measured:number; limit:number }>
|
|
111
|
+
const auditOptions=z.strictObject({subjects:z.array(target).max(128).default([]),collisionBounds:z.array(target).max(128).default([]),cameraCollisionRadiusM:z.number().finite().min(0).max(100).default(0),maxAcceleration:z.number().finite().positive().max(1_000_000).default(50),maxAngularVelocity:z.number().finite().positive().max(100_000).default(4),maxFramingDeviation:z.number().finite().min(0).max(1).default(.1),maxFocusErrorM:z.number().finite().positive().max(1_000_000).default(.5)})
|
|
112
|
+
function worldToCamera(delta:readonly number[],q:readonly number[]):[number,number,number]{const [x,y,z,w]=q, tx=2*(-y!*delta[2]!+z!*delta[1]!),ty=2*(-z!*delta[0]!+x!*delta[2]!),tz=2*(-x!*delta[1]!+y!*delta[0]!);return [delta[0]!+w!*tx+(-y!*tz+z!*ty),delta[1]!+w!*ty+(-z!*tx+x!*tz),delta[2]!+w!*tz+(-x!*ty+y!*tx)]}
|
|
113
|
+
export function auditSpatialCameraTrack(trackInput: unknown, optionsInput: unknown): readonly SpatialCameraAuditFinding[] {
|
|
114
|
+
const track=parseSpatialCameraTrack(trackInput), options=parseSpatialValue(auditOptions,optionsInput,"camera audit options"), findings:SpatialCameraAuditFinding[]=[]
|
|
115
|
+
const add=(sample:SpatialCameraTrack["samples"][number],finding:Omit<SpatialCameraAuditFinding,"cameraId"|"frameIndex"|"timeUs">)=>findings.push({cameraId:track.cameraId,frameIndex:sample.frameIndex,timeUs:sample.timeUs,...finding})
|
|
116
|
+
for(let i=0;i<track.samples.length;i++){const s=track.samples[i]!,p=s.camera.pose.position,proj=s.camera.projection
|
|
117
|
+
for(const subject of options.subjects){const delta=[subject.position[0]-p[0],subject.position[1]-p[1],subject.position[2]-p[2]],local=worldToCamera(delta,s.camera.pose.rotation),depth=-local[2]!, nearEdge=depth-subject.radiusM,farEdge=depth+subject.radiusM;if(nearEdge<proj.near||farEdge>proj.far)add(s,{kind:"clipping",entityId:subject.entityId,measured:depth,limit:nearEdge<proj.near?proj.near:proj.far});const focus=s.camera.lens?.focusDistanceM;if(focus!==undefined&&Math.abs(focus-depth)>options.maxFocusErrorM)add(s,{kind:"focus-error",entityId:subject.entityId,measured:Math.abs(focus-depth),limit:options.maxFocusErrorM});const lateral=Math.hypot(local[0],local[1]),deviation=depth<=0?1:Math.atan2(lateral,depth)/Math.PI;if(deviation>options.maxFramingDeviation)add(s,{kind:"framing-deviation",entityId:subject.entityId,measured:deviation,limit:options.maxFramingDeviation})}
|
|
118
|
+
for(const bound of options.collisionBounds){const d=Math.hypot(bound.position[0]-p[0],bound.position[1]-p[1],bound.position[2]-p[2]),limit=bound.radiusM+options.cameraCollisionRadiusM;if(d<limit)add(s,{kind:"collision",entityId:bound.entityId,measured:d,limit})}
|
|
119
|
+
if(i>=2){const a=track.samples[i-2]!,b=track.samples[i-1]!,dt=(s.timeUs-b.timeUs)/1e6,dt0=(b.timeUs-a.timeUs)/1e6;if(dt>0&&dt0>0){const acceleration=Math.hypot(...([0,1,2].map(k=>(p[k]!-b.camera.pose.position[k]!)/dt-(b.camera.pose.position[k]!-a.camera.pose.position[k]!)/dt0) as [number,number,number]))/((dt+dt0)/2);if(acceleration>options.maxAcceleration)add(s,{kind:"acceleration",measured:acceleration,limit:options.maxAcceleration})}}
|
|
120
|
+
if(i>0){const b=track.samples[i-1]!,seconds=(s.timeUs-b.timeUs)/1e6;if(seconds>0){const raw=s.camera.pose.rotation.reduce((sum,v,k)=>sum+v*b.camera.pose.rotation[k]!,0),dot=Math.min(1,Math.max(-1,Math.abs(raw))),velocity=2*Math.acos(dot)/seconds;if(velocity>options.maxAngularVelocity)add(s,{kind:"angular-velocity",measured:velocity,limit:options.maxAngularVelocity})}}
|
|
121
|
+
}
|
|
122
|
+
const kindOrder={clipping:0,collision:1,acceleration:2,"framing-deviation":3,"focus-error":4,"angular-velocity":5} as const
|
|
123
|
+
findings.sort((a,b)=>a.frameIndex-b.frameIndex||kindOrder[a.kind]-kindOrder[b.kind]||(a.entityId??"").localeCompare(b.entityId??""))
|
|
124
|
+
return deepFreezeJson(findings)
|
|
125
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
2
|
import { deepFreezeJson } from "../code/json-snapshot.js"
|
|
3
3
|
import { SpatialCameraIdSchema, SpatialCameraSchema, SpatialDigestSchema, SpatialFrameRateSchema, SpatialTimeUsSchema } from "./contracts.js"
|
|
4
|
-
import {
|
|
4
|
+
import { createSpatialEvaluationContext, evaluateSpatialSceneInContext } from "./evaluate.js"
|
|
5
5
|
import { parseSpatialScene, parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
|
|
6
6
|
import { reduceSpatialFrameRate, spatialFrameSample } from "./time.js"
|
|
7
7
|
|
|
@@ -73,11 +73,13 @@ export function sampleSpatialCameraTrack(sceneInput: unknown, optionsInput: unkn
|
|
|
73
73
|
// scene once, then evaluate only this closed subset at each absolute time.
|
|
74
74
|
const cameraScene = { ...scene, cameras: [camera], entities: [], assets: [], generators: [], overrides: [],
|
|
75
75
|
animations: scene.animations.filter(channel => channel.targetId === cameraId) }
|
|
76
|
+
// One context for the closed camera subset; every frame reuses its indexes.
|
|
77
|
+
const cameraContext = createSpatialEvaluationContext(cameraScene)
|
|
76
78
|
return parseSpatialCameraTrack({ kind: "slopcamera.spatial-camera-track", schemaVersion: 1,
|
|
77
79
|
sceneSha256: spatialValueSha256(scene), cameraId, clock,
|
|
78
80
|
samples: Array.from({ length: clock.frameCount }, (_, frameIndex) => {
|
|
79
81
|
const sample = absoluteSample(frameIndex, clock)
|
|
80
|
-
return { ...sample, camera:
|
|
82
|
+
return { ...sample, camera: evaluateSpatialSceneInContext(cameraContext, { cameraId, timeUs: sample.timeUs }).camera }
|
|
81
83
|
}),
|
|
82
84
|
})
|
|
83
85
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { deepFreezeJson } from "../code/json-snapshot.js"
|
|
3
|
+
import { SpatialDigestSchema, SpatialTransformSchema } from "./contracts.js"
|
|
4
|
+
import { SPATIAL_GLB_LIMITS, SPATIAL_GLB_RIGGED_PROFILE, type SpatialGlbEvaluateOptions, type SpatialGlbModel } from "./gltf.js"
|
|
5
|
+
import { parseSpatialValue, SpatialSceneError } from "./identity.js"
|
|
6
|
+
import { composeTransform, multiplyTransforms, type Mat4 } from "./math.js"
|
|
7
|
+
|
|
8
|
+
export const HUMANOID_BONE_NAMES = [
|
|
9
|
+
"hips", "spine", "chest", "upperChest", "neck", "head",
|
|
10
|
+
"leftEye", "rightEye", "jaw",
|
|
11
|
+
"leftShoulder", "leftUpperArm", "leftLowerArm", "leftHand",
|
|
12
|
+
"leftUpperLeg", "leftLowerLeg", "leftFoot", "leftToes",
|
|
13
|
+
"rightShoulder", "rightUpperArm", "rightLowerArm", "rightHand",
|
|
14
|
+
"rightUpperLeg", "rightLowerLeg", "rightFoot", "rightToes",
|
|
15
|
+
"leftThumbProximal", "leftThumbIntermediate", "leftThumbDistal",
|
|
16
|
+
"leftIndexProximal", "leftIndexIntermediate", "leftIndexDistal",
|
|
17
|
+
"leftMiddleProximal", "leftMiddleIntermediate", "leftMiddleDistal",
|
|
18
|
+
"leftRingProximal", "leftRingIntermediate", "leftRingDistal",
|
|
19
|
+
"leftLittleProximal", "leftLittleIntermediate", "leftLittleDistal",
|
|
20
|
+
"rightThumbProximal", "rightThumbIntermediate", "rightThumbDistal",
|
|
21
|
+
"rightIndexProximal", "rightIndexIntermediate", "rightIndexDistal",
|
|
22
|
+
"rightMiddleProximal", "rightMiddleIntermediate", "rightMiddleDistal",
|
|
23
|
+
"rightRingProximal", "rightRingIntermediate", "rightRingDistal",
|
|
24
|
+
"rightLittleProximal", "rightLittleIntermediate", "rightLittleDistal",
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
export const CORE_HUMANOID_BONE_NAMES = [
|
|
28
|
+
"hips", "spine", "chest", "upperChest", "neck", "head",
|
|
29
|
+
"leftShoulder", "leftUpperArm", "leftLowerArm", "leftHand",
|
|
30
|
+
"rightShoulder", "rightUpperArm", "rightLowerArm", "rightHand",
|
|
31
|
+
"leftUpperLeg", "leftLowerLeg", "leftFoot",
|
|
32
|
+
"rightUpperLeg", "rightLowerLeg", "rightFoot",
|
|
33
|
+
] as const
|
|
34
|
+
|
|
35
|
+
export type SpatialHumanoidBoneName = typeof HUMANOID_BONE_NAMES[number]
|
|
36
|
+
|
|
37
|
+
const humanoidBoneName = z.enum(HUMANOID_BONE_NAMES)
|
|
38
|
+
|
|
39
|
+
const humanoidBoneMappingSchema = z.strictObject({
|
|
40
|
+
canonicalName: humanoidBoneName,
|
|
41
|
+
sourceNodeIndex: z.number().int().min(0).max(SPATIAL_GLB_LIMITS.nodes - 1),
|
|
42
|
+
restOffset: SpatialTransformSchema,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const SpatialHumanoidMappingSchema = z.strictObject({
|
|
46
|
+
kind: z.literal("slopcamera.spatial-humanoid-mapping"),
|
|
47
|
+
schemaVersion: z.literal(1),
|
|
48
|
+
sourceAssetSha256: SpatialDigestSchema,
|
|
49
|
+
sourceProfile: z.literal(SPATIAL_GLB_RIGGED_PROFILE),
|
|
50
|
+
bones: z.array(humanoidBoneMappingSchema).min(1).max(SPATIAL_GLB_LIMITS.jointsPerSkin),
|
|
51
|
+
}).superRefine((mapping, context) => {
|
|
52
|
+
const names = new Set<string>()
|
|
53
|
+
const nodes = new Set<number>()
|
|
54
|
+
for (const [index, bone] of mapping.bones.entries()) {
|
|
55
|
+
if (names.has(bone.canonicalName)) {
|
|
56
|
+
context.addIssue({ code: "custom", path: ["bones", index, "canonicalName"], message: "Canonical bone name must be unique." })
|
|
57
|
+
} else {
|
|
58
|
+
names.add(bone.canonicalName)
|
|
59
|
+
}
|
|
60
|
+
if (nodes.has(bone.sourceNodeIndex)) {
|
|
61
|
+
context.addIssue({ code: "custom", path: ["bones", index, "sourceNodeIndex"], message: "Source node index must be unique." })
|
|
62
|
+
} else {
|
|
63
|
+
nodes.add(bone.sourceNodeIndex)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const name of CORE_HUMANOID_BONE_NAMES) {
|
|
67
|
+
if (!names.has(name)) {
|
|
68
|
+
context.addIssue({ code: "custom", path: ["bones"], message: `Core bone ${name} is required.` })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export const SpatialHumanoidAttachmentSchema = z.strictObject({
|
|
74
|
+
kind: z.literal("slopcamera.spatial-humanoid-attachment"),
|
|
75
|
+
schemaVersion: z.literal(1),
|
|
76
|
+
name: z.string().min(1).max(256),
|
|
77
|
+
mapping: SpatialHumanoidMappingSchema,
|
|
78
|
+
bone: humanoidBoneName,
|
|
79
|
+
localOffset: SpatialTransformSchema,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T
|
|
83
|
+
export type SpatialHumanoidMapping = DeepReadonly<z.infer<typeof SpatialHumanoidMappingSchema>>
|
|
84
|
+
export type SpatialHumanoidAttachment = DeepReadonly<z.infer<typeof SpatialHumanoidAttachmentSchema>>
|
|
85
|
+
|
|
86
|
+
export function parseHumanoidMapping(input: unknown): SpatialHumanoidMapping {
|
|
87
|
+
const value = parseSpatialValue(SpatialHumanoidMappingSchema, input, "humanoid mapping")
|
|
88
|
+
return deepFreezeJson(value) as unknown as SpatialHumanoidMapping
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function parseHumanoidAttachment(input: unknown): SpatialHumanoidAttachment {
|
|
92
|
+
const value = parseSpatialValue(SpatialHumanoidAttachmentSchema, input, "humanoid attachment")
|
|
93
|
+
return deepFreezeJson(value) as unknown as SpatialHumanoidAttachment
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function evaluateHumanoidAttachmentMatrix(
|
|
97
|
+
attachment: SpatialHumanoidAttachment,
|
|
98
|
+
model: SpatialGlbModel,
|
|
99
|
+
options: SpatialGlbEvaluateOptions,
|
|
100
|
+
sourceAssetSha256: string,
|
|
101
|
+
): Mat4 {
|
|
102
|
+
if (attachment.mapping.sourceAssetSha256 !== sourceAssetSha256) {
|
|
103
|
+
throw new SpatialSceneError("invalid-data", "Stale source asset digest.", "character")
|
|
104
|
+
}
|
|
105
|
+
if (attachment.mapping.sourceProfile !== model.profile || model.rigFacts === undefined) {
|
|
106
|
+
throw new SpatialSceneError("invalid-data", "Source profile does not match or model is not rigged.", "character")
|
|
107
|
+
}
|
|
108
|
+
const joints = new Set<number>()
|
|
109
|
+
for (const skin of model.rigFacts.skins) {
|
|
110
|
+
for (const node of skin.jointNodeIndices) {
|
|
111
|
+
joints.add(node)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const boneByName = new Map(attachment.mapping.bones.map(bone => [bone.canonicalName, bone] as const))
|
|
115
|
+
const mapped = boneByName.get(attachment.bone)
|
|
116
|
+
if (mapped === undefined) {
|
|
117
|
+
throw new SpatialSceneError("not-found", `Canonical bone ${attachment.bone} is not mapped.`, "character")
|
|
118
|
+
}
|
|
119
|
+
if (!joints.has(mapped.sourceNodeIndex)) {
|
|
120
|
+
throw new SpatialSceneError("invalid-data", `Source node ${mapped.sourceNodeIndex} is not a rig joint.`, "character")
|
|
121
|
+
}
|
|
122
|
+
const world = model.jointWorldMatrix(options, mapped.sourceNodeIndex)
|
|
123
|
+
return multiplyTransforms(world, multiplyTransforms(composeTransform(mapped.restOffset), composeTransform(attachment.localOffset)))
|
|
124
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
|
+
import { SpatialPbrMaterialSchema, SpatialFogSchema } from "./material-lighting.js"
|
|
2
3
|
|
|
3
4
|
export const SPATIAL_SCENE_LIMITS = Object.freeze({
|
|
4
5
|
sourceBytes: 2_097_152,
|
|
@@ -69,11 +70,23 @@ export const SpatialProjectionSchema = z.discriminatedUnion("kind", [
|
|
|
69
70
|
context.addIssue({ code: "custom", message: "Orthographic extents must have positive width and height." })
|
|
70
71
|
}
|
|
71
72
|
})
|
|
73
|
+
const boundedPhysical = (minimum: number, maximum: number) => z.number().finite().min(minimum).max(maximum)
|
|
74
|
+
/** Optional physical metadata. Projection remains the calibrated rendering authority. */
|
|
75
|
+
export const SpatialCameraLensSchema = z.strictObject({
|
|
76
|
+
focalLengthMm: boundedPhysical(1, 2_000),
|
|
77
|
+
sensorWidthMm: boundedPhysical(1, 200),
|
|
78
|
+
apertureFStop: boundedPhysical(0.5, 128).optional(),
|
|
79
|
+
focusDistanceM: boundedPhysical(0.001, 1_000_000).optional(),
|
|
80
|
+
shutterAngleDeg: boundedPhysical(0, 360).optional(),
|
|
81
|
+
exposureEv: boundedPhysical(-32, 32).optional(),
|
|
82
|
+
colorTemperatureK: boundedPhysical(1_000, 40_000).optional(),
|
|
83
|
+
})
|
|
72
84
|
export const SpatialCameraSchema = z.strictObject({
|
|
73
85
|
cameraId: SpatialCameraIdSchema,
|
|
74
86
|
name: z.string().min(1).max(256),
|
|
75
87
|
pose: SpatialPoseSchema,
|
|
76
88
|
projection: SpatialProjectionSchema,
|
|
89
|
+
lens: SpatialCameraLensSchema.optional(),
|
|
77
90
|
})
|
|
78
91
|
|
|
79
92
|
const relativePath = z.string().min(1).max(1_024).refine(value =>
|
|
@@ -98,7 +111,9 @@ export const SpatialAssetInterpretationSchema = z.discriminatedUnion("kind", [
|
|
|
98
111
|
z.strictObject({ kind: z.literal("gltf"), format: z.enum(["glb", "gltf"]), metersPerUnit: positiveDimension, sourceUp: z.enum(["x", "y", "z"]) }),
|
|
99
112
|
z.strictObject({ kind: z.literal("font"), format: z.enum(["otf", "woff2"]), family: z.string().min(1).max(128) }),
|
|
100
113
|
z.strictObject({ kind: z.literal("splat"), format: z.enum(["spz", "ply"]), metersPerUnit: positiveDimension, sourceUp: z.enum(["x", "y", "z"]) }),
|
|
101
|
-
|
|
114
|
+
// `slopcamera.provider-metadata` retains verbatim provider bytes — unlike the
|
|
115
|
+
// versioned slopcamera.* documents, the payload is the provider's own JSON.
|
|
116
|
+
z.strictObject({ kind: z.literal("metadata"), format: z.literal("json"), schema: z.enum(["slopcamera.spatial-world-import", "slopcamera.world-labs-provenance", "slopcamera.spatial-asset-facts", "slopcamera.provider-metadata"]) }),
|
|
102
117
|
])
|
|
103
118
|
export const SpatialAssetManifestSchema = z.strictObject({
|
|
104
119
|
assetId: SpatialAssetIdSchema,
|
|
@@ -112,9 +127,15 @@ export const SpatialAssetManifestSchema = z.strictObject({
|
|
|
112
127
|
}),
|
|
113
128
|
})
|
|
114
129
|
const color = z.string().regex(/^#[a-fA-F0-9]{6}$/u)
|
|
130
|
+
/** Emissive contribution on a standard material: sRGB hex color times a bounded scalar intensity. */
|
|
131
|
+
export const SpatialEmissiveSchema = z.strictObject({
|
|
132
|
+
color,
|
|
133
|
+
intensity: z.number().finite().min(0).max(100_000),
|
|
134
|
+
})
|
|
115
135
|
export const SpatialMaterialSchema = z.discriminatedUnion("kind", [
|
|
116
|
-
z.strictObject({ kind: z.literal("unlit"), color, opacity: unit }),
|
|
117
|
-
z.strictObject({ kind: z.literal("standard"), color, opacity: unit, roughness: unit, metalness: unit }),
|
|
136
|
+
z.strictObject({ kind: z.literal("unlit"), color, opacity: unit, map: SpatialAssetIdSchema.optional() }),
|
|
137
|
+
z.strictObject({ kind: z.literal("standard"), color, opacity: unit, roughness: unit, metalness: unit, map: SpatialAssetIdSchema.optional(), emissive: SpatialEmissiveSchema.optional() }),
|
|
138
|
+
SpatialPbrMaterialSchema,
|
|
118
139
|
])
|
|
119
140
|
export const SpatialGeometrySchema = z.discriminatedUnion("kind", [
|
|
120
141
|
z.strictObject({ kind: z.literal("box"), size: z.tuple([positiveDimension, positiveDimension, positiveDimension]) }),
|
|
@@ -123,8 +144,21 @@ export const SpatialGeometrySchema = z.discriminatedUnion("kind", [
|
|
|
123
144
|
z.strictObject({ kind: z.literal("cylinder"), radius: positiveDimension, height: positiveDimension }),
|
|
124
145
|
z.strictObject({ kind: z.literal("asset"), assetId: SpatialAssetIdSchema, nodeIndex: z.number().int().min(0).max(65_535).optional(), materialMode: z.enum(["entity", "source"]).optional(),
|
|
125
146
|
clip: z.strictObject({ index: z.number().int().min(0).max(255), offsetUs: SpatialTimeUsSchema, playback: z.enum(["once", "loop", "freeze"]) }).optional(),
|
|
147
|
+
morphWeights: z.array(unit).max(16).optional(), // bound matches gltf.ts morphTargetsPerPrimitive
|
|
126
148
|
}),
|
|
127
149
|
])
|
|
150
|
+
/**
|
|
151
|
+
* Spot cone parameters: `angle` is the outer cone half-angle in radians measured
|
|
152
|
+
* from the light's local -Z axis; `penumbra` is the angular falloff fraction in
|
|
153
|
+
* [0,1]. `distance` bounds the range in meters (0 is unbounded, the renderer
|
|
154
|
+
* default) and `decay` is the physical falloff exponent.
|
|
155
|
+
*/
|
|
156
|
+
export const SpatialSpotLightSchema = z.strictObject({
|
|
157
|
+
angle: z.number().finite().min(0.000001).max(Math.PI / 2),
|
|
158
|
+
penumbra: unit,
|
|
159
|
+
distance: z.number().finite().min(0).max(1_000_000).optional(),
|
|
160
|
+
decay: z.number().finite().min(0).max(1_000).optional(),
|
|
161
|
+
})
|
|
128
162
|
export const SpatialOriginSchema = z.discriminatedUnion("kind", [
|
|
129
163
|
z.strictObject({ kind: z.literal("authored") }),
|
|
130
164
|
z.strictObject({ kind: z.literal("generated"), generatorId: SpatialGeneratorIdSchema, key: z.string().min(1).max(256) }),
|
|
@@ -151,14 +185,26 @@ const surfaceBase = {
|
|
|
151
185
|
}
|
|
152
186
|
export const SpatialEntitySchema = z.discriminatedUnion("kind", [
|
|
153
187
|
z.strictObject({ ...entityBase, kind: z.literal("group") }),
|
|
154
|
-
z.strictObject({ ...entityBase, kind: z.literal("mesh"), geometry: SpatialGeometrySchema, material: SpatialMaterialSchema
|
|
188
|
+
z.strictObject({ ...entityBase, kind: z.literal("mesh"), geometry: SpatialGeometrySchema, material: SpatialMaterialSchema,
|
|
189
|
+
/** Optional shadow participation; omitted means no casting and no receiving. */
|
|
190
|
+
castShadow: z.boolean().optional(), receiveShadow: z.boolean().optional(),
|
|
191
|
+
/** Optional local-space instancing: every transform is drawn inside the entity's own local frame. */
|
|
192
|
+
instances: z.array(SpatialTransformSchema).min(1).max(SPATIAL_SCENE_LIMITS.entities).optional() }),
|
|
155
193
|
z.strictObject({ ...entityBase, ...surfaceBase, kind: z.literal("image") }),
|
|
156
194
|
z.strictObject({ ...entityBase, ...surfaceBase, kind: z.literal("diagram") }),
|
|
157
195
|
z.strictObject({ ...entityBase, ...surfaceBase, kind: z.literal("video"), sourceOffsetUs: SpatialTimeUsSchema, playback: z.enum(["once", "loop", "freeze"]) }),
|
|
158
196
|
z.strictObject({ ...entityBase, kind: z.literal("text"), text: z.string().max(16_384), fontAssetId: SpatialAssetIdSchema, fontSize: positiveDimension, width: positiveDimension, color, align: z.enum(["left", "center", "right"]) }),
|
|
159
|
-
z.strictObject({ ...entityBase, kind: z.literal("light"), light: z.enum(["ambient", "directional", "point"]), color, intensity: z.number().finite().min(0).max(100_000)
|
|
197
|
+
z.strictObject({ ...entityBase, kind: z.literal("light"), light: z.enum(["ambient", "directional", "point", "spot"]), color, intensity: z.number().finite().min(0).max(100_000),
|
|
198
|
+
spot: SpatialSpotLightSchema.optional(), shadow: z.boolean().optional() }),
|
|
160
199
|
z.strictObject({ ...entityBase, kind: z.literal("splat"), assetId: SpatialAssetIdSchema }),
|
|
161
|
-
|
|
200
|
+
z.strictObject({ ...entityBase, kind: z.literal("environment"), assetId: SpatialAssetIdSchema,
|
|
201
|
+
role: z.enum(["background", "environment", "both"]), intensity: z.number().finite().min(0).max(16) }),
|
|
202
|
+
]).superRefine((entity, context) => {
|
|
203
|
+
if (entity.kind !== "light") return
|
|
204
|
+
if (entity.light === "spot" && entity.spot === undefined) context.addIssue({ code: "custom", path: ["spot"], message: "Spot lights require their spot cone parameters." })
|
|
205
|
+
if (entity.light !== "spot" && entity.spot !== undefined) context.addIssue({ code: "custom", path: ["spot"], message: "Only spot lights may carry spot cone parameters." })
|
|
206
|
+
if (entity.light === "ambient" && entity.shadow !== undefined) context.addIssue({ code: "custom", path: ["shadow"], message: "Ambient lights cannot cast shadows; only directional, point, and spot lights may declare shadow." })
|
|
207
|
+
})
|
|
162
208
|
|
|
163
209
|
const key = <T extends z.ZodType>(value: T) => z.strictObject({ timeUs: SpatialTimeUsSchema, value })
|
|
164
210
|
const channelBase = { channelId: SpatialChannelIdSchema, targetId: z.union([SpatialEntityIdSchema, SpatialCameraIdSchema]) }
|
|
@@ -185,6 +231,12 @@ export const SpatialGeneratorSchema = z.strictObject({
|
|
|
185
231
|
z.strictObject({ kind: z.literal("attempt"), attemptId: z.string().min(1).max(128), runtimeSha256: SpatialDigestSchema }),
|
|
186
232
|
]),
|
|
187
233
|
editableKeys: z.array(z.strictObject({ key: z.string().min(1).max(256), properties: z.array(z.enum(["color", "opacity", "transform"])).min(1).max(3) })).max(SPATIAL_SCENE_LIMITS.entities),
|
|
234
|
+
/**
|
|
235
|
+
* Sorted asset ids owned by this generator's retained output (generated mesh
|
|
236
|
+
* payloads and derived fact manifests). Owned assets are referenced only by
|
|
237
|
+
* this generator's entities and are replaced atomically with the output.
|
|
238
|
+
*/
|
|
239
|
+
assets: z.array(SpatialAssetIdSchema).max(SPATIAL_SCENE_LIMITS.assets).optional(),
|
|
188
240
|
})
|
|
189
241
|
export const SpatialSceneV1Schema = z.strictObject({
|
|
190
242
|
kind: z.literal("slopcamera.spatial-scene"),
|
|
@@ -198,17 +250,24 @@ export const SpatialSceneV1Schema = z.strictObject({
|
|
|
198
250
|
animations: z.array(SpatialAnimationSchema).max(SPATIAL_SCENE_LIMITS.channels),
|
|
199
251
|
generators: z.array(SpatialGeneratorSchema).max(128),
|
|
200
252
|
overrides: z.array(SpatialOverrideSchema).max(SPATIAL_SCENE_LIMITS.entities),
|
|
253
|
+
fog: SpatialFogSchema.optional(),
|
|
201
254
|
})
|
|
202
255
|
|
|
203
256
|
export const SpatialPatchOperationSchema = z.discriminatedUnion("kind", [
|
|
204
257
|
z.strictObject({ kind: z.literal("add-asset"), asset: SpatialAssetManifestSchema }),
|
|
205
258
|
z.strictObject({ kind: z.literal("replace-asset"), asset: SpatialAssetManifestSchema }),
|
|
206
259
|
z.strictObject({ kind: z.literal("set-mesh-geometry"), entityId: SpatialEntityIdSchema, geometry: SpatialGeometrySchema }),
|
|
260
|
+
z.strictObject({ kind: z.literal("set-material"), entityId: SpatialEntityIdSchema, material: SpatialMaterialSchema }),
|
|
207
261
|
z.strictObject({ kind: z.literal("rename-entity"), entityId: SpatialEntityIdSchema, name: z.string().min(1).max(256) }),
|
|
208
262
|
z.strictObject({ kind: z.literal("reparent-entity"), entityId: SpatialEntityIdSchema, parentId: SpatialEntityIdSchema.nullable() }),
|
|
209
263
|
z.strictObject({ kind: z.literal("set-transform"), entityId: SpatialEntityIdSchema, transform: SpatialTransformSchema }),
|
|
210
264
|
z.strictObject({ kind: z.literal("set-color"), entityId: SpatialEntityIdSchema, color }),
|
|
211
265
|
z.strictObject({ kind: z.literal("set-opacity"), entityId: SpatialEntityIdSchema, opacity: unit }),
|
|
266
|
+
z.strictObject({ kind: z.literal("set-emissive"), entityId: SpatialEntityIdSchema, emissive: SpatialEmissiveSchema.nullable() }),
|
|
267
|
+
z.strictObject({ kind: z.literal("set-spot"), entityId: SpatialEntityIdSchema, spot: SpatialSpotLightSchema }),
|
|
268
|
+
z.strictObject({ kind: z.literal("set-instances"), entityId: SpatialEntityIdSchema, instances: z.array(SpatialTransformSchema).min(1).max(SPATIAL_SCENE_LIMITS.entities).nullable() }),
|
|
269
|
+
z.strictObject({ kind: z.literal("set-mesh-shadow"), entityId: SpatialEntityIdSchema, castShadow: z.boolean().nullable(), receiveShadow: z.boolean().nullable() }),
|
|
270
|
+
z.strictObject({ kind: z.literal("set-light-shadow"), entityId: SpatialEntityIdSchema, shadow: z.boolean().nullable() }),
|
|
212
271
|
z.strictObject({ kind: z.literal("set-camera"), camera: SpatialCameraSchema }),
|
|
213
272
|
z.strictObject({ kind: z.literal("set-channel"), channel: SpatialAnimationSchema }),
|
|
214
273
|
z.strictObject({ kind: z.literal("remove-channel"), channelId: SpatialChannelIdSchema }),
|
|
@@ -216,7 +275,9 @@ export const SpatialPatchOperationSchema = z.discriminatedUnion("kind", [
|
|
|
216
275
|
z.strictObject({ kind: z.literal("remove-entity"), entityId: SpatialEntityIdSchema }),
|
|
217
276
|
z.strictObject({ kind: z.literal("set-override"), override: SpatialOverrideSchema }),
|
|
218
277
|
z.strictObject({ kind: z.literal("remove-override"), entityId: SpatialEntityIdSchema, property: z.enum(["color", "opacity", "transform"]) }),
|
|
219
|
-
z.strictObject({ kind: z.literal("replace-generator-output"), generator: SpatialGeneratorSchema, entities: z.array(SpatialEntitySchema).max(SPATIAL_SCENE_LIMITS.entities)
|
|
278
|
+
z.strictObject({ kind: z.literal("replace-generator-output"), generator: SpatialGeneratorSchema, entities: z.array(SpatialEntitySchema).max(SPATIAL_SCENE_LIMITS.entities),
|
|
279
|
+
/** Owned asset manifests replacing the generator's previous owned set; ids must match `generator.assets`. */
|
|
280
|
+
assets: z.array(SpatialAssetManifestSchema).max(SPATIAL_SCENE_LIMITS.assets).optional() }),
|
|
220
281
|
])
|
|
221
282
|
export const SpatialScenePatchV1Schema = z.strictObject({
|
|
222
283
|
kind: z.literal("slopcamera.spatial-scene-patch"),
|
|
@@ -257,12 +318,19 @@ export const EvaluatedSpatialSceneSchema = z.strictObject({
|
|
|
257
318
|
selectionId: z.number().int().min(1).max(SPATIAL_SCENE_LIMITS.entities),
|
|
258
319
|
})).max(SPATIAL_SCENE_LIMITS.entities),
|
|
259
320
|
assets: z.array(SpatialAssetManifestSchema).max(SPATIAL_SCENE_LIMITS.assets),
|
|
321
|
+
fog: SpatialFogSchema.optional(),
|
|
260
322
|
})
|
|
261
323
|
|
|
262
324
|
type DeepReadonly<T> = T extends object ? { readonly [Key in keyof T]: DeepReadonly<T[Key]> } : T
|
|
263
325
|
export type SpatialSceneV1 = DeepReadonly<z.infer<typeof SpatialSceneV1Schema>>
|
|
264
326
|
export type SpatialEntity = DeepReadonly<z.infer<typeof SpatialEntitySchema>>
|
|
327
|
+
export type SpatialMaterial = DeepReadonly<z.infer<typeof SpatialMaterialSchema>>
|
|
328
|
+
export type SpatialEmissive = DeepReadonly<z.infer<typeof SpatialEmissiveSchema>>
|
|
329
|
+
export type SpatialGeometry = DeepReadonly<z.infer<typeof SpatialGeometrySchema>>
|
|
330
|
+
export type SpatialSpotLight = DeepReadonly<z.infer<typeof SpatialSpotLightSchema>>
|
|
331
|
+
export type SpatialPlacement = DeepReadonly<z.infer<typeof SpatialPlacementSchema>>
|
|
265
332
|
export type SpatialCamera = DeepReadonly<z.infer<typeof SpatialCameraSchema>>
|
|
333
|
+
export type SpatialCameraLens = DeepReadonly<z.infer<typeof SpatialCameraLensSchema>>
|
|
266
334
|
export type SpatialProjection = DeepReadonly<z.infer<typeof SpatialProjectionSchema>>
|
|
267
335
|
export type SpatialTransform = DeepReadonly<z.infer<typeof SpatialTransformSchema>>
|
|
268
336
|
export type SpatialPose = DeepReadonly<z.infer<typeof SpatialPoseSchema>>
|