Give cross-body bars a true 3D axis and fix the goblet squat hold

The orbit was always a real camera orbit — figure and props share one
rigid rotation — but a bar's screen-space angle authored the wrong 3D
rod: the default horizontal encoded a rod along the body axis, so
barbells hovered fixed on screen and vanished at the head-on view
where they should span widest. Line props now take "axis": "z" (both
renderers in lockstep, fixture-pinned): the world left-right direction
projects through the camera pitch like the floor quad — end-on plates
in profile, full span face-on, swinging with the hands in between.
Applied to the ten cross-body bars; vertical handles were already
orbit-invariant.

Goblet Squat's hand pins sat so close to the shoulders that the
two-bone IK was degenerate, flipping between a chicken-wing and an
elbow-behind solve; re-pinned level with the shoulders so the elbows
tuck straight down through the whole rep.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
2026-07-08 12:49:35 -04:00
parent 60927b5d1f
commit fd2deaa9c7
79 changed files with 190 additions and 143 deletions
+21 -9
View File
@@ -340,7 +340,7 @@ def joint_points(geo, ref):
sum(p[1] for p in pts) / len(pts)), direction)
def resolve_props(props, geo, anchor, rot=K.IDENTITY, auth_geo=None):
def resolve_props(props, geo, anchor, rot=K.IDENTITY, auth_geo=None, pitch=CAMERA_PITCH):
"""Props -> drawable primitives for one frame: (background, foreground).
`geo` is the frame's drawn (possibly orbit-rotated) geometry, `auth_geo`
@@ -348,7 +348,9 @@ def resolve_props(props, geo, anchor, rot=K.IDENTITY, auth_geo=None):
frame's root canvas anchor, and `rot` the `prop_rotation` between them.
Joint positions come from `geo`; everything authored — scene points,
cable anchors, bar angles, pad perpendiculars, roller offsets — resolves
against `auth_geo` and rotates through `rot`.
against `auth_geo` and rotates through `rot`. `pitch` is the camera
elevation, needed by `axis` props whose world-space direction projects
through it (like the floor quad).
"""
auth = auth_geo if auth_geo is not None else geo
@@ -415,11 +417,21 @@ def resolve_props(props, geo, anchor, rot=K.IDENTITY, auth_geo=None):
_, d = joint_points(auth, p["at"])
if not c or not d:
continue
if t == "bar" or "angle" in p:
if p.get("axis") == "z":
# A cross-body rod (barbell, pull-up bar): its true 3D axis is
# the world left-right axis, projected through the camera
# elevation like the floor quad — a small vertical sliver
# end-on in a profile view (the plates read nearly concentric,
# seen from slightly above), full span face-on, swinging with
# the figure in between, symmetric at 0 and 180.
pr = math.radians(pitch)
ux, uy = swing((0.0, math.sin(pr)), math.cos(pr))
elif t == "bar" or "angle" in p:
axis = dirv(p.get("angle", 0)) # fixed authored-view angle
ux, uy = swing(axis) # foreshortens under orbit
else:
axis = (-d[1], d[0]) # perpendicular to the lower bone
ux, uy = swing(axis) # foreshortens under orbit
ux, uy = swing(axis)
h = p.get("halfLen", {"bar": 24, "dumbbell": 7, "pad": 8}[t])
a = (c[0] - ux * h, c[1] - uy * h)
b = (c[0] + ux * h, c[1] + uy * h)
@@ -662,7 +674,7 @@ def render_exercise(folder, figure="neutral", flip=False, strict=False):
for nf in norms:
out, geo, order, shade = frame_geometry(nf, prof, cam, flip, pitch, mat)
resolved.append(out)
prims = resolve_props(props, geo, nf["root"]["pos"])
prims = resolve_props(props, geo, nf["root"]["pos"], pitch=pitch)
geo, prims = apply_zoom(geo, prims, zoom)
key_cells.append((geo, order, shade, prims))
@@ -682,7 +694,7 @@ def render_exercise(folder, figure="neutral", flip=False, strict=False):
imgs = []
for nf in timeline(resolved):
geo, order, shade = geometry(nf)
prims = resolve_props(props, geo, nf["root"]["pos"])
prims = resolve_props(props, geo, nf["root"]["pos"], pitch=pitch)
geo, prims = apply_zoom(geo, prims, zoom)
imgs.append(draw_geo(geo, order, shade, working, colors, font=font, prims=prims))
imgs[0].save(folder / "preview.gif", save_all=True, append_images=imgs[1:],
@@ -716,7 +728,7 @@ def render_orbit(folder, figure="neutral"):
posed["pins"] = {}
_, geo, order, shade = frame_geometry(posed, prof, cam + off, pitch=pitch, mat=mat)
prims = resolve_props(props, geo, nf["root"]["pos"],
prop_rotation(pitch, off), auth_geo)
prop_rotation(pitch, off), auth_geo, pitch=pitch)
geo, prims = apply_zoom(geo, prims, zoom)
imgs.append(draw_geo(geo, order, shade, working, colors, font=font, prims=prims))
imgs[0].save(folder / "orbit.gif", save_all=True, append_images=imgs[1:],
@@ -734,7 +746,7 @@ def contact_sheet(folders, figure="neutral", out=None):
mat = mat_bounds(norms, prof, cam, pitch)
for i, nf in enumerate(norms, start=1):
_, geo, order, shade = frame_geometry(nf, prof, cam, pitch=pitch, mat=mat)
prims = resolve_props(props, geo, nf["root"]["pos"])
prims = resolve_props(props, geo, nf["root"]["pos"], pitch=pitch)
geo, prims = apply_zoom(geo, prims, zoom)
cells.append((f"{motion['name']} {i}/{len(norms)}",
draw_geo(geo, order, shade, working, PALETTES["default"],
@@ -760,7 +772,7 @@ def demo_sheet(folder):
norms, prof, cam, pitch, props, zoom = prepare(motion, figure, flip)
mat = mat_bounds(norms, prof, cam, pitch)
_, geo, order, shade = frame_geometry(norms[idx], prof, cam, flip, pitch, mat)
prims = resolve_props(props, geo, norms[idx]["root"]["pos"])
prims = resolve_props(props, geo, norms[idx]["root"]["pos"], pitch=pitch)
geo, prims = apply_zoom(geo, prims, zoom)
cells.append((f"{motion['name']}{label}",
draw_geo(geo, order, shade, working, PALETTES[palette],