View the figure from a slight elevation and refine the leg-machine rollers
The default camera pitches down 10 degrees, so the floor reads as a plane (drawn as a rectangle) and near/far contacts straddle it. Elevation is pure presentation - IK pins solve in the flat authored view and the posed body tilts, the same pattern as the orbit, so authored canvas targets never go out of reach. The leg-extension roller moves up onto the shin above the ankle and the leg-curl roller tucks under the heel. Fixtures and reference test values regenerated for the pitched camera. Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
+39
-18
@@ -29,6 +29,11 @@ import kinematics as K
|
||||
LIB = Path(__file__).parent
|
||||
CANVAS = (320, 180)
|
||||
GROUND_Y = 152
|
||||
# The default viewpoint is slightly elevated: the camera pitches down a
|
||||
# touch, so the floor reads as a plane (drawn as a rectangle) instead of a
|
||||
# line. Motions can override via "camera": {"pitch": ...}.
|
||||
CAMERA_PITCH = 10.0
|
||||
FLOOR_HALF_DEPTH = 30.0
|
||||
|
||||
PALETTES = {
|
||||
"default": {
|
||||
@@ -97,7 +102,7 @@ def _bucket(depth):
|
||||
return round(depth / DEPTH_BUCKET)
|
||||
|
||||
|
||||
def frame_geometry(nf, prof, cam, flipped=False):
|
||||
def frame_geometry(nf, prof, cam, flipped=False, pitch=CAMERA_PITCH):
|
||||
"""Resolve one normalized frame into drawable 2D geometry.
|
||||
|
||||
Returns (nf with IK-resolved angles and original pins, geo, order, shade):
|
||||
@@ -108,7 +113,7 @@ def frame_geometry(nf, prof, cam, flipped=False):
|
||||
offset, scaled by how side-on the view is, so overlapping limbs stay
|
||||
distinguishable in profile views.
|
||||
"""
|
||||
p0 = K.pose(nf, prof, cam)
|
||||
p0 = K.pose(nf, prof, cam, pitch)
|
||||
shade, order_parts = {}, []
|
||||
for right, left in PAIRS:
|
||||
dr, dl = _chain_depth(p0["points"][right]), _chain_depth(p0["points"][left])
|
||||
@@ -128,8 +133,12 @@ def frame_geometry(nf, prof, cam, flipped=False):
|
||||
if shade[limb] == "far" and pin in work["pins"]:
|
||||
work["pins"][pin] = [work["pins"][pin][0] - off[0],
|
||||
work["pins"][pin][1] - off[1]]
|
||||
work, p = K.resolve(work, prof, cam)
|
||||
# Pins are canvas targets in the authored, unpitched view: solve IK flat,
|
||||
# then tilt the *posed* body - the camera elevation is pure presentation,
|
||||
# so contacts straddle the floor plane instead of pins going out of reach.
|
||||
work, _ = K.resolve(work, prof, cam, 0.0)
|
||||
work["pins"] = dict(nf["pins"]) # keep authored pins; only angles resolved
|
||||
p = K.pose(work, prof, cam, pitch)
|
||||
|
||||
anchor = nf["root"]["pos"]
|
||||
|
||||
@@ -336,6 +345,15 @@ def draw_prims(d, prims, colors, scale):
|
||||
|
||||
# ------------------------------------------------------------------- drawing
|
||||
|
||||
def floor_rect_svg(colors):
|
||||
"""The floor plane under the elevated camera: a rectangle straddling the
|
||||
ground line (the old horizontal line was the pitch-zero degenerate case)."""
|
||||
fh = FLOOR_HALF_DEPTH * math.sin(math.radians(CAMERA_PITCH))
|
||||
return (f' <rect x="16" y="{GROUND_Y + 4 - fh:.1f}" width="{CANVAS[0] - 32}"'
|
||||
f' height="{2 * fh:.1f}" rx="3" fill="none"'
|
||||
f' stroke="{colors["ground"]}" stroke-width="3"/>')
|
||||
|
||||
|
||||
def part_style(part, working, colors, shade):
|
||||
"""Near pair members draw in the dark ink, far members in the light one;
|
||||
the spine is always dark. Working parts swap ink for the accent teals."""
|
||||
@@ -360,8 +378,7 @@ def svg_for_frame(name, geo, order, shade, working, colors, props=None):
|
||||
w, h = CANVAS
|
||||
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" fill="none">',
|
||||
f' <title>{name}</title>',
|
||||
f' <line x1="16" y1="{GROUND_Y + 4}" x2="{w - 16}" y2="{GROUND_Y + 4}"'
|
||||
f' stroke="{colors["ground"]}" stroke-width="3" stroke-linecap="round"/>']
|
||||
floor_rect_svg(colors)]
|
||||
parts += svg_prims(bg, colors)
|
||||
for part in order:
|
||||
if part == "head":
|
||||
@@ -414,7 +431,10 @@ def draw_geo(geo, order, shade, working, colors, scale=2, font=None, props=None)
|
||||
for x, y in (pts[0], pts[-1]):
|
||||
d.ellipse([x - r, y - r, x + r, y + r], fill=color)
|
||||
|
||||
line([(16, GROUND_Y + 4), (CANVAS[0] - 16, GROUND_Y + 4)], colors["ground"], 3)
|
||||
fh = FLOOR_HALF_DEPTH * math.sin(math.radians(CAMERA_PITCH))
|
||||
d.rounded_rectangle([16 * scale, (GROUND_Y + 4 - fh) * scale,
|
||||
(CANVAS[0] - 16) * scale, (GROUND_Y + 4 + fh) * scale],
|
||||
radius=3 * scale, outline=colors["ground"], width=3 * scale)
|
||||
draw_prims(d, bg, colors, scale)
|
||||
for part in order:
|
||||
if part == "head":
|
||||
@@ -466,6 +486,7 @@ def prepare(motion, figure="neutral", flip=False, strict=False):
|
||||
skel = K.load_skeleton()
|
||||
prof = skel["profiles"][figure]
|
||||
cam = float(motion.get("camera", {}).get("yaw", 0.0)) + (180.0 if flip else 0.0)
|
||||
pitch = float(motion.get("camera", {}).get("pitch", CAMERA_PITCH))
|
||||
norms = [K.normalize_frame(kf) for kf in motion["frames"]]
|
||||
issues = []
|
||||
for i, nf in enumerate(norms, start=1):
|
||||
@@ -481,17 +502,17 @@ def prepare(motion, figure="neutral", flip=False, strict=False):
|
||||
if flip:
|
||||
norms = [mirror_frame(nf, CANVAS[0]) for nf in norms]
|
||||
props = flip_props(props, CANVAS[0])
|
||||
return norms, prof, cam, props
|
||||
return norms, prof, cam, pitch, props
|
||||
|
||||
|
||||
def render_exercise(folder, figure="neutral", flip=False, strict=False):
|
||||
motion = load_motion(folder)
|
||||
working = set(motion.get("working", []))
|
||||
hide = set(motion.get("hide", []))
|
||||
norms, prof, cam, props = prepare(motion, figure, flip, strict)
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure, flip, strict)
|
||||
|
||||
def geometry(nf):
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam, flip)
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam, flip, pitch)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
return geo, order, shade
|
||||
@@ -499,7 +520,7 @@ def render_exercise(folder, figure="neutral", flip=False, strict=False):
|
||||
resolved = []
|
||||
key_geos = []
|
||||
for nf in norms:
|
||||
out, geo, order, shade = frame_geometry(nf, prof, cam, flip)
|
||||
out, geo, order, shade = frame_geometry(nf, prof, cam, flip, pitch)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
resolved.append(out)
|
||||
@@ -536,8 +557,8 @@ def render_orbit(folder, figure="neutral"):
|
||||
motion = load_motion(folder)
|
||||
working = set(motion.get("working", []))
|
||||
hide = set(motion.get("hide", []))
|
||||
norms, prof, cam, props = prepare(motion, figure)
|
||||
resolved = [frame_geometry(nf, prof, cam)[0] for nf in norms]
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure)
|
||||
resolved = [frame_geometry(nf, prof, cam, pitch=pitch)[0] for nf in norms]
|
||||
font = legend_font()
|
||||
colors = PALETTES["default"]
|
||||
ticks = timeline(resolved)
|
||||
@@ -546,9 +567,9 @@ def render_orbit(folder, figure="neutral"):
|
||||
yaw = cam + 360.0 * i / len(ticks)
|
||||
# Pins are canvas targets in the AUTHORED view: resolve the pose there,
|
||||
# then rotate the posed body - never re-pin in the rotated view.
|
||||
posed, _, _, _ = frame_geometry(nf, prof, cam)
|
||||
posed, _, _, _ = frame_geometry(nf, prof, cam, pitch=pitch)
|
||||
posed["pins"] = {}
|
||||
_, geo, order, shade = frame_geometry(posed, prof, yaw)
|
||||
_, geo, order, shade = frame_geometry(posed, prof, yaw, pitch=pitch)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
imgs.append(draw_geo(geo, order, shade, working, colors, font=font, props=props))
|
||||
@@ -563,9 +584,9 @@ def contact_sheet(folders, figure="neutral", out=None):
|
||||
for folder in folders:
|
||||
motion = load_motion(folder)
|
||||
working, hide = set(motion.get("working", [])), set(motion.get("hide", []))
|
||||
norms, prof, cam, props = prepare(motion, figure)
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure)
|
||||
for i, nf in enumerate(norms, start=1):
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam)
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam, pitch=pitch)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
cells.append((f"{motion['name']} {i}/{len(norms)}",
|
||||
@@ -589,8 +610,8 @@ def demo_sheet(folder):
|
||||
("themed (indigo)", "neutral", False, "indigo")]
|
||||
cells = []
|
||||
for label, figure, flip, palette in variants:
|
||||
norms, prof, cam, props = prepare(motion, figure, flip)
|
||||
_, geo, order, shade = frame_geometry(norms[idx], prof, cam, flip)
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure, flip)
|
||||
_, geo, order, shade = frame_geometry(norms[idx], prof, cam, flip, pitch)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
cells.append((f"{motion['name']} — {label}",
|
||||
|
||||
Reference in New Issue
Block a user