Give machine props world-space 3D form that rotates with the camera

Scene shapes, cable anchors, bar angles, pad perpendiculars, and roller
offsets all resolve in the authored view exactly as before, then rotate
about the world-vertical axis through the root anchor - the same
resolve-then-rotate pattern as the figure's pins and the mat - so at the
authored yaw every exercise renders bit-identically to today, and under
an orbiting camera the equipment turns with the figure while staying
welded to its hands and feet. Scene lines gain an optional depth plane
(z) and slab extrusion (depth) so seats, backrests, and platforms keep
form edge-on; the rect shape is retired (re-authored as slab lines).
All 14 machines' props re-authored with depths and verified at eight
orbit angles. The fixture snapshots move into the pipeline as
render.py --fixtures and now cover orbit-presentation samples with
resolved prop primitives for a spread of prop flavors; the in-app
renderer resolves props in MotionSolver (lockstep with resolve_props)
and the view just draws primitives.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
2026-07-06 22:15:45 -04:00
parent ed906535b6
commit 81186c51b1
92 changed files with 723 additions and 403 deletions
+70 -5
View File
@@ -114,7 +114,9 @@ struct ExerciseMotionTests {
/// The projected-geometry ground truth: for every exercise and key frame, the draw
/// order and near/far shading must match exactly and every point land within 0.5 px
/// of the reference, plus the mid-tween sample and (Bird Dog) two orbit-camera views.
/// of the reference, plus the mid-tween sample and orbit-camera views taken through
/// the presentation path resolve at the authored camera, clear the pins, rotate
/// the posed body with the equipment layer resolved into rotated primitives.
@Test func figureFixturesMatchReference() throws {
let bundle = Bundle(for: FigureFixtureMarker.self)
let url = try #require(
@@ -143,9 +145,20 @@ struct ExerciseMotionTests {
expectMatch(MotionSolver.frameGeometry(mid, prof: profile, cam: cam).1,
exercise.tween.sample, "\(exercise.name) tween")
for orbit in exercise.orbit ?? [] {
expectMatch(MotionSolver.frameGeometry(norms[0], prof: profile, cam: orbit.yaw).1,
orbit.sample, "\(exercise.name) orbit \(orbit.yaw)")
guard let orbits = exercise.orbit, !orbits.isEmpty else { continue }
let (posedFrame, authored) = MotionSolver.frameGeometry(norms[0], prof: profile, cam: cam)
var posed = posedFrame
posed.pins = [:]
for orbit in orbits {
let geo = MotionSolver.frameGeometry(posed, prof: profile, cam: orbit.yaw).1
expectMatch(geo, orbit.sample, "\(exercise.name) orbit \(orbit.yaw)")
guard let propFixture = orbit.sample.props else { continue }
let rotation = MotionSolver.propRotation(pitch: fixtures.pitch, yawOffset: orbit.yaw - cam)
let (bg, fg) = MotionSolver.resolveProps(
resources.motion.props ?? [], geo: geo, authored: authored,
anchor: norms[0].rootPos, rotation: rotation)
expectPrims(bg, propFixture.bg, "\(exercise.name) orbit \(orbit.yaw) bg")
expectPrims(fg, propFixture.fg, "\(exercise.name) orbit \(orbit.yaw) fg")
}
}
}
@@ -158,6 +171,7 @@ private final class FigureFixtureMarker {}
private struct FigureFixtures: Decodable {
let profile: String
let pitch: Double
let exercises: [FixtureExercise]
}
@@ -182,13 +196,30 @@ private struct FixtureSample: Decodable {
let armL: [[Double]]
let legR: [[Double]]
let legL: [[Double]]
let props: FixtureProps?
enum CodingKeys: String, CodingKey {
case order, shade, spine, head, nose
case order, shade, spine, head, nose, props
case armR = "arm_r", armL = "arm_l", legR = "leg_r", legL = "leg_l"
}
}
private struct FixtureProps: Decodable {
let bg: [FixturePrim]
let fg: [FixturePrim]
}
/// One reference prop primitive: a line/poly (`pts`) or a circle (`c` + `r`).
private struct FixturePrim: Decodable {
let kind: String
let pts: [[Double]]?
let c: [Double]?
let r: Double?
let w: Double?
let fill: Bool?
let color: String
}
private func expectClose(_ point: CGPoint, _ expected: [Double], _ label: String) {
#expect(abs(Double(point.x) - expected[0]) < 0.5 && abs(Double(point.y) - expected[1]) < 0.5,
"\(label): got (\(point.x), \(point.y)) expected \(expected)")
@@ -227,3 +258,37 @@ private func expectMatch(_ geo: FigureGeometry, _ fixture: FixtureSample, _ labe
expectChain(geo.limbs[.legR], fixture.legR, "\(label).leg_r")
expectChain(geo.limbs[.legL], fixture.legL, "\(label).leg_l")
}
/// One resolved equipment layer against its reference: same primitive kinds and inks
/// in the same order, every point within 0.5 px, radii and widths matching.
private func expectPrims(_ prims: [PropPrimitive], _ fixtures: [FixturePrim], _ label: String) {
guard prims.count == fixtures.count else {
Issue.record("\(label): \(prims.count) primitives, expected \(fixtures.count)")
return
}
for (index, (prim, fixture)) in zip(prims, fixtures).enumerated() {
let tag = "\(label)[\(index)]"
switch prim {
case .line(let points, let width, let ink):
#expect(fixture.kind == "line", "\(tag) kind")
expectChain(points, fixture.pts ?? [], tag)
#expect(width == (fixture.w ?? 4), "\(tag) width")
expectInk(ink, fixture.color, tag)
case .poly(let points, let width, let ink):
#expect(fixture.kind == "poly", "\(tag) kind")
expectChain(points, fixture.pts ?? [], tag)
#expect(width == (fixture.w ?? 4), "\(tag) width")
expectInk(ink, fixture.color, tag)
case .circle(let center, let radius, _, let fill, let ink):
#expect(fixture.kind == "circle", "\(tag) kind")
expectClose(center, fixture.c ?? [], tag)
#expect(radius == fixture.r, "\(tag) radius")
#expect(fill == (fixture.fill ?? false), "\(tag) fill")
expectInk(ink, fixture.color, tag)
}
}
}
private func expectInk(_ ink: PropInk, _ expected: String, _ label: String) {
#expect((ink == .prop ? "prop" : "equipment") == expected, "\(label) ink")
}