Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions src/meshops/proportion/blockout_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
0039: opt-in --join-ready socket overlaps (shoulder/hip/neck/ankle); mutually exclusive with
--nofuse; setup re-emit via run_blockout_emit_setup; schema stay 1.4.0 + join_ready bool.
0047: torso oval ry depth taper (anti-snowman chest/waist/hip); schema stay 1.4.0.
0065: torso front snowman — waist rx pinch + chest ry/front flatten (full3d rear bias);
schema stay 1.4.0.
0049: breast_soft center Z hang drop (before 0033 tilt); B1 floor 0.55*rz; schema stay 1.4.0.
0050: neck column forward tilt (p0/p1) + head/face co-move + radius ceiling vs head.rx;
schema stay 1.4.0.
Expand Down Expand Up @@ -97,12 +99,15 @@
# 0046 B9: template thigh_tilt adduction (medial-shift cap + knee-cluster co-move).
THIGH_TILT_DEG_CAP: Final[float] = 15.0
THIGH_ADDUCTION_MAX_MEDIAL_M: Final[float] = 0.030
# 0047 B1: torso oval anteroposterior depth taper (anti-snowman); keep 0040 rz law.
TORSO_OVAL_RY_CHEST_FRAC: Final[float] = 0.95
TORSO_OVAL_RY_WAIST_FRAC: Final[float] = 0.72
# 0047 B1 + 0065: torso oval depth + front pinch freezes
TORSO_OVAL_RY_CHEST_FRAC: Final[float] = 0.85 # was 0.95
TORSO_OVAL_RY_WAIST_FRAC: Final[float] = 0.58 # was 0.72
TORSO_OVAL_RY_HIP_FRAC: Final[float] = 0.80 # ≠ PELVIS_OVAL_RY_FRAC_HALF_HIP (0.60)
TORSO_OVAL_RZ_SPAN_FRAC: Final[float] = 0.22
TORSO_OVAL_RZ_SPAN_FRAC: Final[float] = 0.22 # fence 0040
TORSO_OVAL_RZ_FLOOR_M: Final[float] = 0.025
TORSO_WAIST_RX_MAX_FRAC_CHEST: Final[float] = 0.80
TORSO_WAIST_PINCH_TAPER_GATE: Final[float] = 0.10
TORSO_CHEST_Y_REAR_BIAS_FRAC_RY: Final[float] = 0.28
# 0049 B1: breast_soft vertical hang floor (center Z drop as fraction of rz).
BREAST_HANG_Z_DROP_FRAC_RZ: Final[float] = 0.55
# 0049 D2: unit min hang drop vs pre-anchor (softer than B1; waist soft-clamp threshold).
Expand Down Expand Up @@ -1668,21 +1673,43 @@ def _build_torso_ovals(
("RECIPE_torso_oval_waist", 0.50),
("RECIPE_torso_oval_hip", 0.85),
]
# 0065 two-pass: precompute post-taper rx, then B1 waist cap vs post-taper chest.
rx_by: dict[str, float] = {
name: _waist_width_at(z_norm, w_s, w_h, taper) for name, z_norm in layers
}
chest_key = "RECIPE_torso_oval_chest"
waist_key = "RECIPE_torso_oval_waist"
if taper >= TORSO_WAIST_PINCH_TAPER_GATE:
rx_by[waist_key] = min(
rx_by[waist_key],
TORSO_WAIST_RX_MAX_FRAC_CHEST * rx_by[chest_key],
)

