Author the machine exercise circuit with props and corrected hip machine motions

Fourteen Planet Fitness machine exercises join the rig library, each with
authored motion, info page, and schematic equipment via the new props layer
(scene shapes, cables, bars, pads) rendered in lockstep by render.py and the
in-app figure renderer. Abductor and Adductor are authored face-on with the
real seated machine motion — knees-bent legs swinging apart/together against
knee pads — replacing the earlier middle-split depiction. The watch target
now bundles the figure renderer and motion rigs too.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
2026-07-06 16:29:31 -04:00
parent 888852cc2e
commit fce8fa4c17
130 changed files with 4116 additions and 82 deletions
+17 -11
View File
@@ -60,7 +60,8 @@ struct FigurePose {
var root: CGPoint
var spine: [Double]
var neck: Double
var gaze: Double
/// Absent for figures facing the viewer no nose tick.
var gaze: Double?
var limbs: [FigureLimb: [Double]]
var pins: [String: CGPoint]
var hold: Double
@@ -89,8 +90,9 @@ struct FigureAttachments {
struct FigureGeometry {
var headCenter: CGPoint
var headRadius: Double
var noseStart: CGPoint
var noseEnd: CGPoint
/// Nil when the pose has no gaze (face-on view).
var noseStart: CGPoint?
var noseEnd: CGPoint?
/// Quadratic Bézier through pelvis mid neck (control = 2·mid (pelvis+neck)/2).
var spineStart: CGPoint
var spineControl: CGPoint
@@ -213,7 +215,7 @@ enum MotionSolver {
y: a.root.y + (b.root.y - a.root.y) * t),
spine: zip(a.spine, b.spine).map { lerpAngle($0, $1, t) },
neck: lerpAngle(a.neck, b.neck, t),
gaze: lerpAngle(a.gaze, b.gaze, t),
gaze: (a.gaze != nil && b.gaze != nil) ? lerpAngle(a.gaze!, b.gaze!, t) : nil,
limbs: limbs, pins: pins, hold: a.hold, tween: a.tween
)
}
@@ -223,12 +225,16 @@ enum MotionSolver {
static func geometry(of pose: FigurePose, body: ExerciseBodyProfile, hide: Set<String>) -> FigureGeometry {
let at = attachments(root: pose.root, spine: pose.spine, body: body)
let head = walk(from: at.neck, angles: [pose.neck], lengths: [body.neck])[1]
let noseTip = walk(from: head, angles: [pose.gaze], lengths: [body.headR + 7])[1]
// Nose tick: 7pt outward from the head rim along the gaze direction.
let d = max(hypot(noseTip.x - head.x, noseTip.y - head.y), 1)
let ux = (noseTip.x - head.x) / d
let uy = (noseTip.y - head.y) / d
let r = body.headR
// Nose tick: 7pt outward from the head rim along the gaze direction
// (omitted for face-on poses without a gaze).
var noseStart: CGPoint?
var noseEnd: CGPoint?
if let gaze = pose.gaze {
let d = direction(gaze)
noseStart = CGPoint(x: head.x + d.dx * r, y: head.y + d.dy * r)
noseEnd = CGPoint(x: head.x + d.dx * (r + 7), y: head.y + d.dy * (r + 7))
}
var limbs: [FigureLimb: [CGPoint]] = [:]
for (limb, authored) in pose.limbs where !hide.contains(limb.rawValue) {
@@ -244,8 +250,8 @@ enum MotionSolver {
return FigureGeometry(
headCenter: head,
headRadius: r,
noseStart: CGPoint(x: head.x + ux * r, y: head.y + uy * r),
noseEnd: CGPoint(x: head.x + ux * (r + 7), y: head.y + uy * (r + 7)),
noseStart: noseStart,
noseEnd: noseEnd,
spineStart: at.pelvis,
spineControl: CGPoint(x: 2 * at.mid.x - (at.pelvis.x + at.neck.x) / 2,
y: 2 * at.mid.y - (at.pelvis.y + at.neck.y) / 2),