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
@@ -109,10 +109,13 @@ struct ExerciseFigureView: View {
let geo = figure.geometry(at: time)
// Ground line.
stroke(&ctx, [CGPoint(x: 16, y: Self.groundY + 4),
CGPoint(x: Self.designSize.width - 16, y: Self.groundY + 4)],
color: .figureGround, width: 3)
// The floor plane under the elevated camera: a rectangle straddling the
// ground line (a horizontal line is the pitch-zero degenerate case).
let floorHalf = MotionSolver.floorHalfDepth * sin(figure.timeline.pitch * .pi / 180)
let floor = CGRect(x: 16, y: Self.groundY + 4 - floorHalf,
width: Self.designSize.width - 32, height: 2 * floorHalf)
ctx.stroke(Path(roundedRect: floor, cornerRadius: 3), with: .color(.figureGround),
style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
// Equipment behind the figure: scene shapes and cables.
drawBackgroundProps(&ctx, geo)
@@ -104,6 +104,8 @@ struct RootValue: Codable {
/// The orthographic camera: `yaw` 0 is the classic side view, 90 face-on.
struct MotionCamera: Codable {
let yaw: Double?
/// Camera elevation override; nil uses the standard slightly-raised viewpoint.
let pitch: Double?
}
/// A prop's joint reference: one joint (`"hand_r"`, `"knee_l"`, `"elbow_r"`, )
+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
}
}