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:
@@ -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")
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user