@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,351 @@
|
|
|
1
|
+
import { deepFreezeJson } from "../code/json-snapshot.js"
|
|
2
|
+
import { lookAtPose, perspectiveFromFov } from "./build.js"
|
|
3
|
+
import type { SpatialMaterial, SpatialSceneV1 } from "./contracts.js"
|
|
4
|
+
import { parseSpatialDesign, type SpatialDesignV1 } from "./design.js"
|
|
5
|
+
import { parseSpatialScene, SpatialSceneError } from "./identity.js"
|
|
6
|
+
|
|
7
|
+
/** Original architectural studies. These functions author inert design data;
|
|
8
|
+
* compilation and rendering remain separate, explicit operations. */
|
|
9
|
+
export interface SpatialDesignTemplate {
|
|
10
|
+
readonly id: string
|
|
11
|
+
readonly name: string
|
|
12
|
+
readonly description: string
|
|
13
|
+
readonly parameterNames: readonly string[]
|
|
14
|
+
readonly cameraId: "camera_hero"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SpatialDesignStarter {
|
|
18
|
+
readonly design: SpatialDesignV1
|
|
19
|
+
readonly scene: SpatialSceneV1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Expression = number | { $param: string } | { $value: string } | { op: string; args: Expression[] }
|
|
23
|
+
type Json = null | boolean | number | string | { [key: string]: Json } | Json[]
|
|
24
|
+
/** Quantize authored constants so libm last-bit differences do not rewrite checked starters. */
|
|
25
|
+
function canonicalTemplateNumbers(value: unknown): unknown {
|
|
26
|
+
if (typeof value === "number") return Math.abs(value) < 1e-12 ? 0 : Number(value.toPrecision(12))
|
|
27
|
+
if (Array.isArray(value)) return value.map(canonicalTemplateNumbers)
|
|
28
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, canonicalTemplateNumbers(child)]))
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
const parseDesign = (value: unknown): SpatialDesignV1 => parseSpatialDesign(canonicalTemplateNumbers(value))
|
|
32
|
+
const p = (name: string): Expression => ({ $param: name })
|
|
33
|
+
const v = (name: string): Expression => ({ $value: name })
|
|
34
|
+
const op = (name: string, ...args: Expression[]): Expression => ({ op: name, args })
|
|
35
|
+
const add = (a: Expression, b: Expression) => op("add", a, b)
|
|
36
|
+
const sub = (a: Expression, b: Expression) => op("sub", a, b)
|
|
37
|
+
const mul = (a: Expression, b: Expression) => op("mul", a, b)
|
|
38
|
+
const div = (a: Expression, b: Expression) => op("div", a, b)
|
|
39
|
+
const sin = (a: Expression) => op("sin", a)
|
|
40
|
+
const cos = (a: Expression) => op("cos", a)
|
|
41
|
+
const expr = (value: Expression): Json => typeof value === "number" ? value : { $expr: value as Json }
|
|
42
|
+
const xyz = (x: Expression, y: Expression, z: Expression): Json[] => [expr(x), expr(y), expr(z)]
|
|
43
|
+
const transform = (position: Json[] = [0, 0, 0], rotation: Json[] = [0, 0, 0, 1]): Json => ({ position, rotation, scale: [1, 1, 1] })
|
|
44
|
+
const yaw = (angle: Expression): Json[] => [0, expr(sin(mul(angle, 0.5))), 0, expr(cos(mul(angle, 0.5)))]
|
|
45
|
+
const parameter = (name: string, label: string, value: number, min: number, max: number, unit: string, step?: number) => ({ name, label, value, min, max, unit, ...(step === undefined ? {} : { step }) })
|
|
46
|
+
const standard = (color: string, roughness = 0.55, metalness = 0): SpatialMaterial => ({ kind: "standard", color, opacity: 1, roughness, metalness })
|
|
47
|
+
const OAK = standard("#a97040", 0.43)
|
|
48
|
+
const WALNUT = standard("#71503c", 0.46)
|
|
49
|
+
const BRONZE = standard("#bc9864", 0.32, 0.64)
|
|
50
|
+
const INK = standard("#283335", 0.39, 0.22)
|
|
51
|
+
const STONE = standard("#d0c4ac", 0.83)
|
|
52
|
+
const CREAM = standard("#eee8dc", 0.87)
|
|
53
|
+
|
|
54
|
+
function geometry(stageId: string, name: string, nodes: Json[], output: string, material: SpatialMaterial, placement?: Json): Json {
|
|
55
|
+
return {
|
|
56
|
+
kind: "geometry", stageId, name,
|
|
57
|
+
graph: { kind: "slopcamera.spatial-geometry-graph", schemaVersion: 1, nodes, output },
|
|
58
|
+
materials: [material as Json], castShadow: true, receiveShadow: true,
|
|
59
|
+
editable: ["color", "transform"], ...(placement === undefined ? {} : { transform: placement }),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function merge(nodes: Json[], inputs: string[]): string {
|
|
64
|
+
if (inputs.length === 1) return inputs[0]!
|
|
65
|
+
nodes.push({ id: "assembly", kind: "merge", inputs })
|
|
66
|
+
return "assembly"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stageScene(id: string, options: { position: [number, number, number]; target: [number, number, number]; extent: number; dark?: boolean }): SpatialSceneV1 {
|
|
70
|
+
const { position, target, extent, dark = false } = options
|
|
71
|
+
const common = { parentId: null, placement: { kind: "world" }, origin: { kind: "authored" }, visible: true }
|
|
72
|
+
const light = (name: string, location: [number, number, number], color: string, intensity: number, shadow = false) => ({
|
|
73
|
+
...common, kind: "light", entityId: `entity_${name}`, name,
|
|
74
|
+
transform: { ...lookAtPose(location, target), scale: [1, 1, 1] },
|
|
75
|
+
light: "directional", color, intensity, shadow,
|
|
76
|
+
})
|
|
77
|
+
const camera = (cameraId: string, name: string, location: [number, number, number], aim: [number, number, number], fovDeg: number) => ({
|
|
78
|
+
cameraId, name, pose: lookAtPose(location, aim),
|
|
79
|
+
projection: perspectiveFromFov({ fovDeg, width: 1440, height: 1080, near: 0.05, far: 300 }),
|
|
80
|
+
})
|
|
81
|
+
return parseSpatialScene(canonicalTemplateNumbers({
|
|
82
|
+
kind: "slopcamera.spatial-scene", schemaVersion: 1, sceneId: `scene_design_${id.replaceAll("-", "_")}`,
|
|
83
|
+
coordinates: "right-handed-y-up-meters", durationUs: 4_000_000,
|
|
84
|
+
entities: [
|
|
85
|
+
{ ...common, kind: "mesh", entityId: "entity_ground", name: "Matte studio ground", transform: transform([0, -0.14, 0]),
|
|
86
|
+
geometry: { kind: "box", size: [200, 0.25, 200] }, material: standard(dark ? "#283739" : "#d7d9d6", 0.94), receiveShadow: true },
|
|
87
|
+
{ ...common, kind: "mesh", entityId: "entity_backdrop", name: "Seamless studio backdrop", transform: transform([0, 20, 0]),
|
|
88
|
+
geometry: { kind: "sphere", radius: 150 },
|
|
89
|
+
material: { kind: "pbr", color: "#000000", opacity: 1, roughness: 1, metalness: 1, doubleSided: true,
|
|
90
|
+
emissive: { color: dark ? "#344245" : "#d9dddb", intensity: 1 } } },
|
|
91
|
+
{ ...common, kind: "light", entityId: "entity_ambient", name: "Soft sky fill", transform: transform(), light: "ambient", color: "#e0ecf1", intensity: 0.7 },
|
|
92
|
+
light("key", [-extent, extent * 1.7, extent], "#fff2df", 2.2, true),
|
|
93
|
+
light("fill", [extent, extent * 0.7, extent * 0.3], "#c6dbef", 0.7),
|
|
94
|
+
light("rim", [extent * 0.3, extent, -extent], "#ffffff", 1.2),
|
|
95
|
+
],
|
|
96
|
+
cameras: [
|
|
97
|
+
camera("camera_hero", "Architectural three-quarter", position, target, 48),
|
|
98
|
+
camera("camera_detail", "Material and connection study", [position[0] * 0.6, target[1] + (position[1] - target[1]) * 0.5, position[2] * 0.6], target, 46),
|
|
99
|
+
camera("camera_plan", "Plan and structural rhythm", [0.01, extent * 2.5, 0], [0, 0, 0], 52),
|
|
100
|
+
],
|
|
101
|
+
assets: [], animations: [], generators: [], overrides: [],
|
|
102
|
+
}))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function pavilion(): SpatialDesignStarter {
|
|
106
|
+
const stations = 29
|
|
107
|
+
const angle = (t: number) => mul(p("bend"), t)
|
|
108
|
+
const centerX = (t: number) => mul(v("curveRadius"), sub(1, cos(angle(t))))
|
|
109
|
+
const centerZ = (t: number) => mul(v("curveRadius"), sin(angle(t)))
|
|
110
|
+
const archPath = Array.from({ length: 25 }, (_, i) => {
|
|
111
|
+
const a = Math.PI * i / 24
|
|
112
|
+
return xyz(mul(p("span"), -0.5 * Math.cos(a)), add(0.16, mul(p("rise"), Math.sin(a))), 0)
|
|
113
|
+
})
|
|
114
|
+
const ribs: Json[] = [
|
|
115
|
+
{ id: "section", kind: "rect", width: expr(p("ribDepth")), height: expr(p("ribWidth")) },
|
|
116
|
+
{ id: "rib", kind: "sweep", profile: "section", path: archPath },
|
|
117
|
+
]
|
|
118
|
+
const ribIds: string[] = []
|
|
119
|
+
for (let i = 0; i < stations; i++) {
|
|
120
|
+
const t = i / (stations - 1) - 0.5, id = `rib_${i}`
|
|
121
|
+
ribs.push({ id, kind: "transform", input: "rib", transform: transform(xyz(centerX(t), 0, centerZ(t)), yaw(angle(t))) })
|
|
122
|
+
ribIds.push(id)
|
|
123
|
+
}
|
|
124
|
+
const deckPath = Array.from({ length: 33 }, (_, i) => {
|
|
125
|
+
const t = i / 32 * 1.1 - 0.55
|
|
126
|
+
return xyz(centerX(t), 0.1, centerZ(t))
|
|
127
|
+
})
|
|
128
|
+
const rails: Json[] = [{ id: "section", kind: "rect", width: 0.075, height: 0.11 }]
|
|
129
|
+
const railIds: string[] = []
|
|
130
|
+
for (const [i, sectionAngle] of [Math.PI / 6, Math.PI / 2, 5 * Math.PI / 6].entries()) {
|
|
131
|
+
const lateral = mul(p("span"), 0.5 * Math.cos(sectionAngle)), id = `rail_${i}`
|
|
132
|
+
rails.push({ id, kind: "sweep", profile: "section", path: Array.from({ length: 33 }, (_, j) => {
|
|
133
|
+
const t = j / 32 - 0.5
|
|
134
|
+
return xyz(add(centerX(t), mul(lateral, cos(angle(t)))), add(0.16, mul(p("rise"), Math.sin(sectionAngle))), sub(centerZ(t), mul(lateral, sin(angle(t)))))
|
|
135
|
+
}) })
|
|
136
|
+
railIds.push(id)
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
design: parseDesign({
|
|
140
|
+
kind: "slopcamera.spatial-design", schemaVersion: 1, designId: "crescent-pavilion",
|
|
141
|
+
parameters: [
|
|
142
|
+
parameter("span", "Clear span", 6.2, 4, 8, "m"), parameter("length", "Walk length", 11.5, 8, 16, "m"),
|
|
143
|
+
parameter("rise", "Crown height", 4.1, 2.8, 5.5, "m"), parameter("bend", "Plan curvature", 1.1, 0.15, 1.7, "rad"),
|
|
144
|
+
parameter("ribDepth", "Rib depth", 0.26, 0.15, 0.38, "m"), parameter("ribWidth", "Rib width", 0.13, 0.08, 0.2, "m"),
|
|
145
|
+
],
|
|
146
|
+
values: [{ name: "curveRadius", expression: div(p("length"), p("bend")) }],
|
|
147
|
+
constraints: [
|
|
148
|
+
{ name: "inside-radius", left: v("curveRadius"), operator: "gt", right: add(mul(p("span"), 0.5), 1), message: "The curve must leave an open inner edge; increase length or reduce bend/span." },
|
|
149
|
+
{ name: "rib-spacing", left: p("ribWidth"), operator: "lt", right: mul(div(p("length"), stations - 1), 0.6), message: "Timber ribs need daylight between them." },
|
|
150
|
+
],
|
|
151
|
+
stages: [
|
|
152
|
+
geometry("timber-ribs", "Laminated oak arch ribs", ribs, merge(ribs, ribIds), OAK),
|
|
153
|
+
geometry("longitudinal-ties", "Bronze longitudinal ties", rails, merge(rails, railIds), BRONZE),
|
|
154
|
+
geometry("curved-plinth", "Continuous limestone walk", [
|
|
155
|
+
{ id: "section", kind: "rect", width: 0.2, height: expr(add(p("span"), 1.1)) },
|
|
156
|
+
{ id: "deck", kind: "sweep", profile: "section", path: deckPath },
|
|
157
|
+
], "deck", STONE),
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
scene: stageScene("crescent-pavilion", { position: [15, 12, 18], target: [0.6, 1.5, 0], extent: 12 }),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function spiralStair(): SpatialDesignStarter {
|
|
165
|
+
const steps = 36
|
|
166
|
+
const theta = (t: number) => mul(p("turns"), Math.PI * 2 * t)
|
|
167
|
+
const elevation = (t: number) => add(0.25, mul(p("height"), t))
|
|
168
|
+
const tread: Json[] = []
|
|
169
|
+
for (let i = 0; i <= 6; i++) tread.push(xyz(mul(p("radius"), cos(mul(v("stepAngle"), i / 6))), mul(p("radius"), sin(mul(v("stepAngle"), i / 6))), 0).slice(0, 2))
|
|
170
|
+
for (let i = 6; i >= 0; i--) tread.push(xyz(mul(p("innerRadius"), cos(mul(v("stepAngle"), i / 6))), mul(p("innerRadius"), sin(mul(v("stepAngle"), i / 6))), 0).slice(0, 2))
|
|
171
|
+
const treads: Json[] = [
|
|
172
|
+
{ id: "outline", kind: "profile", points: tread },
|
|
173
|
+
{ id: "solid", kind: "extrude", profile: "outline", depth: expr(p("treadThickness")) },
|
|
174
|
+
{ id: "tread", kind: "transform", input: "solid", transform: transform([0, 0, 0], [Math.SQRT1_2, 0, 0, Math.SQRT1_2]) },
|
|
175
|
+
]
|
|
176
|
+
const treadIds: string[] = []
|
|
177
|
+
for (let i = 0; i < steps; i++) {
|
|
178
|
+
const id = `tread_${i}`
|
|
179
|
+
treads.push({ id, kind: "transform", input: "tread", transform: transform(xyz(0, elevation((i + 1) / steps), 0), yaw(mul(theta(i / steps), -1))) })
|
|
180
|
+
treadIds.push(id)
|
|
181
|
+
}
|
|
182
|
+
const metal: Json[] = [{ id: "post", kind: "cylinder", radius: 0.018, height: expr(p("railHeight")), segments: 8 }]
|
|
183
|
+
const metalIds: string[] = []
|
|
184
|
+
for (const [side, r] of [sub(p("radius"), 0.06), add(p("innerRadius"), 0.065)].entries()) {
|
|
185
|
+
const posts: string[] = []
|
|
186
|
+
for (let i = 0; i < steps; i++) {
|
|
187
|
+
const t = (i + 0.5) / steps, id = `baluster_${side}_${i}`
|
|
188
|
+
metal.push({ id, kind: "transform", input: "post", transform: transform(xyz(mul(r, cos(theta(t))), add(elevation((i + 1) / steps), mul(p("railHeight"), 0.5)), mul(r, sin(theta(t))))) })
|
|
189
|
+
posts.push(id)
|
|
190
|
+
}
|
|
191
|
+
const id = `balustrade_${side}`
|
|
192
|
+
metal.push({ id, kind: "merge", inputs: posts })
|
|
193
|
+
metalIds.push(id)
|
|
194
|
+
}
|
|
195
|
+
const rails: Json[] = [{ id: "round", kind: "ellipse", radiusX: 0.04, radiusY: 0.04, segments: 12 }]
|
|
196
|
+
const railIds: string[] = []
|
|
197
|
+
for (const [i, radius] of [sub(p("radius"), 0.06), add(p("innerRadius"), 0.065)].entries()) {
|
|
198
|
+
const id = `handrail_${i}`
|
|
199
|
+
rails.push({ id, kind: "sweep", profile: "round", path: Array.from({ length: 73 }, (_, j) => {
|
|
200
|
+
const t = j / 72
|
|
201
|
+
return xyz(mul(radius, cos(theta(t))), add(elevation(t), add(p("railHeight"), div(p("height"), steps * 2))), mul(radius, sin(theta(t))))
|
|
202
|
+
}) })
|
|
203
|
+
railIds.push(id)
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
design: parseDesign({
|
|
207
|
+
kind: "slopcamera.spatial-design", schemaVersion: 1, designId: "spiral-stair",
|
|
208
|
+
parameters: [
|
|
209
|
+
parameter("height", "Total rise", 6, 4.5, 7.2, "m"), parameter("radius", "Outer radius", 2.25, 1.6, 3, "m"),
|
|
210
|
+
parameter("innerRadius", "Central support radius", 0.55, 0.3, 0.9, "m"), parameter("turns", "Spiral turns", 1.25, 1, 1.5, "ratio"),
|
|
211
|
+
parameter("treadThickness", "Stone tread thickness", 0.095, 0.055, 0.13, "m"), parameter("railHeight", "Handrail height", 1.05, 0.9, 1.2, "m"),
|
|
212
|
+
],
|
|
213
|
+
values: [{ name: "stepAngle", expression: mul(div(mul(p("turns"), Math.PI * 2), steps), 0.96) }],
|
|
214
|
+
constraints: [
|
|
215
|
+
{ name: "walking-width", left: sub(p("radius"), p("innerRadius")), operator: "gte", right: 1.1, message: "Leave at least 1.1 m between the central opening and outer edge." },
|
|
216
|
+
{ name: "tread-clearance", left: p("treadThickness"), operator: "lt", right: mul(div(p("height"), steps), 0.85), message: "Tread thickness must leave a visible gap below the next tread." },
|
|
217
|
+
{ name: "headroom", left: div(p("height"), p("turns")), operator: "gte", right: 2.4, message: "The spiral needs at least 2.4 m rise per revolution." },
|
|
218
|
+
],
|
|
219
|
+
stages: [
|
|
220
|
+
geometry("stone-treads", "Radial limestone treads", treads, merge(treads, treadIds), standard("#d5be96", 0.63)),
|
|
221
|
+
geometry("balusters", "Slender bronze balusters", metal, merge(metal, metalIds), BRONZE),
|
|
222
|
+
geometry("handrails", "Continuous dark handrails", rails, merge(rails, railIds), INK),
|
|
223
|
+
geometry("central-support", "Central bronze support", [{ id: "column", kind: "cylinder", radius: expr(add(p("innerRadius"), 0.035)), height: expr(add(p("height"), 0.38)), segments: 64 }], "column", INK, transform(xyz(0, mul(add(p("height"), 0.38), 0.5), 0))),
|
|
224
|
+
geometry("plinth", "Circular limestone plinth", [{ id: "base", kind: "cylinder", radius: expr(add(p("radius"), 0.4)), height: 0.22, segments: 96 }], "base", STONE, transform([0, 0.11, 0])),
|
|
225
|
+
],
|
|
226
|
+
}),
|
|
227
|
+
scene: stageScene("spiral-stair", { position: [11, 9, 12], target: [0, 3.3, 0], extent: 8 }),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function ribbedTower(): SpatialDesignStarter {
|
|
232
|
+
const ribs: Json[] = [{ id: "section", kind: "ellipse", radiusX: expr(p("ribRadius")), radiusY: expr(p("ribRadius")), segments: 8 }]
|
|
233
|
+
const ids: string[] = []
|
|
234
|
+
const radiusAt = (t: number) => mul(p("radius"), add(sub(1, mul(p("taper"), t)), mul(p("belly"), Math.sin(t * Math.PI))))
|
|
235
|
+
ribs.push({ id: "rib", kind: "sweep", profile: "section", path: Array.from({ length: 25 }, (_, j) => {
|
|
236
|
+
const t = j / 24, a = mul(p("twist"), t), r = radiusAt(t)
|
|
237
|
+
return xyz(mul(r, cos(a)), add(0.35, mul(p("height"), t)), mul(r, sin(a)))
|
|
238
|
+
}) })
|
|
239
|
+
for (let i = 0; i < 40; i++) {
|
|
240
|
+
const id = `rib_${i}`, phase = i / 40 * Math.PI * 2
|
|
241
|
+
ribs.push({ id, kind: "transform", input: "rib", transform: transform([0, 0, 0], [0, Math.sin(-phase / 2), 0, Math.cos(-phase / 2)]) })
|
|
242
|
+
ids.push(id)
|
|
243
|
+
}
|
|
244
|
+
const floors: Json[] = []
|
|
245
|
+
const floorIds: string[] = []
|
|
246
|
+
for (let i = 0; i <= 12; i++) {
|
|
247
|
+
const t = i / 12, id = `floor_${i}`
|
|
248
|
+
floors.push({ id: `${id}_solid`, kind: "cylinder", radius: expr(sub(radiusAt(t), 0.025)), height: 0.12, segments: 64 })
|
|
249
|
+
floors.push({ id, kind: "transform", input: `${id}_solid`, transform: transform(xyz(0, add(0.35, mul(p("height"), t)), 0)) })
|
|
250
|
+
floorIds.push(id)
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
design: parseDesign({
|
|
254
|
+
kind: "slopcamera.spatial-design", schemaVersion: 1, designId: "ribbed-tower",
|
|
255
|
+
parameters: [
|
|
256
|
+
parameter("height", "Tower height", 16, 12, 21, "m"), parameter("radius", "Ground radius", 3.25, 2.5, 4.2, "m"),
|
|
257
|
+
parameter("twist", "Facade rotation", 1.35, 0, 2.3, "rad"), parameter("taper", "Crown taper", 0.27, 0.05, 0.45, "ratio"),
|
|
258
|
+
parameter("belly", "Mid-height swell", 0.2, 0, 0.4, "ratio"), parameter("ribRadius", "Bronze rib radius", 0.065, 0.035, 0.11, "m"),
|
|
259
|
+
],
|
|
260
|
+
constraints: [{ name: "rib-daylight", left: mul(p("ribRadius"), 3), operator: "lt", right: mul(mul(p("radius"), sub(1, p("taper"))), Math.PI * 2 / 40), message: "The narrow crown needs daylight between adjacent bronze ribs." }],
|
|
261
|
+
stages: [
|
|
262
|
+
geometry("bronze-exoskeleton", "Forty continuous twisting bronze ribs", ribs, merge(ribs, ids), BRONZE),
|
|
263
|
+
geometry("floor-plates", "Thirteen recessed floor plates", floors, merge(floors, floorIds), standard("#33494a", 0.42, 0.3)),
|
|
264
|
+
geometry("central-core", "Opaque central service core", [{ id: "core", kind: "cylinder", radius: expr(mul(p("radius"), 0.47)), height: expr(p("height")), segments: 64 }], "core", standard("#253738", 0.33, 0.3), transform(xyz(0, add(0.35, mul(p("height"), 0.5)), 0))),
|
|
265
|
+
geometry("podium", "Low circular podium", [{ id: "podium", kind: "cylinder", radius: expr(add(p("radius"), 0.9)), height: 0.32, segments: 96 }], "podium", standard("#a6a795", 0.83), transform([0, 0.16, 0])),
|
|
266
|
+
],
|
|
267
|
+
}),
|
|
268
|
+
scene: stageScene("ribbed-tower", { position: [24, 17, 29], target: [0, 8, 0], extent: 17, dark: true }),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function bookshelf(): SpatialDesignStarter {
|
|
273
|
+
const board: Json[] = [
|
|
274
|
+
{ id: "board", kind: "box", size: xyz(p("width"), p("thickness"), p("depth")) },
|
|
275
|
+
{ id: "levels", kind: "array", input: "board", count: expr(add(p("rows"), 1)), step: xyz(0, v("rowPitch"), 0) },
|
|
276
|
+
]
|
|
277
|
+
const uprights: Json[] = [
|
|
278
|
+
{ id: "upright", kind: "box", size: xyz(p("thickness"), sub(p("height"), p("thickness")), p("depth")) },
|
|
279
|
+
{ id: "bays", kind: "array", input: "upright", count: expr(add(p("bays"), 1)), step: xyz(v("bayPitch"), 0, 0) },
|
|
280
|
+
]
|
|
281
|
+
const cellX = (column: number) => add(mul(p("width"), -0.5), add(mul(p("thickness"), 0.5), mul(v("bayPitch"), column + 0.5)))
|
|
282
|
+
const shelfY = (row: number) => add(0.19, add(mul(v("rowPitch"), row), p("thickness")))
|
|
283
|
+
const backs: Json[] = [{ id: "panel", kind: "box", size: xyz(sub(v("bayPitch"), p("thickness")), sub(v("rowPitch"), p("thickness")), 0.018) }]
|
|
284
|
+
const backIds: string[] = []
|
|
285
|
+
for (const [i, [column, row]] of [[0, 0], [2, 1], [1, 2]].entries()) {
|
|
286
|
+
const id = `back_${i}`
|
|
287
|
+
backs.push({ id, kind: "transform", input: "panel", transform: transform(xyz(cellX(column!), add(shelfY(row!), mul(sub(v("rowPitch"), p("thickness")), 0.5)), mul(p("depth"), -0.5))) })
|
|
288
|
+
backIds.push(id)
|
|
289
|
+
}
|
|
290
|
+
const books: Json[] = [], bookIds: string[] = []
|
|
291
|
+
for (let i = 0; i < 8; i++) {
|
|
292
|
+
const id = `book_${i}`, column = i < 4 ? 0 : 2, row = i < 4 ? 1 : 0, offset = (i % 4 - 1.5) * 0.07
|
|
293
|
+
const bookHeight = mul(v("rowPitch"), 0.58 + (i % 3) * 0.09)
|
|
294
|
+
books.push({ id: `${id}_solid`, kind: "box", size: xyz(0.055, bookHeight, mul(p("depth"), 0.68)) })
|
|
295
|
+
books.push({ id, kind: "transform", input: `${id}_solid`, transform: transform(xyz(add(cellX(column), offset), add(shelfY(row), mul(bookHeight, 0.5)), mul(p("depth"), 0.04))) })
|
|
296
|
+
bookIds.push(id)
|
|
297
|
+
}
|
|
298
|
+
const vase: Json[] = [
|
|
299
|
+
{ id: "profile", kind: "profile", points: [[0, 0], [0.075, 0], [0.1, 0.06], [0.105, 0.16], [0.08, 0.24], [0.048, 0.27], [0.048, 0.32], [0, 0.32]] },
|
|
300
|
+
{ id: "vase", kind: "revolve", profile: "profile", segments: 48 },
|
|
301
|
+
]
|
|
302
|
+
return {
|
|
303
|
+
design: parseDesign({
|
|
304
|
+
kind: "slopcamera.spatial-design", schemaVersion: 1, designId: "modular-bookshelf",
|
|
305
|
+
parameters: [
|
|
306
|
+
parameter("width", "Overall width", 4.8, 3.6, 6.2, "m"), parameter("height", "Overall height", 2.7, 2.2, 3.4, "m"),
|
|
307
|
+
parameter("depth", "Shelf depth", 0.42, 0.3, 0.6, "m"), parameter("thickness", "Board thickness", 0.042, 0.025, 0.065, "m"),
|
|
308
|
+
parameter("bays", "Vertical bays", 5, 3, 8, "count", 1), parameter("rows", "Shelf rows", 4, 3, 6, "count", 1),
|
|
309
|
+
],
|
|
310
|
+
values: [
|
|
311
|
+
{ name: "bayPitch", expression: div(sub(p("width"), p("thickness")), p("bays")) },
|
|
312
|
+
{ name: "rowPitch", expression: div(sub(p("height"), p("thickness")), p("rows")) },
|
|
313
|
+
],
|
|
314
|
+
constraints: [
|
|
315
|
+
{ name: "book-clearance", left: sub(v("rowPitch"), p("thickness")), operator: "gte", right: 0.34, message: "Shelf openings must leave 0.34 m for the retained ceramic and books." },
|
|
316
|
+
{ name: "bay-clearance", left: sub(v("bayPitch"), p("thickness")), operator: "gte", right: 0.4, message: "Bays need at least 0.4 m clear width." },
|
|
317
|
+
{ name: "shelf-span", left: v("bayPitch"), operator: "lte", right: 1.25, message: "Keep unsupported shelf spans at or below 1.25 m." },
|
|
318
|
+
],
|
|
319
|
+
stages: [
|
|
320
|
+
geometry("horizontal-shelves", "Continuous walnut shelves", board, "levels", WALNUT, transform(xyz(0, add(0.19, mul(p("thickness"), 0.5)), 0))),
|
|
321
|
+
geometry("vertical-dividers", "Walnut uprights", uprights, "bays", WALNUT, transform(xyz(mul(sub(p("width"), p("thickness")), -0.5), add(0.19, mul(p("height"), 0.5)), 0))),
|
|
322
|
+
geometry("inset-backs", "Alternating terracotta backs", backs, merge(backs, backIds), standard("#ad6048", 0.89)),
|
|
323
|
+
geometry("books", "Indigo clothbound books", books, merge(books, bookIds), standard("#536d80", 0.87)),
|
|
324
|
+
geometry("ceramic", "Ivory turned ceramic", vase, "vase", CREAM, transform(xyz(cellX(1), shelfY(2), 0.015))),
|
|
325
|
+
geometry("plinth", "Recessed dark plinth", [{ id: "plinth", kind: "box", size: xyz(sub(p("width"), 0.14), 0.2, sub(p("depth"), 0.1)) }], "plinth", INK, transform([0, 0.1, 0])),
|
|
326
|
+
],
|
|
327
|
+
}),
|
|
328
|
+
scene: stageScene("modular-bookshelf", { position: [5.2, 3.4, 7.8], target: [0, 1.35, 0], extent: 6 }),
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const TEMPLATES: readonly SpatialDesignTemplate[] = deepFreezeJson([
|
|
333
|
+
{ id: "crescent-pavilion", name: "Crescent timber pavilion", description: "Twenty-nine oak arches follow a curved limestone walk; span, rise and curvature reshape the whole assembly.", parameterNames: ["span", "length", "rise", "bend", "ribDepth", "ribWidth"], cameraId: "camera_hero" },
|
|
334
|
+
{ id: "spiral-stair", name: "Sculptural spiral stair", description: "Thirty-six stone treads, bronze balusters and continuous handrails share one controlled helix.", parameterNames: ["height", "radius", "innerRadius", "turns", "treadThickness", "railHeight"], cameraId: "camera_hero" },
|
|
335
|
+
{ id: "ribbed-tower", name: "Twisting bronze tower", description: "Forty swept bronze ribs wrap thirteen floor plates with a shared taper, swell and twist.", parameterNames: ["height", "radius", "twist", "taper", "belly", "ribRadius"], cameraId: "camera_hero" },
|
|
336
|
+
{ id: "modular-bookshelf", name: "Modular walnut bookshelf", description: "An editable furniture system with linked bays, rows, joinery thickness and retained display objects.", parameterNames: ["width", "height", "depth", "thickness", "bays", "rows"], cameraId: "camera_hero" },
|
|
337
|
+
])
|
|
338
|
+
|
|
339
|
+
/** The fixed catalog is inert, deterministic metadata suitable for CLI discovery. */
|
|
340
|
+
export function listSpatialDesignTemplates(): readonly SpatialDesignTemplate[] { return TEMPLATES }
|
|
341
|
+
|
|
342
|
+
/** Return editable source plus a staged base scene, without generating assets. */
|
|
343
|
+
export function createSpatialDesignStarter(id: unknown): SpatialDesignStarter {
|
|
344
|
+
if (typeof id !== "string") throw new SpatialSceneError("invalid-data", "Spatial design template must be a catalog ID string.", "template")
|
|
345
|
+
const factories: Record<string, () => SpatialDesignStarter> = {
|
|
346
|
+
"crescent-pavilion": pavilion, "spiral-stair": spiralStair, "ribbed-tower": ribbedTower, "modular-bookshelf": bookshelf,
|
|
347
|
+
}
|
|
348
|
+
const factory = Object.prototype.hasOwnProperty.call(factories, id) ? factories[id] : undefined
|
|
349
|
+
if (factory === undefined) throw new SpatialSceneError("invalid-data", `Unknown spatial design template ${id.slice(0, 80)}. Choose ${TEMPLATES.map(template => template.id).join(", ")}.`, "template")
|
|
350
|
+
return deepFreezeJson(factory())
|
|
351
|
+
}
|