ry_chest: float | None = None
ry_waist: float | None = None
ry_hip: float | None = None
chest_cy: float | None = None
rx_chest_emit: float | None = None
rx_waist_emit: float | None = None
for name, z_norm in layers:
z_m = z_top - z_norm * span
hw = _waist_width_at(z_norm, w_s, w_h, taper)
hw = rx_by[name]
# Vertical radius must exceed half layer spacing (0.35*span/2) so chest/waist/hip
# ovals overlap. 0.12 left a belly gap; 0.18 barely kissed; 0.22 ≈ 4cm overlap.
rz = max(TORSO_OVAL_RZ_FLOOR_M, span * TORSO_OVAL_RZ_SPAN_FRAC)
center_y = y
if name.endswith("_chest"):
ry = half_chest * TORSO_OVAL_RY_CHEST_FRAC
ry_chest = ry
# B5: rear bias only when full3d (chest_y known); front_plane stays mid.
if placement == "full3d":
center_y = y + TORSO_CHEST_Y_REAR_BIAS_FRAC_RY * ry
chest_cy = center_y
rx_chest_emit = hw
elif name.endswith("_waist"):
ry = half_chest * TORSO_OVAL_RY_WAIST_FRAC
ry_waist = ry
rx_waist_emit = hw
else: # hip
ry = half_hip * TORSO_OVAL_RY_HIP_FRAC
ry_hip = ry
Expand All @@ -1691,7 +1718,7 @@ def _build_torso_ovals(
name=name,
role="torso",
kind="ellipsoid",
center=[0.0, y, z_m],
center=[0.0, center_y, z_m],
rx_m=hw,
ry_m=ry,
rz_m=rz,
Expand All @@ -1704,6 +1731,21 @@ def _build_torso_ovals(
"torso depth taper: chest/waist/hip "
f"ry={ry_chest:.4f}/{ry_waist:.4f}/{ry_hip:.4f} (anti-snowman)"
)
# 0065 B12: front pinch inventory (waist/chest rx + chest front/rear poles).
if (
rx_chest_emit is not None
and rx_waist_emit is not None
and chest_cy is not None
and ry_chest is not None
):
frac = rx_waist_emit / rx_chest_emit if abs(rx_chest_emit) > 1e-12 else float("nan")
front_y = chest_cy - ry_chest
rear_y = chest_cy + ry_chest
messages.append(
"torso front pinch: "
f"waist_rx/chest_rx={rx_waist_emit:.4f}/{rx_chest_emit:.4f} "
f"({frac:.3f}) chest_front_y={front_y:.4f} chest_rear_y={rear_y:.4f}"
)

# 0053 pelvis shelf freezes (was B10: ry = hip_half * 0.85)
if m.height_m is not None:
Expand Down Expand Up @@ -4805,6 +4847,14 @@ def run_blockout_emit_setup(
"THIGH_ADDUCTION_MAX_MEDIAL_M",
"THIGH_PROX_SOFT_SCALE",
"THIGH_TILT_DEG_CAP",
"TORSO_CHEST_Y_REAR_BIAS_FRAC_RY",
"TORSO_OVAL_RY_CHEST_FRAC",
"TORSO_OVAL_RY_HIP_FRAC",
"TORSO_OVAL_RY_WAIST_FRAC",
"TORSO_OVAL_RZ_FLOOR_M",
"TORSO_OVAL_RZ_SPAN_FRAC",
"TORSO_WAIST_PINCH_TAPER_GATE",
"TORSO_WAIST_RX_MAX_FRAC_CHEST",
"_BASELINE_ROLES_NO_PROFILE",
"_MICHELIN_FRAC",
"BlockoutRecipePackage",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"cleft_frac": 0.12
},
"pelvis_y_m": 0.027,
"torso_waist_taper": 0.14,
"torso_waist_taper": 0.22,
"thigh_tilt_deg": 10.0,
"neck_thickness_scale": 0.72675,
"neck_thickness_notes": {
Expand Down
14 changes: 12 additions & 2 deletions tests/test_proportion_blockout_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -2103,8 +2103,12 @@ def test_recipe__axial_chest_y_prefers_mid_not_front() -> None:
"""0032 pin: shoulders y null + chest_front=-0.13 + mid=0 → axial Y≈0, not front.

0050: neck tip leans -Y by L*sin(tilt); base p0 stays mid.
0065: chest oval alone gets full3d rear bias; waist/hip stay mid.
"""
from meshops.proportion.blockout_recipe import NECK_FORWARD_TILT_DEG
from meshops.proportion.blockout_recipe import (
NECK_FORWARD_TILT_DEG,
TORSO_CHEST_Y_REAR_BIAS_FRAC_RY,
)

report = _axial_pin_report(chest_front_y=-0.13, chest_mid_y=0.0, shoulder_y=None)
pkg = build_blockout_recipe(report, limbs=False, torso="ovals")
Expand All @@ -2118,7 +2122,13 @@ def test_recipe__axial_chest_y_prefers_mid_not_front() -> None:
assert ovals
for o in ovals:
assert o.center is not None
assert o.center[1] == pytest.approx(0.0, abs=1e-6)
if o.name == "RECIPE_torso_oval_chest":
# 0065 B5: full3d chest rear bias (mid=0 → cy = bias * ry)
ry = float(o.ry_m or 0.0)
expected_cy = 0.0 + TORSO_CHEST_Y_REAR_BIAS_FRAC_RY * ry
assert o.center[1] == pytest.approx(expected_cy, abs=1e-6)
else:
assert o.center[1] == pytest.approx(0.0, abs=1e-6)
assert o.center[1] != pytest.approx(-0.13, abs=1e-3)
bridges = [p for p in pkg.parts if p.role == "shoulder_bridge"]
assert bridges
Expand Down
2 changes: 1 addition & 1 deletion tests/test_proportion_body_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def test_female_seeds__neck_and_breast() -> None:
assert doc.breast.rz_scale == pytest.approx(2.1)
assert doc.neck_thickness_scale == pytest.approx(0.72675)
assert doc.neck_thickness_notes.stages == [0.95, 0.9, 0.85]
assert doc.torso_waist_taper == pytest.approx(0.14)
assert doc.torso_waist_taper == pytest.approx(0.22)
assert doc.thigh_tilt_deg == pytest.approx(10.0)
assert doc.glute.y_frac is not None
assert doc.glute.y_frac > 0
Expand Down
6 changes: 3 additions & 3 deletions tests/test_proportion_pelvis_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,9 @@ def test_t8_0052_glute_seat_constants_still_exist() -> None:


def test_t9_torso_oval_fracs_unchanged_0047() -> None:
"""T9: torso chest/waist/hip ry fracs unchanged (0047 magnitudes)."""
assert TORSO_OVAL_RY_CHEST_FRAC == 0.95
assert TORSO_OVAL_RY_WAIST_FRAC == 0.72
"""T9: torso chest/waist/hip ry fracs (0047 + 0065 retarget 0.85/0.58/0.80)."""
assert TORSO_OVAL_RY_CHEST_FRAC == 0.85
assert TORSO_OVAL_RY_WAIST_FRAC == 0.58
assert TORSO_OVAL_RY_HIP_FRAC == 0.80
report = _full_torso_report()
pkg = build_blockout_recipe(report, limbs=False, torso="ovals")
Expand Down
Loading