Surface machine settings in the exercise library and refine machine rigs

Library detail screens now lead with the user's recorded machine settings
(per-split when they disagree, empty-state card for machine-based entries)
and append the weight progression chart. Starter seeds mark machine
exercises with an empty machineSettings list so the settings UI lights up
before first use. The figure rig gains a frontal body profile for face-on
machines, props that can ride mid joints (knees/elbows), and an
alternating four-frame Bird Dog loop.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
2026-07-06 18:35:15 -04:00
parent ff1ffbb1f6
commit 669ecf1259
76 changed files with 733 additions and 268 deletions
+20 -8
View File
@@ -57,7 +57,14 @@ LIMBS = { # limb -> (attach joint, [bone length keys], pin key)
ANGLE_KEYS = ["spine", "neck", "gaze", "arm_r", "arm_l", "leg_r", "leg_l"]
JOINT_LIMB = {"hand_r": "arm_r", "hand_l": "arm_l", "foot_r": "leg_r", "foot_l": "leg_l"}
# Prop joint refs -> (limb, chain index): extremities are index 2, mid joints
# (elbows/knees) index 1, so equipment can ride either joint.
JOINT_LIMB = {
"hand_r": ("arm_r", 2), "elbow_r": ("arm_r", 1),
"hand_l": ("arm_l", 2), "elbow_l": ("arm_l", 1),
"foot_r": ("leg_r", 2), "knee_r": ("leg_r", 1),
"foot_l": ("leg_l", 2), "knee_l": ("leg_l", 1),
}
def bodies():
@@ -244,17 +251,19 @@ def flip_props(props, width):
def joint_points(geo, ref):
"""Resolve a joint ref — `"hand_r"` or a `["hand_r", "hand_l"]` midpoint
to (point, lower-bone unit direction). None when the limb isn't drawn."""
"""Resolve a joint ref — `"hand_r"`, `"knee_l"`, or a midpoint list like
`["knee_r", "foot_r"]` — to (point, unit direction of the bone ending at
the first joint). None when the limb isn't drawn."""
names = ref if isinstance(ref, list) else [ref]
pts, direction = [], None
for name in names:
limb = geo.get(JOINT_LIMB[name])
limb_name, idx = JOINT_LIMB[name]
limb = geo.get(limb_name)
if not limb:
return None, None
pts.append(limb[-1])
pts.append(limb[idx])
if direction is None:
a, b = limb[-2], limb[-1]
a, b = limb[idx - 1], limb[idx]
d = math.hypot(b[0] - a[0], b[1] - a[1]) or 1.0
direction = ((b[0] - a[0]) / d, (b[1] - a[1]) / d)
return ((sum(p[0] for p in pts) / len(pts),
@@ -487,7 +496,9 @@ def export_app_resources(folders):
def render_exercise(folder, figure="neutral", flip=False):
motion = load_motion(folder)
body = bodies()[figure]
# A motion may pin its own profile (e.g. "frontal": foreshortened legs for
# face-on seated machines); it wins over the CLI default.
body = bodies()[motion.get("figure", figure)]
working = set(motion.get("working", []))
hide = set(motion.get("hide", []))
props = motion.get("props", [])
@@ -521,10 +532,11 @@ def contact_sheet(folders, figure="neutral", out=None):
from PIL import Image, ImageDraw
font = legend_font()
body = bodies()[figure]
profiles = bodies()
cells = []
for folder in folders:
motion = load_motion(folder)
body = profiles[motion.get("figure", figure)]
working, hide = set(motion.get("working", [])), set(motion.get("hide", []))
props = motion.get("props", [])
for i, kf in enumerate(motion["frames"], start=1):