@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,327 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import type { JsonValue } from "../code/contracts.js"
|
|
3
|
+
import { createBoundedJsonValueSnapshot, deepFreezeJson } from "../code/json-snapshot.js"
|
|
4
|
+
import { SPATIAL_SCENE_LIMITS, SpatialMaterialSchema, type SpatialSceneV1 } from "./contracts.js"
|
|
5
|
+
import { createSpatialGeneratorSceneShell } from "./generate.js"
|
|
6
|
+
import { applySpatialScenePatch } from "./patch.js"
|
|
7
|
+
import { parseSpatialScene, parseSpatialValue, SpatialSceneError, spatialTopologicalIds, spatialValueSha256 } from "./identity.js"
|
|
8
|
+
import {
|
|
9
|
+
emitSpatialParametric, estimateSpatialParametric, SpatialParametricSpecSchema,
|
|
10
|
+
type SpatialParametricEstimate, type SpatialParametricOutput, type SpatialParametricSpec,
|
|
11
|
+
} from "./parametric.js"
|
|
12
|
+
|
|
13
|
+
/** A portable design is inert data: scalar dependencies feed retained geometry stages. */
|
|
14
|
+
export const SPATIAL_DESIGN_LIMITS = Object.freeze({
|
|
15
|
+
sourceBytes: 1_048_576, sourceDepth: 32, sourceValues: 100_000,
|
|
16
|
+
parameters: 128, values: 256, constraints: 128, stages: 32, expressionNodes: 8_192,
|
|
17
|
+
expressionDepth: 16, parts: 64, assets: 128, vertices: 262_144, triangles: 400_000,
|
|
18
|
+
outputBytes: 67_108_864, scalarMagnitude: 1e12,
|
|
19
|
+
})
|
|
20
|
+
export const SPATIAL_DESIGN_COMPILER = "slopcamera.spatial-design-v1" as const
|
|
21
|
+
|
|
22
|
+
export type SpatialDesignScalarExpression = number | { readonly $param: string } | { readonly $value: string }
|
|
23
|
+
| { readonly op: "add" | "sub" | "mul" | "div" | "min" | "max" | "neg" | "abs" | "sin" | "cos" | "floor" | "ceil"; readonly args: readonly SpatialDesignScalarExpression[] }
|
|
24
|
+
|
|
25
|
+
const identifier = z.string().min(1).max(64).regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/u)
|
|
26
|
+
const scalar = z.number().finite().min(-SPATIAL_DESIGN_LIMITS.scalarMagnitude).max(SPATIAL_DESIGN_LIMITS.scalarMagnitude)
|
|
27
|
+
const template = z.record(z.string(), z.json())
|
|
28
|
+
const parameterSchema = z.strictObject({
|
|
29
|
+
name: identifier, label: z.string().min(1).max(160).optional(), value: scalar, min: scalar, max: scalar,
|
|
30
|
+
step: z.number().finite().min(1e-9).max(SPATIAL_DESIGN_LIMITS.scalarMagnitude).optional(),
|
|
31
|
+
unit: z.enum(["m", "rad", "count", "ratio"]).optional(),
|
|
32
|
+
})
|
|
33
|
+
const expressionSchema: z.ZodType<SpatialDesignScalarExpression> = z.lazy(() => z.union([
|
|
34
|
+
scalar, z.strictObject({ $param: identifier }), z.strictObject({ $value: identifier }),
|
|
35
|
+
z.strictObject({ op: z.enum(["add", "sub", "mul", "div", "min", "max", "neg", "abs", "sin", "cos", "floor", "ceil"]), args: z.array(expressionSchema).min(1).max(16) }),
|
|
36
|
+
]))
|
|
37
|
+
const stageBase = { stageId: identifier, name: z.string().min(1).max(160).optional(), seed: z.number().int().min(0).max(0xffff_ffff).optional() }
|
|
38
|
+
const stageSchema = z.discriminatedUnion("kind", [
|
|
39
|
+
z.strictObject({ ...stageBase, kind: z.literal("parametric"), spec: template }),
|
|
40
|
+
z.strictObject({ ...stageBase, kind: z.literal("geometry"), graph: template, materials: z.array(SpatialMaterialSchema).min(1).max(16),
|
|
41
|
+
transform: template.optional(), editable: z.array(z.enum(["color", "opacity", "transform"])).max(3).optional(),
|
|
42
|
+
castShadow: z.boolean().optional(), receiveShadow: z.boolean().optional() }),
|
|
43
|
+
])
|
|
44
|
+
export const SpatialDesignV1Schema = z.strictObject({
|
|
45
|
+
kind: z.literal("slopcamera.spatial-design"), schemaVersion: z.literal(1), designId: identifier,
|
|
46
|
+
name: z.string().min(1).max(160).optional(),
|
|
47
|
+
parameters: z.array(parameterSchema).max(SPATIAL_DESIGN_LIMITS.parameters),
|
|
48
|
+
values: z.array(z.strictObject({ name: identifier, expression: expressionSchema })).max(SPATIAL_DESIGN_LIMITS.values).optional(),
|
|
49
|
+
constraints: z.array(z.strictObject({ name: identifier, left: expressionSchema, operator: z.enum(["lt", "lte", "eq", "gte", "gt"]), right: expressionSchema,
|
|
50
|
+
message: z.string().min(1).max(512).optional() })).max(SPATIAL_DESIGN_LIMITS.constraints).optional(),
|
|
51
|
+
stages: z.array(stageSchema).min(1).max(SPATIAL_DESIGN_LIMITS.stages),
|
|
52
|
+
})
|
|
53
|
+
export type SpatialDesignV1 = Readonly<z.infer<typeof SpatialDesignV1Schema>>
|
|
54
|
+
export type SpatialDesignParameter = SpatialDesignV1["parameters"][number]
|
|
55
|
+
export type SpatialDesignStage = SpatialDesignV1["stages"][number]
|
|
56
|
+
export interface SpatialDesignDependencies { readonly parameters: readonly string[]; readonly values: readonly string[] }
|
|
57
|
+
export interface SpatialDesignResolvedStage {
|
|
58
|
+
readonly stageId: string
|
|
59
|
+
readonly name?: string
|
|
60
|
+
readonly kind: SpatialDesignStage["kind"]
|
|
61
|
+
readonly generatorId: string
|
|
62
|
+
readonly spec: SpatialParametricSpec
|
|
63
|
+
readonly seed?: number
|
|
64
|
+
readonly dependencies: SpatialDesignDependencies
|
|
65
|
+
readonly estimate: SpatialParametricEstimate
|
|
66
|
+
}
|
|
67
|
+
export interface SpatialDesignInspection {
|
|
68
|
+
readonly design: SpatialDesignV1
|
|
69
|
+
readonly designSha256: string
|
|
70
|
+
readonly parameters: Readonly<Record<string, number>>
|
|
71
|
+
readonly values: readonly { readonly name: string; readonly value: number; readonly dependencies: SpatialDesignDependencies }[]
|
|
72
|
+
readonly valueOrder: readonly string[]
|
|
73
|
+
readonly constraints: readonly { readonly name: string; readonly left: number; readonly operator: string; readonly right: number; readonly passed: boolean }[]
|
|
74
|
+
readonly stages: readonly SpatialDesignResolvedStage[]
|
|
75
|
+
readonly estimate: SpatialParametricEstimate
|
|
76
|
+
}
|
|
77
|
+
export interface SpatialDesignReceipt {
|
|
78
|
+
readonly kind: "slopcamera.spatial-design-receipt"
|
|
79
|
+
readonly schemaVersion: 1
|
|
80
|
+
readonly compiler: typeof SPATIAL_DESIGN_COMPILER
|
|
81
|
+
readonly designId: string
|
|
82
|
+
readonly designSha256: string
|
|
83
|
+
readonly parametersSha256: string
|
|
84
|
+
readonly inputSceneSha256: string | null
|
|
85
|
+
readonly sceneSha256: string
|
|
86
|
+
readonly parameters: Readonly<Record<string, number>>
|
|
87
|
+
readonly stages: readonly { readonly stageId: string; readonly generatorId: string; readonly specSha256: string; readonly receiptSha256: string; readonly entities: readonly string[]; readonly dependencies: SpatialDesignDependencies; readonly changed: boolean }[]
|
|
88
|
+
readonly estimate: SpatialParametricEstimate
|
|
89
|
+
readonly artifactBytes: number
|
|
90
|
+
}
|
|
91
|
+
export interface SpatialDesignCompilation {
|
|
92
|
+
readonly design: SpatialDesignV1
|
|
93
|
+
readonly scene: SpatialSceneV1
|
|
94
|
+
readonly outputs: readonly SpatialParametricOutput[]
|
|
95
|
+
readonly receipt: SpatialDesignReceipt
|
|
96
|
+
readonly receiptSha256: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fail(message: string, path = "design"): never { throw new SpatialSceneError("invalid-data", message, path) }
|
|
100
|
+
function compare(a: string, b: string): number { return a < b ? -1 : a > b ? 1 : 0 }
|
|
101
|
+
function byName<T extends { name: string }>(items: readonly T[]): T[] { return [...items].sort((a, b) => compare(a.name, b.name)) }
|
|
102
|
+
function unique<T>(items: readonly T[], key: (item: T) => string, path: string): Map<string, T> {
|
|
103
|
+
const result = new Map<string, T>()
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
const name = key(item)
|
|
106
|
+
if (result.has(name)) fail(`Duplicate identity ${name}.`, path)
|
|
107
|
+
result.set(name, item)
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
function capture(input: unknown, path: string): JsonValue {
|
|
112
|
+
try {
|
|
113
|
+
return createBoundedJsonValueSnapshot(input, SPATIAL_DESIGN_LIMITS.sourceBytes, path, {
|
|
114
|
+
maximumDepth: SPATIAL_DESIGN_LIMITS.sourceDepth, maximumValues: SPATIAL_DESIGN_LIMITS.sourceValues,
|
|
115
|
+
}).value
|
|
116
|
+
} catch (error) { return fail(error instanceof Error ? error.message : "Invalid design data.", path) }
|
|
117
|
+
}
|
|
118
|
+
function readDesign(input: unknown): SpatialDesignV1 {
|
|
119
|
+
const parsed = parseSpatialValue(SpatialDesignV1Schema, capture(input, "design"), "design")
|
|
120
|
+
return deepFreezeJson({ ...parsed, parameters: byName(parsed.parameters),
|
|
121
|
+
...(parsed.values === undefined ? {} : { values: byName(parsed.values) }),
|
|
122
|
+
...(parsed.constraints === undefined ? {} : { constraints: byName(parsed.constraints) }),
|
|
123
|
+
stages: [...parsed.stages].sort((a, b) => compare(a.stageId, b.stageId)) })
|
|
124
|
+
}
|
|
125
|
+
function validateParameter(parameter: SpatialDesignParameter, value: number): void {
|
|
126
|
+
const path = `design.parameters.${parameter.name}`
|
|
127
|
+
if (parameter.min > parameter.max) fail("Parameter min must not exceed max.", path)
|
|
128
|
+
if (value < parameter.min || value > parameter.max) fail(`Parameter ${parameter.name} must be between ${parameter.min} and ${parameter.max}.`, path)
|
|
129
|
+
if (parameter.unit === "count" && (!Number.isSafeInteger(value) || !Number.isSafeInteger(parameter.min) || !Number.isSafeInteger(parameter.max) || (parameter.step !== undefined && !Number.isSafeInteger(parameter.step)))) {
|
|
130
|
+
fail(`Count parameter ${parameter.name} requires integer value, bounds and step.`, path)
|
|
131
|
+
}
|
|
132
|
+
if (parameter.step !== undefined) {
|
|
133
|
+
const steps = (value - parameter.min) / parameter.step
|
|
134
|
+
if (!Number.isFinite(steps) || Math.abs(steps - Math.round(steps)) > 1e-7) fail(`Parameter ${parameter.name} must follow step ${parameter.step} from min ${parameter.min}.`, path)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function resolveParameters(design: SpatialDesignV1, input?: unknown): Readonly<Record<string, number>> {
|
|
138
|
+
const parameters = unique(design.parameters, item => item.name, "design.parameters")
|
|
139
|
+
for (const parameter of parameters.values()) validateParameter(parameter, parameter.value)
|
|
140
|
+
const updates = input === undefined ? {} : parseSpatialValue(z.record(identifier, scalar), capture(input, "parameter updates"), "parameter updates")
|
|
141
|
+
for (const name of Object.keys(updates)) {
|
|
142
|
+
const parameter = parameters.get(name)
|
|
143
|
+
if (parameter === undefined) fail(`Unknown parameter ${name}.`, "parameter updates")
|
|
144
|
+
validateParameter(parameter, updates[name]!)
|
|
145
|
+
}
|
|
146
|
+
return Object.fromEntries([...parameters.values()].map(parameter => [parameter.name, Object.hasOwn(updates, parameter.name) ? updates[parameter.name]! : parameter.value]))
|
|
147
|
+
}
|
|
148
|
+
interface MutableDependencies { parameters: Set<string>; values: Set<string> }
|
|
149
|
+
function emptyDependencies(): MutableDependencies { return { parameters: new Set(), values: new Set() } }
|
|
150
|
+
function dependencies(value: MutableDependencies): SpatialDesignDependencies {
|
|
151
|
+
return { parameters: [...value.parameters].sort(compare), values: [...value.values].sort(compare) }
|
|
152
|
+
}
|
|
153
|
+
function include(target: MutableDependencies, source: SpatialDesignDependencies): void {
|
|
154
|
+
for (const name of source.parameters) target.parameters.add(name)
|
|
155
|
+
for (const name of source.values) target.values.add(name)
|
|
156
|
+
}
|
|
157
|
+
interface ExpressionContext {
|
|
158
|
+
parameters: Readonly<Record<string, number>>
|
|
159
|
+
values: ReadonlyMap<string, { value: number; dependencies: SpatialDesignDependencies }>
|
|
160
|
+
budget: { nodes: number }
|
|
161
|
+
}
|
|
162
|
+
function checkedNumber(value: number, path: string): number {
|
|
163
|
+
if (!Number.isFinite(value) || Math.abs(value) > SPATIAL_DESIGN_LIMITS.scalarMagnitude) fail("Scalar arithmetic exceeded the finite magnitude bound.", path)
|
|
164
|
+
return Object.is(value, -0) ? 0 : value
|
|
165
|
+
}
|
|
166
|
+
function evaluateExpression(expression: SpatialDesignScalarExpression, context: ExpressionContext, refs: MutableDependencies, path: string, depth = 0): number {
|
|
167
|
+
if (++context.budget.nodes > SPATIAL_DESIGN_LIMITS.expressionNodes || depth > SPATIAL_DESIGN_LIMITS.expressionDepth) fail("Design expression budget exceeded.", path)
|
|
168
|
+
if (typeof expression === "number") return checkedNumber(expression, path)
|
|
169
|
+
if ("$param" in expression) {
|
|
170
|
+
if (!Object.hasOwn(context.parameters, expression.$param)) fail(`Missing parameter ${expression.$param}.`, path)
|
|
171
|
+
refs.parameters.add(expression.$param)
|
|
172
|
+
return context.parameters[expression.$param]!
|
|
173
|
+
}
|
|
174
|
+
if ("$value" in expression) {
|
|
175
|
+
const value = context.values.get(expression.$value)
|
|
176
|
+
if (value === undefined) fail(`Missing value ${expression.$value}.`, path)
|
|
177
|
+
refs.values.add(expression.$value); include(refs, value.dependencies)
|
|
178
|
+
return value.value
|
|
179
|
+
}
|
|
180
|
+
const { op, args } = expression
|
|
181
|
+
const unary = ["neg", "abs", "sin", "cos", "floor", "ceil"].includes(op)
|
|
182
|
+
if (args.length !== (unary ? 1 : 2) && op !== "min" && op !== "max") fail(`Operation ${op} requires ${unary ? 1 : 2} arguments.`, path)
|
|
183
|
+
if ((op === "min" || op === "max") && args.length < 2) fail(`Operation ${op} requires at least two arguments.`, path)
|
|
184
|
+
const operands = args.map(arg => evaluateExpression(arg, context, refs, path, depth + 1))
|
|
185
|
+
const a = operands[0]!, b = operands[1]!
|
|
186
|
+
let result: number
|
|
187
|
+
switch (op) {
|
|
188
|
+
case "add": result = a + b; break
|
|
189
|
+
case "sub": result = a - b; break
|
|
190
|
+
case "mul": result = a * b; break
|
|
191
|
+
case "div": if (b === 0) fail("Division by zero.", path); result = a / b; break
|
|
192
|
+
case "min": result = Math.min(...operands); break
|
|
193
|
+
case "max": result = Math.max(...operands); break
|
|
194
|
+
case "neg": result = -a; break
|
|
195
|
+
case "abs": result = Math.abs(a); break
|
|
196
|
+
case "sin": result = Math.sin(a); break
|
|
197
|
+
case "cos": result = Math.cos(a); break
|
|
198
|
+
case "floor": result = Math.floor(a); break
|
|
199
|
+
case "ceil": result = Math.ceil(a); break
|
|
200
|
+
}
|
|
201
|
+
return checkedNumber(result, path)
|
|
202
|
+
}
|
|
203
|
+
function valueReferences(expression: SpatialDesignScalarExpression, depth = 0): string[] {
|
|
204
|
+
if (depth > SPATIAL_DESIGN_LIMITS.expressionDepth) fail("Design expression depth exceeded.", "design.values")
|
|
205
|
+
if (typeof expression === "number" || "$param" in expression) return []
|
|
206
|
+
if ("$value" in expression) return [expression.$value]
|
|
207
|
+
return expression.args.flatMap(arg => valueReferences(arg, depth + 1))
|
|
208
|
+
}
|
|
209
|
+
function resolveTemplate(value: JsonValue, context: ExpressionContext, refs: MutableDependencies, path: string): JsonValue {
|
|
210
|
+
if (value === null || typeof value !== "object") return value
|
|
211
|
+
if (Array.isArray(value)) return value.map((item, index) => resolveTemplate(item, context, refs, `${path}.${index}`))
|
|
212
|
+
const record = value as Readonly<Record<string, JsonValue>>
|
|
213
|
+
if (Object.keys(record).some(key => key.startsWith("$"))) {
|
|
214
|
+
const sentinel = Object.hasOwn(record, "$expr")
|
|
215
|
+
? parseSpatialValue(z.strictObject({ $expr: expressionSchema }), record, path).$expr
|
|
216
|
+
: parseSpatialValue(expressionSchema, record, path)
|
|
217
|
+
return evaluateExpression(sentinel, context, refs, path)
|
|
218
|
+
}
|
|
219
|
+
return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, resolveTemplate(item, context, refs, `${path}.${key}`)]))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function generatorPrefix(designId: string): string {
|
|
223
|
+
return `generator_design_${spatialValueSha256({ domain: "slopcamera.spatial-design-owner.v1", designId }).slice(0, 16)}_`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Stable stage ownership survives parameter changes, stage reordering and relabeling. */
|
|
227
|
+
export function spatialDesignGeneratorId(designId: string, stageId: string): string {
|
|
228
|
+
const ids = parseSpatialValue(z.strictObject({ designId: identifier, stageId: identifier }), { designId, stageId }, "design stage identity")
|
|
229
|
+
return `${generatorPrefix(ids.designId)}${spatialValueSha256({ domain: "slopcamera.spatial-design-stage.v1", stageId: ids.stageId }).slice(0, 32)}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Resolves and checks dependencies and static budgets without evaluating a mesh. */
|
|
233
|
+
export function inspectSpatialDesign(input: unknown, options: { readonly parameters?: unknown } = {}): SpatialDesignInspection {
|
|
234
|
+
const design = readDesign(input)
|
|
235
|
+
const parameters = resolveParameters(design, options.parameters)
|
|
236
|
+
const definitions = unique(design.values ?? [], item => item.name, "design.values")
|
|
237
|
+
unique(design.constraints ?? [], item => item.name, "design.constraints")
|
|
238
|
+
unique(design.stages, item => item.stageId, "design.stages")
|
|
239
|
+
const order = spatialTopologicalIds(new Map([...definitions].map(([name, definition]) => [name, [...new Set(valueReferences(definition.expression))].sort(compare)])), "design.values")
|
|
240
|
+
const values = new Map<string, { value: number; dependencies: SpatialDesignDependencies }>()
|
|
241
|
+
const context: ExpressionContext = { parameters, values, budget: { nodes: 0 } }
|
|
242
|
+
for (const name of order) {
|
|
243
|
+
const refs = emptyDependencies()
|
|
244
|
+
const value = evaluateExpression(definitions.get(name)!.expression, context, refs, `design.values.${name}`)
|
|
245
|
+
values.set(name, { value, dependencies: dependencies(refs) })
|
|
246
|
+
}
|
|
247
|
+
const constraints = (design.constraints ?? []).map(constraint => {
|
|
248
|
+
const refs = emptyDependencies(), path = `design.constraints.${constraint.name}`
|
|
249
|
+
const left = evaluateExpression(constraint.left, context, refs, path), right = evaluateExpression(constraint.right, context, refs, path)
|
|
250
|
+
const passed = constraint.operator === "lt" ? left < right : constraint.operator === "lte" ? left <= right
|
|
251
|
+
: constraint.operator === "eq" ? left === right : constraint.operator === "gte" ? left >= right : left > right
|
|
252
|
+
if (!passed) fail(constraint.message ?? `Constraint ${constraint.name} failed: ${left} ${constraint.operator} ${right}.`, path)
|
|
253
|
+
return { name: constraint.name, left, operator: constraint.operator, right, passed }
|
|
254
|
+
})
|
|
255
|
+
const estimate = { parts: 0, assets: 0, vertices: 0, triangles: 0, bytes: 0 }
|
|
256
|
+
const stages = design.stages.map(stage => {
|
|
257
|
+
const refs = emptyDependencies()
|
|
258
|
+
const raw = stage.kind === "parametric" ? stage.spec : {
|
|
259
|
+
kind: "geometry", graph: stage.graph, materials: stage.materials,
|
|
260
|
+
...(stage.transform === undefined ? {} : { transform: stage.transform }),
|
|
261
|
+
...(stage.editable === undefined ? {} : { editable: stage.editable }),
|
|
262
|
+
...(stage.castShadow === undefined ? {} : { castShadow: stage.castShadow }),
|
|
263
|
+
...(stage.receiveShadow === undefined ? {} : { receiveShadow: stage.receiveShadow }),
|
|
264
|
+
}
|
|
265
|
+
const spec = parseSpatialValue(SpatialParametricSpecSchema, resolveTemplate(raw as JsonValue, context, refs, `design.stages.${stage.stageId}`), `design.stages.${stage.stageId}.spec`)
|
|
266
|
+
const stageEstimate = estimateSpatialParametric(spec)
|
|
267
|
+
for (const key of ["parts", "assets", "vertices", "triangles", "bytes"] as const) estimate[key] += stageEstimate[key]
|
|
268
|
+
if (estimate.parts > SPATIAL_DESIGN_LIMITS.parts || estimate.assets > SPATIAL_DESIGN_LIMITS.assets || estimate.vertices > SPATIAL_DESIGN_LIMITS.vertices
|
|
269
|
+
|| estimate.triangles > SPATIAL_DESIGN_LIMITS.triangles || estimate.bytes > SPATIAL_DESIGN_LIMITS.outputBytes) fail("Design aggregate geometry budget exceeded before emission.", "design.stages")
|
|
270
|
+
return { stageId: stage.stageId, ...(stage.name === undefined ? {} : { name: stage.name }), kind: stage.kind,
|
|
271
|
+
generatorId: spatialDesignGeneratorId(design.designId, stage.stageId), spec,
|
|
272
|
+
...(stage.seed === undefined ? {} : { seed: stage.seed }), dependencies: dependencies(refs), estimate: stageEstimate }
|
|
273
|
+
})
|
|
274
|
+
return deepFreezeJson({ design, designSha256: spatialValueSha256({ domain: "slopcamera.spatial-design.v1", design }), parameters,
|
|
275
|
+
values: [...values].map(([name, value]) => ({ name, ...value })), valueOrder: order, constraints, stages, estimate })
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Full authoring validation, including parameter, dependency, constraint and static geometry checks. */
|
|
279
|
+
export function parseSpatialDesign(input: unknown): SpatialDesignV1 { return inspectSpatialDesign(input).design }
|
|
280
|
+
|
|
281
|
+
/** Atomically updates named defaults, validating the complete dependent design before returning. */
|
|
282
|
+
export function editSpatialDesignParameters(input: unknown, updates: unknown): SpatialDesignV1 {
|
|
283
|
+
const design = readDesign(input), parameters = resolveParameters(design, updates)
|
|
284
|
+
return parseSpatialDesign({ ...design, parameters: design.parameters.map(parameter => ({ ...parameter, value: parameters[parameter.name]! })) })
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Emits all validated stages and merges them through existing retained-output/override rules. */
|
|
288
|
+
export function compileSpatialDesign(input: unknown, options: { readonly scene?: unknown; readonly parameters?: unknown } = {}): SpatialDesignCompilation {
|
|
289
|
+
const inspection = inspectSpatialDesign(options.parameters === undefined ? input : editSpatialDesignParameters(input, options.parameters))
|
|
290
|
+
const base = options.scene === undefined ? undefined : parseSpatialScene(options.scene)
|
|
291
|
+
const owned = new Set(inspection.stages.map(stage => stage.generatorId))
|
|
292
|
+
const stale = base?.generators.find(generator => generator.generatorId.startsWith(generatorPrefix(inspection.design.designId)) && !owned.has(generator.generatorId))
|
|
293
|
+
if (stale !== undefined) throw new SpatialSceneError("conflict", `Design removed a retained stage (${stale.generatorId}); explicitly remove its retained output and dependent overrides before recompiling.`, "design.stages")
|
|
294
|
+
const retiredAssets = new Set(base?.generators.filter(generator => owned.has(generator.generatorId)).flatMap(generator => generator.assets ?? []) ?? [])
|
|
295
|
+
const keptAssets = base?.assets.filter(asset => !retiredAssets.has(asset.assetId)).length ?? 0
|
|
296
|
+
const keptEntities = base?.entities.filter(entity => entity.origin.kind !== "generated" || !owned.has(entity.origin.generatorId)).length ?? 0
|
|
297
|
+
const keptGenerators = base?.generators.filter(generator => !owned.has(generator.generatorId)).length ?? 0
|
|
298
|
+
if (keptAssets + inspection.estimate.assets > SPATIAL_SCENE_LIMITS.assets || keptEntities + inspection.estimate.parts > SPATIAL_SCENE_LIMITS.entities || keptGenerators + owned.size > 128) {
|
|
299
|
+
fail("Design and retained scene exceed aggregate scene capacity before emission.", "scene")
|
|
300
|
+
}
|
|
301
|
+
const outputs = inspection.stages.map(stage => emitSpatialParametric({ kind: "slopcamera.spatial-parametric-request", schemaVersion: 1,
|
|
302
|
+
generatorId: stage.generatorId, spec: stage.spec, ...(stage.seed === undefined ? {} : { seed: stage.seed }),
|
|
303
|
+
...(stage.name === undefined ? {} : { name: stage.name }) }))
|
|
304
|
+
const artifactBytes = outputs.reduce((total, output) => total + output.artifacts.reduce((sum, artifact) => sum + artifact.bytes.byteLength, 0), 0)
|
|
305
|
+
if (artifactBytes > SPATIAL_DESIGN_LIMITS.outputBytes) fail("Design emitted artifacts exceed the byte budget.", "design.stages")
|
|
306
|
+
const original = base ?? parseSpatialScene(createSpatialGeneratorSceneShell())
|
|
307
|
+
const scene = applySpatialScenePatch(original, { kind: "slopcamera.spatial-scene-patch", schemaVersion: 1,
|
|
308
|
+
expectedSceneSha256: spatialValueSha256(original),
|
|
309
|
+
operations: outputs.map(output => ({ kind: "replace-generator-output", generator: output.generator, entities: output.entities, assets: output.manifests })),
|
|
310
|
+
}).scene
|
|
311
|
+
const receipt: SpatialDesignReceipt = {
|
|
312
|
+
kind: "slopcamera.spatial-design-receipt", schemaVersion: 1, compiler: SPATIAL_DESIGN_COMPILER,
|
|
313
|
+
designId: inspection.design.designId, designSha256: inspection.designSha256,
|
|
314
|
+
parametersSha256: spatialValueSha256({ domain: "slopcamera.spatial-design-parameters.v1", parameters: inspection.parameters }),
|
|
315
|
+
inputSceneSha256: base === undefined ? null : spatialValueSha256(base), sceneSha256: spatialValueSha256(scene),
|
|
316
|
+
parameters: inspection.parameters, estimate: inspection.estimate, artifactBytes,
|
|
317
|
+
stages: inspection.stages.map((stage, index) => {
|
|
318
|
+
const output = outputs[index]!
|
|
319
|
+
const previous = base?.generators.find(generator => generator.generatorId === stage.generatorId)
|
|
320
|
+
return { stageId: stage.stageId, generatorId: stage.generatorId, specSha256: output.receipt.specSha256,
|
|
321
|
+
receiptSha256: output.receiptSha256, entities: output.receipt.entities, dependencies: stage.dependencies,
|
|
322
|
+
changed: previous === undefined || spatialValueSha256(previous) !== spatialValueSha256(scene.generators.find(generator => generator.generatorId === stage.generatorId)!) }
|
|
323
|
+
}),
|
|
324
|
+
}
|
|
325
|
+
const metadata = deepFreezeJson({ design: inspection.design, scene, receipt, receiptSha256: spatialValueSha256({ domain: "slopcamera.spatial-design-receipt.v1", receipt }) })
|
|
326
|
+
return Object.freeze({ ...metadata, outputs: Object.freeze(outputs) })
|
|
327
|
+
}
|