@hraness/slopcamera 3.2.7 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +65 -20
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +6 -9
- package/apps/desktop/application/default-registry.ts +26 -10
- package/apps/desktop/application/operation.ts +14 -5
- package/apps/desktop/application/operations/index.ts +4 -1
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +528 -97
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +31 -2
- package/apps/desktop/cli/commands.ts +1215 -70
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +183 -23
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +26 -50
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +18 -5
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +24 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +737 -291
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +32 -434
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -240
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +1 -1
- package/package.json +11 -24
- package/skills/slopcamera/SKILL.md +14 -1
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +250 -102
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -23
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-mcy8z0br.js +0 -192
- package/dist/index-p63wavx0.js +0 -2029
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-rvghzt7b.js +0 -2013
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-z7239b4h.js +0 -2363
- package/dist/skill-install-dh0ntqba.js +0 -10
- package/src/desktop.ts +0 -253
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { deepFreezeJson } from "../code/json-snapshot.js"
|
|
3
|
+
import { align, facing, groundSnap, nextTo, onTopOf, type SpatialAxis, type SpatialLayoutEntry } from "./build.js"
|
|
4
|
+
import {
|
|
5
|
+
SpatialEntityIdSchema, SpatialQuaternionSchema, SpatialScenePatchV1Schema, SpatialTransformSchema,
|
|
6
|
+
SpatialVec3Schema, type SpatialScenePatchV1, type SpatialTransform,
|
|
7
|
+
} from "./contracts.js"
|
|
8
|
+
import { parseSpatialValue, SpatialSceneError } from "./identity.js"
|
|
9
|
+
import { composeTransform, transformBounds, type Bounds, type Quaternion, type Transform, type Vec3 } from "./math.js"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Declarative relation goals solved into concrete transforms plus a
|
|
13
|
+
* ready-to-apply patch document. The solver composes the phase-1 placement
|
|
14
|
+
* primitives (`onTopOf`, `nextTo`, `facing`, `align`, `groundSnap`) over a
|
|
15
|
+
* dependency DAG: a goal whose relations name another goal entity runs after
|
|
16
|
+
* it, so targets see solved transforms. Within one goal, relations apply in
|
|
17
|
+
* declared order and each sees earlier results.
|
|
18
|
+
*
|
|
19
|
+
* Relation contract:
|
|
20
|
+
* - `onTopOf`/`nextTo`/`align`/`groundSnap` need local bounds on every
|
|
21
|
+
* participating entity; a missing enclosure fails `bounds-unknown` naming
|
|
22
|
+
* that entity. `align` moves the goal entity's `edge` (min | center | max)
|
|
23
|
+
* on `axis` onto the target's same edge; it is expressed through `align`
|
|
24
|
+
* on the pair, transferring the anchor's would-be delta back to the mover
|
|
25
|
+
* so the named target never moves.
|
|
26
|
+
* - `facing` rotates the goal entity so local -Z looks at the target's
|
|
27
|
+
* transform position (camera poses arrive as bases the same way). `at`
|
|
28
|
+
* assigns absolute transform components. Neither needs bounds.
|
|
29
|
+
*
|
|
30
|
+
* Determinism: identical input yields byte-identical output. The emitted
|
|
31
|
+
* patch carries `SPATIAL_SOLVE_PENDING_SCENE_SHA256` in place of the scene
|
|
32
|
+
* digest; the caller fills it (e.g. from `scene inspect`) before `scene
|
|
33
|
+
* patch`, keeping solve output stable across scene revisions.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export const SPATIAL_SOLVE_LIMITS = Object.freeze({
|
|
37
|
+
goals: 64,
|
|
38
|
+
relationsPerGoal: 8,
|
|
39
|
+
bases: 1_024,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/** Emitted patch digest slot: parses as a digest and conflicts honestly on apply until the caller fills it. */
|
|
43
|
+
export const SPATIAL_SOLVE_PENDING_SCENE_SHA256 = "0".repeat(64)
|
|
44
|
+
|
|
45
|
+
export type SpatialSolveErrorCode =
|
|
46
|
+
| "invalid-data"
|
|
47
|
+
| "unknown-entity"
|
|
48
|
+
| "bounds-unknown"
|
|
49
|
+
| "relation-cycle"
|
|
50
|
+
| "duplicate-goal"
|
|
51
|
+
| "relation-failed"
|
|
52
|
+
|
|
53
|
+
/** Typed solver failures; `path` addresses the offending goal, relation or base entry. */
|
|
54
|
+
export class SpatialSolveError extends Error {
|
|
55
|
+
readonly code: SpatialSolveErrorCode
|
|
56
|
+
readonly path: string
|
|
57
|
+
constructor(code: SpatialSolveErrorCode, message: string, path = "solve") {
|
|
58
|
+
super(message)
|
|
59
|
+
this.name = "SpatialSolveError"
|
|
60
|
+
this.code = code
|
|
61
|
+
this.path = path
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Goal entities must be scene entity IDs so emitted `set-transform` operations stay schema-valid. */
|
|
66
|
+
export const SpatialSolveEntityKeySchema = SpatialEntityIdSchema
|
|
67
|
+
/** Anchor keys also cover cameras and caller-declared virtual bases, so only the shared id charset applies. */
|
|
68
|
+
export const SpatialSolveAnchorKeySchema = z.string().min(1).max(128)
|
|
69
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/u, "Solve keys start alphanumeric and may contain _ or -.")
|
|
70
|
+
|
|
71
|
+
const axisSchema = z.enum(["x", "y", "z"])
|
|
72
|
+
const boundedNumber = z.number().finite().min(-1_000_000).max(1_000_000)
|
|
73
|
+
const positiveDimension = z.number().finite().min(0.000001).max(1_000_000)
|
|
74
|
+
const scaleTuple = z.tuple([positiveDimension, positiveDimension, positiveDimension])
|
|
75
|
+
|
|
76
|
+
export const SpatialSolveBoundsSchema = z.strictObject({ min: SpatialVec3Schema, max: SpatialVec3Schema })
|
|
77
|
+
.refine(bounds => bounds.min.every((value, index) => value <= bounds.max[index]!), "Bounds min must not exceed max.")
|
|
78
|
+
|
|
79
|
+
export const SpatialSolveRelationSchema = z.discriminatedUnion("kind", [
|
|
80
|
+
z.strictObject({ kind: z.literal("onTopOf"), target: SpatialSolveAnchorKeySchema }),
|
|
81
|
+
z.strictObject({
|
|
82
|
+
kind: z.literal("nextTo"), target: SpatialSolveAnchorKeySchema,
|
|
83
|
+
axis: axisSchema.optional(), side: z.enum(["before", "after"]).optional(), gap: boundedNumber.optional(),
|
|
84
|
+
}),
|
|
85
|
+
z.strictObject({ kind: z.literal("facing"), target: SpatialSolveAnchorKeySchema, up: SpatialVec3Schema.optional() }),
|
|
86
|
+
z.strictObject({
|
|
87
|
+
kind: z.literal("align"), target: SpatialSolveAnchorKeySchema,
|
|
88
|
+
axis: axisSchema, edge: z.enum(["min", "center", "max"]).optional(),
|
|
89
|
+
}),
|
|
90
|
+
z.strictObject({
|
|
91
|
+
kind: z.literal("at"),
|
|
92
|
+
position: SpatialVec3Schema.optional(), rotation: SpatialQuaternionSchema.optional(), scale: scaleTuple.optional(),
|
|
93
|
+
}).refine(
|
|
94
|
+
relation => relation.position !== undefined || relation.rotation !== undefined || relation.scale !== undefined,
|
|
95
|
+
"An at relation sets at least one of position, rotation, or scale.",
|
|
96
|
+
),
|
|
97
|
+
z.strictObject({ kind: z.literal("groundSnap"), floorY: boundedNumber.optional() }),
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
export const SpatialSolveGoalSchema = z.strictObject({
|
|
101
|
+
entityKey: SpatialSolveEntityKeySchema,
|
|
102
|
+
relations: z.array(SpatialSolveRelationSchema).min(1).max(SPATIAL_SOLVE_LIMITS.relationsPerGoal),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
export const SpatialSolveBaseSchema = z.strictObject({
|
|
106
|
+
transform: SpatialTransformSchema,
|
|
107
|
+
bounds: SpatialSolveBoundsSchema.optional(),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export const SpatialSolveRequestSchema = z.strictObject({
|
|
111
|
+
goals: z.array(SpatialSolveGoalSchema).min(1).max(SPATIAL_SOLVE_LIMITS.goals),
|
|
112
|
+
bases: z.record(SpatialSolveAnchorKeySchema, SpatialSolveBaseSchema),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/** Goals-file variant: base entries may supply only the fields the scene cannot derive. */
|
|
116
|
+
export const SpatialSolveBasePatchSchema = z.strictObject({
|
|
117
|
+
transform: SpatialTransformSchema.optional(),
|
|
118
|
+
bounds: SpatialSolveBoundsSchema.optional(),
|
|
119
|
+
})
|
|
120
|
+
export const SpatialSolveGoalsFileSchema = z.strictObject({
|
|
121
|
+
goals: z.array(SpatialSolveGoalSchema).min(1).max(SPATIAL_SOLVE_LIMITS.goals),
|
|
122
|
+
bases: z.record(SpatialSolveAnchorKeySchema, SpatialSolveBasePatchSchema).optional(),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Structural interfaces mirror the schemas rather than inferring from them —
|
|
127
|
+
* recursive readonly mappers over the nested strict-object graph measurably
|
|
128
|
+
* slow authored-source typechecks. `solveSpatialRelations` parses foreign
|
|
129
|
+
* input through the schemas, so any drift fails compilation at the parse site.
|
|
130
|
+
*/
|
|
131
|
+
export type SpatialSolveRelation =
|
|
132
|
+
| { readonly kind: "onTopOf"; readonly target: string }
|
|
133
|
+
| {
|
|
134
|
+
readonly kind: "nextTo"; readonly target: string
|
|
135
|
+
readonly axis?: SpatialAxis | undefined; readonly side?: "before" | "after" | undefined; readonly gap?: number | undefined
|
|
136
|
+
}
|
|
137
|
+
| { readonly kind: "facing"; readonly target: string; readonly up?: Vec3 | undefined }
|
|
138
|
+
| { readonly kind: "align"; readonly target: string; readonly axis: SpatialAxis; readonly edge?: "min" | "center" | "max" | undefined }
|
|
139
|
+
| { readonly kind: "at"; readonly position?: Vec3 | undefined; readonly rotation?: Quaternion | undefined; readonly scale?: Vec3 | undefined }
|
|
140
|
+
| { readonly kind: "groundSnap"; readonly floorY?: number | undefined }
|
|
141
|
+
|
|
142
|
+
export interface SpatialSolveGoal {
|
|
143
|
+
readonly entityKey: string
|
|
144
|
+
readonly relations: readonly SpatialSolveRelation[]
|
|
145
|
+
}
|
|
146
|
+
export interface SpatialSolveBase {
|
|
147
|
+
readonly transform: Transform
|
|
148
|
+
readonly bounds?: Bounds | undefined
|
|
149
|
+
}
|
|
150
|
+
export interface SpatialSolveRequest {
|
|
151
|
+
readonly goals: readonly SpatialSolveGoal[]
|
|
152
|
+
readonly bases: Readonly<Record<string, SpatialSolveBase>>
|
|
153
|
+
}
|
|
154
|
+
export interface SpatialSolveBasePatch {
|
|
155
|
+
readonly transform?: Transform | undefined
|
|
156
|
+
readonly bounds?: Bounds | undefined
|
|
157
|
+
}
|
|
158
|
+
export interface SpatialSolveGoalsFile {
|
|
159
|
+
readonly goals: readonly SpatialSolveGoal[]
|
|
160
|
+
readonly bases?: Readonly<Record<string, SpatialSolveBasePatch>> | undefined
|
|
161
|
+
}
|
|
162
|
+
export interface SpatialSolveResult {
|
|
163
|
+
/** Solved transforms keyed by goal entity, in declared goal order. */
|
|
164
|
+
readonly transforms: Readonly<Record<string, SpatialTransform>>
|
|
165
|
+
readonly patch: SpatialScenePatchV1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface SolveState {
|
|
169
|
+
transform: Transform
|
|
170
|
+
readonly bounds: Bounds | undefined
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseSolveRequest(input: unknown): SpatialSolveRequest {
|
|
174
|
+
let parsed: z.infer<typeof SpatialSolveRequestSchema>
|
|
175
|
+
try {
|
|
176
|
+
parsed = parseSpatialValue(SpatialSolveRequestSchema, input, "solve request")
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof SpatialSceneError) throw new SpatialSolveError("invalid-data", error.message, error.path)
|
|
179
|
+
throw error
|
|
180
|
+
}
|
|
181
|
+
if (Object.keys(parsed.bases).length > SPATIAL_SOLVE_LIMITS.bases) {
|
|
182
|
+
throw new SpatialSolveError("invalid-data", `Solve bases are bounded to ${SPATIAL_SOLVE_LIMITS.bases} entries.`, "bases")
|
|
183
|
+
}
|
|
184
|
+
const request: SpatialSolveRequest = parsed
|
|
185
|
+
return request
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Deterministic topological order preferring declared goal order. Edges point
|
|
190
|
+
* from a goal to each goal entity its relations target; bases are static
|
|
191
|
+
* anchors. Leftover goals form cycles — one concrete cycle is named.
|
|
192
|
+
*/
|
|
193
|
+
function orderGoals(goals: readonly SpatialSolveGoal[], dependencies: ReadonlyMap<string, ReadonlySet<string>>): readonly SpatialSolveGoal[] {
|
|
194
|
+
const remaining = new Map(goals.map(goal => [goal.entityKey, goal]))
|
|
195
|
+
const ordered: SpatialSolveGoal[] = []
|
|
196
|
+
for (;;) {
|
|
197
|
+
const ready = goals.find(goal => {
|
|
198
|
+
if (!remaining.has(goal.entityKey)) return false
|
|
199
|
+
for (const dependency of dependencies.get(goal.entityKey)!) if (remaining.has(dependency)) return false
|
|
200
|
+
return true
|
|
201
|
+
})
|
|
202
|
+
if (ready === undefined) break
|
|
203
|
+
remaining.delete(ready.entityKey)
|
|
204
|
+
ordered.push(ready)
|
|
205
|
+
}
|
|
206
|
+
if (remaining.size === 0) return ordered
|
|
207
|
+
// Extract one concrete cycle over the leftover dependency subgraph.
|
|
208
|
+
const stack: string[] = []
|
|
209
|
+
const seen = new Set<string>()
|
|
210
|
+
let current: string | undefined = goals.find(goal => remaining.has(goal.entityKey))!.entityKey
|
|
211
|
+
while (current !== undefined && remaining.has(current) && !seen.has(current)) {
|
|
212
|
+
seen.add(current)
|
|
213
|
+
stack.push(current)
|
|
214
|
+
current = [...dependencies.get(current)!].find(dependency => remaining.has(dependency))
|
|
215
|
+
}
|
|
216
|
+
const from = current === undefined ? 0 : stack.indexOf(current)
|
|
217
|
+
const cycle = stack.slice(from === -1 ? 0 : from)
|
|
218
|
+
if (current !== undefined) cycle.push(current)
|
|
219
|
+
throw new SpatialSolveError("relation-cycle", `Relation goals form a cycle: ${cycle.join(" → ")}.`, "goals")
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function requireState(states: ReadonlyMap<string, SolveState>, key: string, path: string): SolveState {
|
|
223
|
+
const state = states.get(key)
|
|
224
|
+
if (state === undefined) {
|
|
225
|
+
throw new SpatialSolveError("unknown-entity", `Relation target ${key} has no base and is not a solve goal.`, path)
|
|
226
|
+
}
|
|
227
|
+
return state
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function requireBounds(state: SolveState, key: string, kind: string, path: string): Bounds {
|
|
231
|
+
if (state.bounds === undefined) {
|
|
232
|
+
throw new SpatialSolveError("bounds-unknown", `Entity ${key} has no bounds; ${kind} needs local bounds on every participant.`, path)
|
|
233
|
+
}
|
|
234
|
+
return state.bounds
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function emitTransform(transform: Transform, path: string): SpatialTransform {
|
|
238
|
+
try {
|
|
239
|
+
return parseSpatialValue(SpatialTransformSchema, {
|
|
240
|
+
position: [...transform.position], rotation: [...transform.rotation], scale: [...transform.scale],
|
|
241
|
+
}, "solved transform")
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof SpatialSceneError) throw new SpatialSolveError("relation-failed", error.message, path)
|
|
244
|
+
throw error
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function applyRelation(
|
|
249
|
+
relation: SpatialSolveRelation, entityKey: string, mover: SolveState,
|
|
250
|
+
states: ReadonlyMap<string, SolveState>, path: string,
|
|
251
|
+
): Transform {
|
|
252
|
+
try {
|
|
253
|
+
switch (relation.kind) {
|
|
254
|
+
case "onTopOf": {
|
|
255
|
+
const target = requireState(states, relation.target, path)
|
|
256
|
+
return onTopOf(
|
|
257
|
+
requireBounds(mover, entityKey, relation.kind, path), mover.transform,
|
|
258
|
+
requireBounds(target, relation.target, relation.kind, path), target.transform,
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
case "nextTo": {
|
|
262
|
+
const target = requireState(states, relation.target, path)
|
|
263
|
+
return nextTo(
|
|
264
|
+
requireBounds(mover, entityKey, relation.kind, path), mover.transform,
|
|
265
|
+
requireBounds(target, relation.target, relation.kind, path), target.transform,
|
|
266
|
+
{
|
|
267
|
+
...(relation.axis === undefined ? {} : { axis: relation.axis }),
|
|
268
|
+
...(relation.side === undefined ? {} : { side: relation.side }),
|
|
269
|
+
...(relation.gap === undefined ? {} : { gap: relation.gap }),
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
case "facing": {
|
|
274
|
+
const target = requireState(states, relation.target, path)
|
|
275
|
+
return facing(mover.transform, target.transform.position, relation.up ?? [0, 1, 0])
|
|
276
|
+
}
|
|
277
|
+
case "align": {
|
|
278
|
+
const target = requireState(states, relation.target, path)
|
|
279
|
+
const moverBounds = requireBounds(mover, entityKey, relation.kind, path)
|
|
280
|
+
const targetBounds = requireBounds(target, relation.target, relation.kind, path)
|
|
281
|
+
const entries: SpatialLayoutEntry[] = [
|
|
282
|
+
{ entityId: entityKey, transform: mover.transform, bounds: moverBounds },
|
|
283
|
+
{ entityId: relation.target, transform: target.transform, bounds: targetBounds },
|
|
284
|
+
]
|
|
285
|
+
const [moved, anchorMoved] = align(entries, relation.axis, relation.edge ?? "center")
|
|
286
|
+
// Transfer the anchor's would-be delta back to the mover: the result
|
|
287
|
+
// places the mover's edge exactly on the anchor's edge while the named
|
|
288
|
+
// target never moves. Off-axis components of the correction are zero.
|
|
289
|
+
const correction = target.transform.position.map((value, index) => value - anchorMoved!.position[index]!)
|
|
290
|
+
return emitTransform({
|
|
291
|
+
position: moved!.position.map((value, index) => value + correction[index]!) as unknown as Vec3,
|
|
292
|
+
rotation: moved!.rotation, scale: moved!.scale,
|
|
293
|
+
}, path)
|
|
294
|
+
}
|
|
295
|
+
case "at":
|
|
296
|
+
return emitTransform({
|
|
297
|
+
position: relation.position ?? mover.transform.position,
|
|
298
|
+
rotation: relation.rotation ?? mover.transform.rotation,
|
|
299
|
+
scale: relation.scale ?? mover.transform.scale,
|
|
300
|
+
}, path)
|
|
301
|
+
case "groundSnap": {
|
|
302
|
+
const bounds = requireBounds(mover, entityKey, relation.kind, path)
|
|
303
|
+
const world = transformBounds(composeTransform(mover.transform), bounds)
|
|
304
|
+
return groundSnap(mover.transform, mover.transform.position[1] - world.min[1], relation.floorY ?? 0)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
if (error instanceof SpatialSolveError) throw error
|
|
309
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
310
|
+
throw new SpatialSolveError("relation-failed", `${relation.kind} failed: ${message}`, path)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Resolves declarative relation goals into transforms and a patch document.
|
|
316
|
+
* The patch's `expectedSceneSha256` is `SPATIAL_SOLVE_PENDING_SCENE_SHA256`;
|
|
317
|
+
* the caller replaces it with the scene's actual digest before `scene patch`.
|
|
318
|
+
*/
|
|
319
|
+
export function solveSpatialRelations(input: unknown): SpatialSolveResult {
|
|
320
|
+
const request = parseSolveRequest(input)
|
|
321
|
+
const goalKeys = new Set<string>()
|
|
322
|
+
for (const [index, goal] of request.goals.entries()) {
|
|
323
|
+
if (goalKeys.has(goal.entityKey)) {
|
|
324
|
+
throw new SpatialSolveError("duplicate-goal", `Duplicate solve goal for ${goal.entityKey}.`, `goals[${index}]`)
|
|
325
|
+
}
|
|
326
|
+
goalKeys.add(goal.entityKey)
|
|
327
|
+
}
|
|
328
|
+
const states = new Map<string, SolveState>()
|
|
329
|
+
for (const [key, base] of Object.entries(request.bases)) {
|
|
330
|
+
states.set(key, { transform: base.transform, bounds: base.bounds })
|
|
331
|
+
}
|
|
332
|
+
const dependencies = new Map<string, ReadonlySet<string>>()
|
|
333
|
+
for (const [index, goal] of request.goals.entries()) {
|
|
334
|
+
if (!states.has(goal.entityKey)) {
|
|
335
|
+
throw new SpatialSolveError("unknown-entity", `Goal entity ${goal.entityKey} has no base transform; add bases.${goal.entityKey}.`, `goals[${index}]`)
|
|
336
|
+
}
|
|
337
|
+
const required = new Set<string>()
|
|
338
|
+
for (const [relationIndex, relation] of goal.relations.entries()) {
|
|
339
|
+
if (!("target" in relation)) continue
|
|
340
|
+
const path = `goals[${index}].relations[${relationIndex}].target`
|
|
341
|
+
if (!states.has(relation.target) && !goalKeys.has(relation.target)) {
|
|
342
|
+
throw new SpatialSolveError("unknown-entity", `Relation target ${relation.target} has no base and is not a solve goal.`, path)
|
|
343
|
+
}
|
|
344
|
+
if (goalKeys.has(relation.target)) required.add(relation.target)
|
|
345
|
+
}
|
|
346
|
+
dependencies.set(goal.entityKey, required)
|
|
347
|
+
}
|
|
348
|
+
for (const goal of orderGoals(request.goals, dependencies)) {
|
|
349
|
+
const state = states.get(goal.entityKey)!
|
|
350
|
+
const index = request.goals.indexOf(goal)
|
|
351
|
+
for (const [relationIndex, relation] of goal.relations.entries()) {
|
|
352
|
+
state.transform = applyRelation(relation, goal.entityKey, state, states, `goals[${index}].relations[${relationIndex}]`)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const transforms: Record<string, SpatialTransform> = {}
|
|
356
|
+
for (const goal of request.goals) {
|
|
357
|
+
transforms[goal.entityKey] = emitTransform(states.get(goal.entityKey)!.transform, `transforms.${goal.entityKey}`)
|
|
358
|
+
}
|
|
359
|
+
const patch = parseSpatialValue(SpatialScenePatchV1Schema, {
|
|
360
|
+
kind: "slopcamera.spatial-scene-patch",
|
|
361
|
+
schemaVersion: 1,
|
|
362
|
+
expectedSceneSha256: SPATIAL_SOLVE_PENDING_SCENE_SHA256,
|
|
363
|
+
operations: request.goals.map(goal => ({ kind: "set-transform", entityId: goal.entityKey, transform: transforms[goal.entityKey] })),
|
|
364
|
+
}, "solve patch")
|
|
365
|
+
return deepFreezeJson({ transforms, patch })
|
|
366
|
+
}
|