@hraness/slopcamera 3.3.1 → 3.3.3
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 +3 -3
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/commands.ts +1 -1
- package/apps/desktop/cli/spatial-scene-service.ts +6 -3
- package/apps/desktop/dist/cli/main.js +220 -220
- package/dist/cli.js +1 -1
- package/dist/code/index.js +4 -4
- package/dist/{index-f0ptv8dm.js → index-3e1pcxb5.js} +2 -2
- package/dist/index-z1kd1x8j.js +3 -0
- package/dist/index.js +1 -1
- package/docs/studio.md +12 -0
- package/package.json +1 -1
- package/skills/slopcamera/SKILL.md +1 -0
- package/skills/slopcamera/references/directed-scenes.md +26 -0
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +4 -0
- package/skills/slopcamera/references/native-studio.md +19 -0
- package/skills/slopcamera/references/parametric-design.md +5 -1
- package/skills/slopcamera/references/scene-building.md +143 -1
- package/src/mcp/tools.ts +6 -1
- package/src/spatial-scene/behavior-bake.ts +2 -3
- package/src/spatial-scene/behavior-gallery.ts +2 -3
- package/src/spatial-scene/behavior.ts +2 -3
- package/src/spatial-scene/geometry.ts +88 -35
- package/src/spatial-scene/parametric.ts +42 -15
- package/src/spatial-scene/performance.ts +19 -19
- package/src/version.ts +1 -1
- package/dist/index-cgpkyjqa.js +0 -3
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { SpatialAssetFactsV1Schema, SpatialCollisionProxySchema, type SpatialAssetFactsV1, type SpatialCollisionProxy, type SpatialRetainedArtifact } from "./asset-admission.js"
|
|
19
19
|
import { mulberry32 } from "./build.js"
|
|
20
20
|
import {
|
|
21
|
-
SPATIAL_GEOMETRY_PROFILE, SpatialGeometryGraphSchema, emitSpatialGeometryGlb, evaluateSpatialGeometry, estimateSpatialGeometryGraph, parseSpatialGeometryGraph,
|
|
21
|
+
SPATIAL_GEOMETRY_PROFILE, SPATIAL_GEOMETRY_BOOLEAN_COMPILER, SpatialGeometryGraphSchema, emitSpatialGeometryGlb, evaluateSpatialGeometry, estimateSpatialGeometryGraph, parseSpatialGeometryGraph,
|
|
22
22
|
type SpatialGeometryMesh,
|
|
23
23
|
} from "./geometry.js"
|
|
24
24
|
import {
|
|
@@ -68,6 +68,8 @@ const specVec3 = z.tuple([coordinate, coordinate, coordinate])
|
|
|
68
68
|
const editable = z.array(z.enum(["color", "opacity", "transform"])).max(3).optional()
|
|
69
69
|
const partPlacement = { transform: SpatialTransformSchema.optional(), editable, castShadow: z.boolean().optional(), receiveShadow: z.boolean().optional() }
|
|
70
70
|
|
|
71
|
+
/** center is [horizontal center, sill height from the wall base]. Arch height
|
|
72
|
+
* includes the semicircular crown, whose radius is half its width. */
|
|
71
73
|
const opening = z.discriminatedUnion("kind", [
|
|
72
74
|
z.strictObject({ kind: z.literal("rect"), center: specVec2, width: meter, height: meter }),
|
|
73
75
|
z.strictObject({ kind: z.literal("arch"), center: specVec2, width: meter, height: meter }),
|
|
@@ -155,6 +157,9 @@ function boxBounds(bounds: Bounds): SpatialCollisionProxy {
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
const LOD1_DISTANCE_M = 25
|
|
160
|
+
// Wall lowering changed while the retained GLB/facts format remains v1. Keep
|
|
161
|
+
// the revision local to this planner so unrelated generators retain identity.
|
|
162
|
+
export const SPATIAL_PARAMETRIC_WALL_COMPILER = "slopcamera.parametric-wall-v2" as const
|
|
158
163
|
|
|
159
164
|
function planWall(spec: Extract<SpatialParametricSpec, { kind: "wall" }>): PartPlan[] {
|
|
160
165
|
const { length, height, thickness } = spec
|
|
@@ -165,19 +170,33 @@ function planWall(spec: Extract<SpatialParametricSpec, { kind: "wall" }>): PartP
|
|
|
165
170
|
if (cx - item.width / 2 < -length / 2 || cx + item.width / 2 > length / 2 || baseY < 0 || baseY + item.height > height) {
|
|
166
171
|
pfail(`Wall opening ${index} must stay inside the wall rectangle.`, "spec.openings")
|
|
167
172
|
}
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
const cutterId = `cut${index}`
|
|
174
|
+
// The body is still centered until the final lift. Authored opening Y is
|
|
175
|
+
// measured from its base, so cutters must share the body's local frame.
|
|
176
|
+
let cutterY = baseY + item.height / 2 - height / 2
|
|
172
177
|
if (item.kind === "arch") {
|
|
173
178
|
const radius = item.width / 2
|
|
174
179
|
if (radius > item.height) pfail(`Arch opening ${index} rise exceeds its height.`, "spec.openings")
|
|
175
|
-
const
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
const springline = item.height - radius
|
|
181
|
+
// One extruded cutter has a flat sill and a semicircular crown. A full
|
|
182
|
+
// circle would cut below a shallow opening, while a full-height box
|
|
183
|
+
// would remove the masonry that should remain at the arch shoulders.
|
|
184
|
+
const points: number[][] = [[-radius, 0], [radius, 0]]
|
|
185
|
+
if (springline > 0) points.push([radius, springline])
|
|
186
|
+
for (let segment = 1; segment <= 12; segment++) {
|
|
187
|
+
const angle = Math.PI * segment / 12
|
|
188
|
+
points.push(segment === 12 ? [-radius, springline] : [radius * Math.cos(angle), springline + radius * Math.sin(angle)])
|
|
189
|
+
}
|
|
190
|
+
// At height == radius the arc ends at the first profile point.
|
|
191
|
+
if (springline === 0) points.pop()
|
|
192
|
+
cutterNodes.push({ id: `${cutterId}profile`, kind: "profile", points })
|
|
193
|
+
cutterNodes.push({ id: cutterId, kind: "extrude", profile: `${cutterId}profile`, depth: thickness * 4 })
|
|
194
|
+
cutterY = baseY - height / 2
|
|
195
|
+
} else {
|
|
196
|
+
cutterNodes.push({ id: cutterId, kind: "box", size: [item.width, item.height, thickness * 4] })
|
|
180
197
|
}
|
|
198
|
+
cutterNodes.push({ id: `${cutterId}p`, kind: "transform", input: cutterId, transform: { position: [cx, cutterY, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] } })
|
|
199
|
+
cutterIds.push(`${cutterId}p`)
|
|
181
200
|
}
|
|
182
201
|
const nodes: unknown[] = [{ id: "body", kind: "box", size: [length, height, thickness] }]
|
|
183
202
|
let output = "body"
|
|
@@ -534,6 +553,8 @@ export interface SpatialParametricArtifact { readonly assetId: string; readonly
|
|
|
534
553
|
export interface SpatialParametricFacts { readonly manifest: SpatialAssetManifest; readonly facts: SpatialAssetFactsV1 }
|
|
535
554
|
export interface SpatialParametricReceipt {
|
|
536
555
|
readonly name?: string
|
|
556
|
+
readonly compiler?: typeof SPATIAL_PARAMETRIC_WALL_COMPILER
|
|
557
|
+
readonly booleanCompiler?: typeof SPATIAL_GEOMETRY_BOOLEAN_COMPILER
|
|
537
558
|
readonly kind: "slopcamera.spatial-parametric-receipt"
|
|
538
559
|
readonly schemaVersion: 1
|
|
539
560
|
readonly generatorId: string
|
|
@@ -580,14 +601,19 @@ export function emitSpatialParametric(input: unknown): SpatialParametricOutput {
|
|
|
580
601
|
outputs: [...outputs],
|
|
581
602
|
})
|
|
582
603
|
}
|
|
583
|
-
const sourceSha256 = spatialValueSha256({ domain: "slopcamera.parametric-source.v1", profile: SPATIAL_GEOMETRY_PROFILE, kind: spec.kind })
|
|
584
|
-
const parametersSha256 = spatialGeneratorParametersSha256(spec)
|
|
585
|
-
const specSha256 = spatialValueSha256({ domain: "slopcamera.parametric-spec.v1", spec })
|
|
586
|
-
const seed = request.seed ?? deriveSpatialGeneratorSeed(sourceSha256)
|
|
587
|
-
const runtimeSha256 = spatialValueSha256({ domain: "slopcamera.parametric-runtime.v1", profile: SPATIAL_GEOMETRY_PROFILE })
|
|
588
604
|
const plans = PLANNERS[spec.kind](spec as never)
|
|
589
605
|
if (plans.length > SPATIAL_PARAMETRIC_LIMITS.parts) pfail("Spec expands beyond the part budget.", "spec")
|
|
590
606
|
validatePartMaterials(plans)
|
|
607
|
+
const usesBoolean = plans.some(part => part.lods.some(lod => parseSpatialGeometryGraph(lod.graph).nodes.some(node => node.kind === "boolean")))
|
|
608
|
+
const compiler: Pick<SpatialParametricReceipt, "compiler" | "booleanCompiler"> = {
|
|
609
|
+
...(spec.kind === "wall" ? { compiler: SPATIAL_PARAMETRIC_WALL_COMPILER } : {}),
|
|
610
|
+
...(usesBoolean ? { booleanCompiler: SPATIAL_GEOMETRY_BOOLEAN_COMPILER } : {}),
|
|
611
|
+
}
|
|
612
|
+
const sourceSha256 = spatialValueSha256({ domain: "slopcamera.parametric-source.v1", profile: SPATIAL_GEOMETRY_PROFILE, kind: spec.kind, ...compiler })
|
|
613
|
+
const parametersSha256 = spatialGeneratorParametersSha256(spec)
|
|
614
|
+
const specSha256 = spatialValueSha256({ domain: "slopcamera.parametric-spec.v1", spec })
|
|
615
|
+
const seed = request.seed ?? deriveSpatialGeneratorSeed(sourceSha256)
|
|
616
|
+
const runtimeSha256 = spatialValueSha256({ domain: "slopcamera.parametric-runtime.v1", profile: SPATIAL_GEOMETRY_PROFILE, ...compiler })
|
|
591
617
|
const manifests: SpatialAssetManifest[] = []
|
|
592
618
|
const artifacts: SpatialParametricArtifact[] = []
|
|
593
619
|
const entities: SpatialEntity[] = []
|
|
@@ -682,6 +708,7 @@ export function emitSpatialParametric(input: unknown): SpatialParametricOutput {
|
|
|
682
708
|
generatorId, specSha256, parametersSha256, seed,
|
|
683
709
|
...(request.name === undefined ? {} : { name: request.name }),
|
|
684
710
|
profile: SPATIAL_GEOMETRY_PROFILE,
|
|
711
|
+
...compiler,
|
|
685
712
|
assets: receiptAssets,
|
|
686
713
|
entities: entities.map(entity => entity.entityId).sort(),
|
|
687
714
|
native: retainedArtifacts.map(artifact => ({ requestSha256: artifact.requestSha256, receiptSha256: artifact.receiptSha256 })),
|
|
@@ -76,7 +76,7 @@ export const SPATIAL_PERFORMANCE_BODY_MASKS = Object.freeze({
|
|
|
76
76
|
|
|
77
77
|
export type SpatialPerformanceBodyMaskName = keyof typeof SPATIAL_PERFORMANCE_BODY_MASKS
|
|
78
78
|
|
|
79
|
-
export const SPATIAL_PERFORMANCE_COMPILER_ID = "slopcamera.spatial-performance-compiler@
|
|
79
|
+
export const SPATIAL_PERFORMANCE_COMPILER_ID = "slopcamera.spatial-performance-compiler@v3"
|
|
80
80
|
|
|
81
81
|
const humanoidBoneName = z.enum(HUMANOID_BONE_NAMES)
|
|
82
82
|
const unit = z.number().finite().min(0).max(1)
|
|
@@ -1150,15 +1150,15 @@ export function compileSpatialPerformance(planInput: unknown, sourcesInput: unkn
|
|
|
1150
1150
|
const order = mappedBoneOrder(sources.mapping)
|
|
1151
1151
|
|
|
1152
1152
|
const morphDirectives = plan.directives.filter((d) => d.kind === "morph") as Extract<SpatialPerformancePlan["directives"][number], { kind: "morph" }>[]
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1153
|
+
const attachmentDirectives = plan.directives.filter((d) => d.kind === "attach" || d.kind === "release")
|
|
1154
|
+
.sort((a, b) => a.startUs - b.startUs || Number(a.kind === "attach") - Number(b.kind === "attach"))
|
|
1155
1155
|
const springDirectives = plan.directives.filter((d) => d.kind === "spring") as Extract<SpatialPerformancePlan["directives"][number], { kind: "spring" }>[]
|
|
1156
1156
|
const lookAtDirectives = plan.directives.filter((d) => d.kind === "look-at") as Extract<SpatialPerformancePlan["directives"][number], { kind: "look-at" }>[]
|
|
1157
1157
|
const twoBoneDirectives = plan.directives.filter((d) => d.kind === "two-bone-ik") as Extract<SpatialPerformancePlan["directives"][number], { kind: "two-bone-ik" }>[]
|
|
1158
1158
|
const footPlantDirectives = plan.directives.filter((d) => d.kind === "foot-plant") as Extract<SpatialPerformancePlan["directives"][number], { kind: "foot-plant" }>[]
|
|
1159
1159
|
const rootTrajectory = plan.directives.find((d) => d.kind === "root-trajectory") as Extract<SpatialPerformancePlan["directives"][number], { kind: "root-trajectory" }> | undefined
|
|
1160
1160
|
|
|
1161
|
-
const propIds = new Set<string>([...Object.keys(sources.props), ...
|
|
1161
|
+
const propIds = new Set<string>([...Object.keys(sources.props), ...attachmentDirectives.map((d) => d.propId)])
|
|
1162
1162
|
|
|
1163
1163
|
type MutableSample = {
|
|
1164
1164
|
timeUs: number
|
|
@@ -1307,22 +1307,22 @@ export function compileSpatialPerformance(planInput: unknown, sourcesInput: unkn
|
|
|
1307
1307
|
let attached = false
|
|
1308
1308
|
let parentBone: string | undefined
|
|
1309
1309
|
let localOffset: { position: [number, number, number]; rotation: [number, number, number, number]; scale: [number, number, number] } | undefined
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1310
|
+
// Ownership follows the latest event, including expired attaches so an
|
|
1311
|
+
// older interval cannot resume. At one instant, release closes the old
|
|
1312
|
+
// binding before attach opens the new one. Stable sorting preserves
|
|
1313
|
+
// authored precedence among simultaneous attaches.
|
|
1314
|
+
let latest: (typeof attachmentDirectives)[number] | undefined
|
|
1315
|
+
for (const directive of attachmentDirectives) {
|
|
1316
|
+
if (directive.startUs > timeUs) break
|
|
1317
|
+
if (directive.propId === propId) latest = directive
|
|
1320
1318
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
localOffset
|
|
1319
|
+
if (latest?.kind === "attach" && timeUs < latest.endUs) {
|
|
1320
|
+
attached = true
|
|
1321
|
+
parentBone = latest.bone
|
|
1322
|
+
localOffset = {
|
|
1323
|
+
position: [latest.localOffset.position[0], latest.localOffset.position[1], latest.localOffset.position[2]],
|
|
1324
|
+
rotation: [latest.localOffset.rotation[0], latest.localOffset.rotation[1], latest.localOffset.rotation[2], latest.localOffset.rotation[3]],
|
|
1325
|
+
scale: [latest.localOffset.scale[0], latest.localOffset.scale[1], latest.localOffset.scale[2]],
|
|
1326
1326
|
}
|
|
1327
1327
|
}
|
|
1328
1328
|
let worldPosition: [number, number, number] = [0, 0, 0]
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SLOPCAMERA_VERSION = "3.3.
|
|
1
|
+
export const SLOPCAMERA_VERSION = "3.3.3" as const
|