View the figure from a slight elevation and refine the leg-machine rollers

The default camera pitches down 10 degrees, so the floor reads as a
plane (drawn as a rectangle) and near/far contacts straddle it.
Elevation is pure presentation - IK pins solve in the flat authored
view and the posed body tilts, the same pattern as the orbit, so
authored canvas targets never go out of reach. The leg-extension
roller moves up onto the shin above the ankle and the leg-curl roller
tucks under the heel. Fixtures and reference test values regenerated
for the pitched camera.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
2026-07-06 21:20:07 -04:00
parent 5e4980f0d7
commit b82054b81a
108 changed files with 838 additions and 778 deletions
+28 -9
View File
@@ -279,9 +279,12 @@ enum MotionSolver {
return [attach, knee, ankleJoint, toe]
}
/// FK a normalized frame into view space through the camera yaw.
static func pose(_ nf: NormalizedFrame, prof: SkeletonProfile, cam: Double) -> FigurePose {
let fRoot = chain(Mat3.rotY(-cam), Mat3.rotY(nf.yaw), Mat3.rotZ(-nf.pitch), Mat3.rotX(nf.roll))
/// FK a normalized frame into view space through the camera yaw; `camPitch`
/// tilts the viewpoint down from slightly above (the scene rotates about the root).
static func pose(_ nf: NormalizedFrame, prof: SkeletonProfile, cam: Double,
camPitch: Double = 0) -> FigurePose {
let fRoot = chain(Mat3.rotX(camPitch), Mat3.rotY(-cam),
Mat3.rotY(nf.yaw), Mat3.rotZ(-nf.pitch), Mat3.rotX(nf.roll))
let origin = Vec3(0, 0, 0)
let s1 = nf.spine[0], s2 = nf.spine[1]
let f1 = chain(fRoot, Mat3.rotZ(-s1.flexion), Mat3.rotX(s1.lateral), Mat3.rotY(-s1.rotation))
@@ -397,8 +400,15 @@ enum MotionSolver {
/// restored afterward), the depth-sorted draw order (head last), the spine curve,
/// and the foreshortened gaze nose tick (hidden when the face points at/away from
/// the camera).
static func frameGeometry(_ nf: NormalizedFrame, prof: SkeletonProfile, cam: Double) -> (NormalizedFrame, FigureGeometry) {
let p0 = pose(nf, prof: prof, cam: cam)
/// The standard slightly-elevated viewpoint: the camera pitches down a touch so
/// the floor reads as a plane. Kept 1:1 with the reference renderer's CAMERA_PITCH.
static let defaultPitch: Double = 10
/// Floor plane half-depth; its projected height is `floorHalfDepth · sin(pitch)`.
static let floorHalfDepth: Double = 30
static func frameGeometry(_ nf: NormalizedFrame, prof: SkeletonProfile, cam: Double,
pitch: Double = MotionSolver.defaultPitch) -> (NormalizedFrame, FigureGeometry) {
let p0 = pose(nf, prof: prof, cam: cam, camPitch: pitch)
var shade: [FigureLimb: Shade] = [:]
for (right, left) in pairs {
guard let rp = p0.limbs[right], let lp = p0.limbs[left] else { continue }
@@ -421,8 +431,14 @@ enum MotionSolver {
work.pins[limb.pinKey] = CGPoint(x: pin.x - off.x, y: pin.y - off.y)
}
}
// Pins are canvas targets in the authored, unpitched view: solve IK flat,
// then tilt the *posed* body - the camera elevation is pure presentation,
// so contacts straddle the floor plane instead of pins going out of reach.
var (resolved, p) = resolve(work, prof: prof, cam: cam)
resolved.pins = nf.pins // keep authored pins; only angles resolved
if pitch != 0 {
p = pose(resolved, prof: prof, cam: cam, camPitch: pitch)
}
let anchor = nf.rootPos
func scr(_ v: Vec3, _ limbOffset: CGPoint = .zero) -> CGPoint {
@@ -476,18 +492,21 @@ struct MotionTimeline {
let resolved: [NormalizedFrame]
let profile: SkeletonProfile
let cam: Double
let pitch: Double
let duration: Double
init?(motion: ExerciseMotion, profile: SkeletonProfile) {
let cam = motion.camera?.yaw ?? 0
let pitch = motion.camera?.pitch ?? MotionSolver.defaultPitch
let norms = motion.frames.map { MotionSolver.normalize($0) }
guard !norms.isEmpty else { return nil }
let resolved = norms.map { MotionSolver.frameGeometry($0, prof: profile, cam: cam).0 }
let resolved = norms.map { MotionSolver.frameGeometry($0, prof: profile, cam: cam, pitch: pitch).0 }
let duration = resolved.reduce(0) { $0 + $1.hold + $1.tween }
guard duration > 0 else { return nil }
self.resolved = resolved
self.profile = profile
self.cam = cam
self.pitch = pitch
self.duration = duration
}
@@ -516,10 +535,10 @@ struct MotionTimeline {
func geometry(at time: Double, yawOffset: Double = 0) -> FigureGeometry {
let frame = frame(at: time)
guard yawOffset != 0 else {
return MotionSolver.frameGeometry(frame, prof: profile, cam: cam).1
return MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch).1
}
var posed = MotionSolver.frameGeometry(frame, prof: profile, cam: cam).0
var posed = MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch).0
posed.pins = [:]
return MotionSolver.frameGeometry(posed, prof: profile, cam: cam + yawOffset).1
return MotionSolver.frameGeometry(posed, prof: profile, cam: cam + yawOffset, pitch: pitch).1
}
}