Add Warm-Up and Stretching activity types

Adds WorkoutActivityType.warmUp (HealthKit .preparationAndRecovery) and
.stretching (.flexibility), and retags the six starter splits that were all
mislabeled as Functional Strength:

- Warm-Up:    Upper Body Warm-Up, Lower Body Warm-Up, Morning Wake-Up
- Stretching: Morning Mobility, Full Body Stretch, Evening Stretch

The split editor's activity picker surfaces them automatically (CaseIterable).
Older app versions decode the new raw values as the default type — additive and
not schema-gated, so no quarantine.
This commit is contained in:
2026-07-09 15:45:31 -04:00
parent f01e149269
commit e12fe31152
308 changed files with 5594 additions and 990 deletions
+42 -8
View File
@@ -118,6 +118,18 @@ def _clamp(x, lo=-1.0, hi=1.0):
return max(lo, min(hi, x))
# Axial rotation of a two-bone limb is recoverable only from the lower bone's
# lateral tip; below this magnitude the limb is effectively in-plane and its
# rotation is left at 0 (see invert_limb).
ROT_MIN_LATERAL = 0.08
# When a leg's authored knee sits within this fraction of the thigh length off the
# hip->ankle line (~sin 8.6 deg), the leg is treated as straight and its IK knee is
# bent anatomically forward rather than trusting the (unreliable) authored side
# (see solve_limb).
KNEE_STRAIGHT_FRAC = 0.15
# ---------------------------------------------------------------- the frame
# DoF names per joint type; the first is the shorthand a bare number sets.
@@ -296,13 +308,27 @@ def solve_limb(kind, attach, target, guess_mid, lengths, parent, sigma):
along = (a * a + d * d - b * b) / (2 * d)
h = math.sqrt(max(a * a - along * along, 0.0))
best = None
for sign in (1.0, -1.0):
if kind == "leg":
# A knee bends one way only. Near full extension the two knee solutions
# straddle the hip->ankle line and the authored guess (also near that line)
# cannot reliably pick a side - the bend plane's normal is a cross of two
# near-parallel vectors, so its sign is noise and the knee can flip behind
# the leg, swinging the whole thigh backward. When the authored knee sits
# within KNEE_STRAIGHT_FRAC of the line, treat the leg as straight and bend
# the knee anatomically forward (anterior); otherwise honor the authored side.
gm = vsub(guess_mid, attach)
gm_perp = vsub(gm, vscale(dir_t, vdot(gm, dir_t)))
ref = mvec(parent, (1, 0, 0)) if vlen(gm_perp) < KNEE_STRAIGHT_FRAC * a else gm_perp
sign = 1.0 if vdot(perp, ref) >= 0 else -1.0
mid = vadd(attach, vadd(vscale(dir_t, along), vscale(perp, sign * h)))
dist = vlen(vsub(mid, guess_mid))
if best is None or dist < best[0]:
best = (dist, mid)
mid = best[1]
else:
best = None
for sign in (1.0, -1.0):
mid = vadd(attach, vadd(vscale(dir_t, along), vscale(perp, sign * h)))
dist = vlen(vsub(mid, guess_mid))
if best is None or dist < best[0]:
best = (dist, mid)
mid = best[1]
end = vadd(mid, vscale(vnorm(vsub(target, mid)), b))
return invert_limb(kind, attach, mid, end, lengths, parent, sigma)
@@ -318,9 +344,17 @@ def invert_limb(kind, attach, mid, end, lengths, parent, sigma):
peel = mtrans(chain(rot_z(flex), rot_x(-sigma * abd)))
w = vnorm(mvec(peel, mvec(pt, vsub(end, mid))))
bend = math.degrees(math.acos(_clamp(-w[1])))
rot = sigma * math.degrees(math.atan2(w[2], w[0])) if bend > 0.5 else 0.0
# Axial rotation is observable only through the lower bone's lateral tip
# (w[2]); a near-sagittal limb (w[2] ~ 0) carries no recoverable rotation.
# Gating on `bend` alone is too weak: a limb can straighten through this
# degeneracy while still visibly bent, and atan2 then snaps to +/-180 on the
# sign of a near-zero anterior component - twisting the limb a half-turn and
# flipping a pinned hand/foot backward.
twist = bend > 0.5 and abs(w[2]) > ROT_MIN_LATERAL
if kind == "leg": # knees hinge backward; the sign convention flips
rot = sigma * math.degrees(math.atan2(-w[2], -w[0])) if bend > 0.5 else 0.0
rot = sigma * math.degrees(math.atan2(-w[2], -w[0])) if twist else 0.0
else:
rot = sigma * math.degrees(math.atan2(w[2], w[0])) if twist else 0.0
upper = {"flexion": flex, "abduction": abd, "rotation": rot}
return upper, {"flexion": bend}