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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user