From 0680e26f26f6df98413bb55b9079566e875dbd43 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 9 Aug 2026 08:38:59 -0400 Subject: [PATCH] feat(proportion): torso snowman front waist pinch + chest flatten (0065) Stronger front hourglass and flatter chest plate on oval torso recipe: waist rx capped at 0.80x post-taper chest when taper>=0.10, female torso_waist_taper 0.22, waist/chest ry fracs 0.58/0.85, full3d chest rear bias 0.28x ry. Pin retargets and dedicated T0-T13 suite. --- src/meshops/proportion/blockout_recipe.py | 62 ++- .../body_templates/female_adult_athletic.json | 2 +- tests/test_proportion_blockout_recipe.py | 14 +- tests/test_proportion_body_template.py | 2 +- tests/test_proportion_pelvis_bucket.py | 6 +- tests/test_proportion_torso_snowman_front.py | 506 ++++++++++++++++++ 6 files changed, 579 insertions(+), 13 deletions(-) create mode 100644 tests/test_proportion_torso_snowman_front.py diff --git a/src/meshops/proportion/blockout_recipe.py b/src/meshops/proportion/blockout_recipe.py index 1d122d7..14edafb 100644 --- a/src/meshops/proportion/blockout_recipe.py +++ b/src/meshops/proportion/blockout_recipe.py @@ -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. @@ -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). @@ -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 @@ -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, @@ -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: @@ -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", diff --git a/src/meshops/proportion/body_templates/female_adult_athletic.json b/src/meshops/proportion/body_templates/female_adult_athletic.json index 09026da..5c32ecc 100644 --- a/src/meshops/proportion/body_templates/female_adult_athletic.json +++ b/src/meshops/proportion/body_templates/female_adult_athletic.json @@ -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": { diff --git a/tests/test_proportion_blockout_recipe.py b/tests/test_proportion_blockout_recipe.py index 12c72dc..4b6307c 100644 --- a/tests/test_proportion_blockout_recipe.py +++ b/tests/test_proportion_blockout_recipe.py @@ -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") @@ -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 diff --git a/tests/test_proportion_body_template.py b/tests/test_proportion_body_template.py index 746a31e..ed62f4f 100644 --- a/tests/test_proportion_body_template.py +++ b/tests/test_proportion_body_template.py @@ -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 diff --git a/tests/test_proportion_pelvis_bucket.py b/tests/test_proportion_pelvis_bucket.py index 2bae4b6..5d09f84 100644 --- a/tests/test_proportion_pelvis_bucket.py +++ b/tests/test_proportion_pelvis_bucket.py @@ -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") diff --git a/tests/test_proportion_torso_snowman_front.py b/tests/test_proportion_torso_snowman_front.py new file mode 100644 index 0000000..7d0ded1 --- /dev/null +++ b/tests/test_proportion_torso_snowman_front.py @@ -0,0 +1,506 @@ +"""Track 0065 - torso snowman front (waist rx pinch + chest flatten; authoring only).""" + +from __future__ import annotations + +import math + +import pytest + +from meshops.proportion.blockout_recipe import ( + 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, + _build_torso_ovals, + _ResolvedMetrics, + _waist_width_at, + build_blockout_recipe, +) +from meshops.proportion.body_template import ( + AppliedConstants, + TemplateAppliedPackage, + load_body_template, +) +from meshops.proportion.connection_metrics import connection_gap_metrics +from meshops.proportion.constraints import validate_constraints +from meshops.proportion.models import ( + CrossSection, + DepthBand, + DiameterMeasure, + LandmarkXYZ, + ProportionReport, + QualityFlags, +) + + +def _lm( + id_: str, + *, + x_m: float | None = None, + y_m: float | None = None, + z_m: float | None = None, +) -> LandmarkXYZ: + return LandmarkXYZ(id=id_, x_m=x_m, y_m=y_m, z_m=z_m) + + +def _diam(band_id: str, *, half_width_m: float = 0.05) -> DiameterMeasure: + return DiameterMeasure( + band_id=band_id, + view="front", + width_px=40.0, + width_eucl_px=40.0, + theta_deg=90.0, + width_frac=0.1, + width_m=half_width_m * 2.0, + half_width_m=half_width_m, + mid_x_px=100.0, + mid_y_px=200.0, + ) + + +def _band( + band_id: str, + *, + depth_m: float = 0.24, + z_frac: float = 0.72, + y_mid: float = 0.0, +) -> DepthBand: + return DepthBand( + band_id=band_id, + depth_px=50.0, + depth_frac=0.12, + depth_m=depth_m, + y_front=0.1, + y_back=-0.1, + y_mid=y_mid, + z_frac=z_frac, + ) + + +def _full_torso_report( + *, + height_m: float = 1.72, + shoulder_x: float = 0.20, + hip_x: float = 0.14, + hip_z: float = 0.95, + shoulder_z: float = 1.38, + chest_depth_m: float = 0.24, + hip_depth_m: float = 0.26, + with_soft_cs: bool = False, + chest_mid_y: float | None = None, +) -> ProportionReport: + """Minimal report so torso ovals emit (shoulder/hip hw + chest/hip depth).""" + lms: dict[str, LandmarkXYZ] = { + "sole": _lm("sole", x_m=0.0, y_m=0.0, z_m=0.0), + "chin": _lm("chin", x_m=0.0, y_m=-0.02, z_m=1.50), + "shoulder_l": _lm("shoulder_l", x_m=-shoulder_x, y_m=0.0, z_m=shoulder_z), + "shoulder_r": _lm("shoulder_r", x_m=shoulder_x, y_m=0.0, z_m=shoulder_z), + "hip_l": _lm("hip_l", x_m=-hip_x, y_m=0.0, z_m=hip_z), + "hip_r": _lm("hip_r", x_m=hip_x, y_m=0.0, z_m=hip_z), + "cranial_vertex": _lm("cranial_vertex", x_m=0.0, y_m=-0.01, z_m=1.68), + "crotch_pubic": _lm("crotch_pubic", x_m=0.0, y_m=0.0, z_m=0.86), + "chest_front": _lm("chest_front", x_m=0.0, y_m=-0.05, z_m=1.25), + } + if chest_mid_y is not None: + lms["chest_mid"] = _lm("chest_mid", x_m=0.0, y_m=chest_mid_y, z_m=1.25) + diams = [ + _diam("bust", half_width_m=0.16), + _diam("waist", half_width_m=0.13), + _diam("neck", half_width_m=0.05), + ] + bands = [ + _band("chest", depth_m=chest_depth_m, z_frac=0.72, y_mid=0.0), + _band("hip", depth_m=hip_depth_m, z_frac=0.55), + ] + cs: list[CrossSection] = [] + if with_soft_cs: + cs = [ + CrossSection( + level_id="bust", + z_frac=0.72, + rx_frac=0.10, + ry_frac=0.08, + sources=["test"], + ), + CrossSection( + level_id="glute", + z_frac=0.50, + rx_frac=0.11, + ry_frac=0.09, + sources=["test"], + ), + ] + return ProportionReport( + schema_version="1.1.0", + height_m=height_m, + head_unit_frac=1.0 / 7.5, + landmarks_xyz=lms, + diameters=diams, + depth_bands=bands, + cross_sections=cs, + quality=QualityFlags(), + ) + + +def _template( + *, + taper: float, + template_id: str = "female_adult_athletic", + sex: str = "female", +) -> TemplateAppliedPackage: + constants = AppliedConstants( + breast_mode="dual_tilted" if sex == "female" else "none", + glute_mode_default="two_spheres" if sex == "female" else "oval", + torso_mode_default="ovals", + torso_waist_taper=taper, + ) + return TemplateAppliedPackage( + template_id=template_id, + sex=sex, # type: ignore[arg-type] + archetype="adult_athletic", + source_report="mem", + height_m=1.72, + constants=constants, + ) + + +def _metrics_for_ovals( + *, + chest_y: float | None = 0.0, + shoulder_hw: float = 0.20, + hip_hw: float = 0.14, + half_chest: float = 0.12, + half_hip: float = 0.13, + shoulder_z: float = 1.38, + hip_z: float = 0.95, + height_m: float = 1.72, +) -> _ResolvedMetrics: + m = _ResolvedMetrics() + m.shoulder_hw = shoulder_hw + m.hip_hw = hip_hw + m.chest_half_depth = half_chest + m.hip_half_depth = half_hip + m.shoulder_z = shoulder_z + m.hip_z = hip_z + m.chest_y = chest_y + m.height_m = height_m + return m + + +# --------------------------------------------------------------------------- +# T0-T13 +# --------------------------------------------------------------------------- + + +def test_t0_public_freezes_exported_in_bands() -> None: + """T0: public freezes exported; values within plan open bands.""" + assert 0.76 <= TORSO_WAIST_RX_MAX_FRAC_CHEST <= 0.84 + assert 0.08 <= TORSO_WAIST_PINCH_TAPER_GATE <= 0.12 + assert 0.52 <= TORSO_OVAL_RY_WAIST_FRAC <= 0.64 + assert 0.80 <= TORSO_OVAL_RY_CHEST_FRAC <= 0.90 + assert 0.20 <= TORSO_CHEST_Y_REAR_BIAS_FRAC_RY <= 0.35 + assert TORSO_OVAL_RY_HIP_FRAC == 0.80 + assert TORSO_OVAL_RZ_SPAN_FRAC == 0.22 + assert TORSO_OVAL_RZ_FLOOR_M == 0.025 + # exact freeze defaults + assert TORSO_WAIST_RX_MAX_FRAC_CHEST == 0.80 + assert TORSO_WAIST_PINCH_TAPER_GATE == 0.10 + assert TORSO_OVAL_RY_CHEST_FRAC == 0.85 + assert TORSO_OVAL_RY_WAIST_FRAC == 0.58 + assert TORSO_CHEST_Y_REAR_BIAS_FRAC_RY == 0.28 + + +def test_t1_taper_ge_gate_waist_rx_capped() -> None: + """T1: taper >= 0.10 -> hard max *binds* (raw > 0.80x chest, emit = cap).""" + # Geometry where sin-pinch alone leaves waist > 0.80x post-taper chest. + report = _full_torso_report(shoulder_x=0.25, hip_x=0.24) + tpl = _template(taper=0.14) + pkg = build_blockout_recipe(report, limbs=False, torso="ovals", template_applied=tpl) + by = {p.name: p for p in pkg.parts} + rx_c = float(by["RECIPE_torso_oval_chest"].rx_m or 0.0) + rx_w = float(by["RECIPE_torso_oval_waist"].rx_m or 0.0) + raw_c = _waist_width_at(0.15, 0.25, 0.24, 0.14) + raw_w = _waist_width_at(0.50, 0.25, 0.24, 0.14) + assert raw_w > TORSO_WAIST_RX_MAX_FRAC_CHEST * raw_c + 1e-6 + assert rx_c == pytest.approx(raw_c, abs=1e-9) + assert rx_w == pytest.approx(TORSO_WAIST_RX_MAX_FRAC_CHEST * rx_c, abs=1e-9) + assert rx_w <= TORSO_WAIST_RX_MAX_FRAC_CHEST * rx_c + 1e-9 + + +def test_t2_male_taper_skips_hard_max() -> None: + """T2: taper 0.05 -> hard max NOT applied (ratio may exceed 0.80).""" + report = _full_torso_report(shoulder_x=0.20, hip_x=0.14) + tpl = _template(taper=0.05, template_id="male_adult_athletic", sex="male") + pkg = build_blockout_recipe(report, limbs=False, torso="ovals", template_applied=tpl) + by = {p.name: p for p in pkg.parts} + rx_c = float(by["RECIPE_torso_oval_chest"].rx_m or 0.0) + rx_w = float(by["RECIPE_torso_oval_waist"].rx_m or 0.0) + # Geometry with taper=0.05 yields raw ratio > 0.80 when hard max off. + raw_c = _waist_width_at(0.15, 0.20, 0.14, 0.05) + raw_w = _waist_width_at(0.50, 0.20, 0.14, 0.05) + assert raw_w / raw_c > TORSO_WAIST_RX_MAX_FRAC_CHEST + 1e-6 + assert rx_c == pytest.approx(raw_c, abs=1e-9) + assert rx_w == pytest.approx(raw_w, abs=1e-9) + assert rx_w > TORSO_WAIST_RX_MAX_FRAC_CHEST * rx_c + 1e-9 + + +def test_t3_ry_magnitudes_from_fracs() -> None: + """T3: ry chest half*0.85; waist half*0.58; hip half*0.80.""" + half_chest = 0.12 + half_hip = 0.13 + report = _full_torso_report(chest_depth_m=0.24, hip_depth_m=0.26) + pkg = build_blockout_recipe(report, limbs=False, torso="ovals") + by = {p.name: p for p in pkg.parts} + assert float(by["RECIPE_torso_oval_chest"].ry_m or 0.0) == pytest.approx( + half_chest * TORSO_OVAL_RY_CHEST_FRAC, abs=1e-9 + ) + assert float(by["RECIPE_torso_oval_waist"].ry_m or 0.0) == pytest.approx( + half_chest * TORSO_OVAL_RY_WAIST_FRAC, abs=1e-9 + ) + assert float(by["RECIPE_torso_oval_hip"].ry_m or 0.0) == pytest.approx( + half_hip * TORSO_OVAL_RY_HIP_FRAC, abs=1e-9 + ) + + +def test_t4_full3d_chest_rear_bias() -> None: + """T4: full3d - chest cy = y_mid + 0.28*ry; waist/hip cy = y_mid.""" + y_mid = 0.0 + report = _full_torso_report(chest_mid_y=y_mid) + pkg = build_blockout_recipe(report, limbs=False, torso="ovals") + by = {p.name: p for p in pkg.parts} + chest = by["RECIPE_torso_oval_chest"] + waist = by["RECIPE_torso_oval_waist"] + hip = by["RECIPE_torso_oval_hip"] + assert chest.center is not None and chest.ry_m is not None + ry = float(chest.ry_m) + assert chest.center[1] == pytest.approx(y_mid + TORSO_CHEST_Y_REAR_BIAS_FRAC_RY * ry, abs=1e-9) + assert waist.center is not None + assert hip.center is not None + assert waist.center[1] == pytest.approx(y_mid, abs=1e-9) + assert hip.center[1] == pytest.approx(y_mid, abs=1e-9) + assert chest.placement == "full3d" + + +def test_t4b_front_plane_no_b5_bias() -> None: + """T4b: front_plane (chest_y None) - chest cy stays y_mid / 0 - no B5 bias.""" + msgs: list[str] = [] + m = _metrics_for_ovals(chest_y=None) + parts = _build_torso_ovals(m, msgs, taper=0.14) + by = {p.name: p for p in parts} + chest = by["RECIPE_torso_oval_chest"] + assert chest.center is not None + assert chest.placement == "front_plane" + assert chest.center[1] == pytest.approx(0.0, abs=1e-9) + # Not the biased rear position + ry = float(chest.ry_m or 0.0) + biased = TORSO_CHEST_Y_REAR_BIAS_FRAC_RY * ry + assert abs(chest.center[1] - biased) > 1e-4 + + +def test_t5_ry_order_and_hip_gt_pelvis() -> None: + """T5: ry order + ry_hip > ry_pelvis.""" + report = _full_torso_report() + pkg = build_blockout_recipe(report, limbs=False, torso="ovals") + by = {p.name: p for p in pkg.parts} + ry_c = float(by["RECIPE_torso_oval_chest"].ry_m or 0.0) + ry_w = float(by["RECIPE_torso_oval_waist"].ry_m or 0.0) + ry_h = float(by["RECIPE_torso_oval_hip"].ry_m or 0.0) + ry_p = float(by["RECIPE_pelvis_oval"].ry_m or 0.0) + eps = 1e-9 + assert ry_w < ry_c - eps + assert ry_h >= ry_w - eps + assert ry_h > ry_p + eps + + +def test_t6_rz_span_and_layer_overlap() -> None: + """T6: rz = span*0.22 (+ floor) and adjacent layers overlap on Z.""" + report = _full_torso_report() + pkg = build_blockout_recipe(report, limbs=False, torso="ovals") + by = {p.name: p for p in pkg.parts} + names = ( + "RECIPE_torso_oval_chest", + "RECIPE_torso_oval_waist", + "RECIPE_torso_oval_hip", + ) + # span from report geometry + shoulder_z = 1.38 + hip_z = 0.95 + chest_z = 0.72 * 1.72 + z_top = max(shoulder_z, chest_z) + span = z_top - hip_z + expected_rz = max(TORSO_OVAL_RZ_FLOOR_M, span * TORSO_OVAL_RZ_SPAN_FRAC) + for name in names: + p = by[name] + assert p.rz_m is not None + assert float(p.rz_m) == pytest.approx(expected_rz, abs=1e-9) + for i in range(len(names) - 1): + a = by[names[i]] + b = by[names[i + 1]] + assert a.center is not None and b.center is not None + assert a.rz_m is not None and b.rz_m is not None + a_lo = float(a.center[2]) - float(a.rz_m) + b_hi = float(b.center[2]) + float(b.rz_m) + # adjacent layers touch or overlap + assert a_lo <= b_hi + 1e-9 + + +def test_t7_dual_breasts_proud_of_chest_front() -> None: + """T7: dual breasts - breast cy < chest front y (proud of plate).""" + report = _full_torso_report(with_soft_cs=True) + # Product-class breast prior: hang in front of flattened chest plate (B9). + tpl = _template(taper=0.22) + tpl = tpl.model_copy( + update={ + "constants": tpl.constants.model_copy( + update={"breast_y_m": -0.10, "breast_tilt_x_deg": 20.0} + ) + } + ) + pkg = build_blockout_recipe(report, limbs=False, torso="ovals", template_applied=tpl) + by = {p.name: p for p in pkg.parts} + chest = by["RECIPE_torso_oval_chest"] + assert chest.center is not None and chest.ry_m is not None + chest_front_y = float(chest.center[1]) - float(chest.ry_m) + breasts = [p for p in pkg.parts if p.role == "breast_soft" and p.center] + assert len(breasts) >= 2 + for b in breasts: + assert b.center is not None + assert b.center[1] < chest_front_y + 1e-9 + + +def test_t8_messages_depth_taper_and_front_pinch() -> None: + """T8: messages - depth taper + front pinch with front_y AND rear_y.""" + report = _full_torso_report() + pkg = build_blockout_recipe(report, limbs=False, torso="ovals") + depth = [m for m in pkg.messages if m.startswith("torso depth taper:")] + pinch = [m for m in pkg.messages if m.startswith("torso front pinch:")] + assert len(depth) == 1 + assert "anti-snowman" in depth[0] + assert len(pinch) == 1 + assert "waist_rx/chest_rx=" in pinch[0] + assert "chest_front_y=" in pinch[0] + assert "chest_rear_y=" in pinch[0] + by = {p.name: p for p in pkg.parts} + chest = by["RECIPE_torso_oval_chest"] + assert chest.center is not None and chest.ry_m is not None + front_y = float(chest.center[1]) - float(chest.ry_m) + rear_y = float(chest.center[1]) + float(chest.ry_m) + assert f"chest_front_y={front_y:.4f}" in pinch[0] + assert f"chest_rear_y={rear_y:.4f}" in pinch[0] + + +def test_t9_trap_path_unchanged() -> None: + """T9: trap path unchanged (default torso emits trap, not ovals).""" + report = _full_torso_report() + pkg = build_blockout_recipe(report, limbs=False) # default trap + names = {p.name for p in pkg.parts} + assert "RECIPE_torso_trap" in names + assert "RECIPE_torso_oval_chest" not in names + assert not any(m.startswith("torso front pinch:") for m in pkg.messages) + assert not any(m.startswith("torso depth taper:") for m in pkg.messages) + + +def test_t10_connection_gap_finite_and_constraints_smoke() -> None: + """T10: connection gap finite; constraints smoke green (axial).""" + # chest_front well forward of mid so head tip / neck tilt stay axial-pass (0032/0050). + report = _full_torso_report(with_soft_cs=True, chest_mid_y=0.0) + report = report.model_copy( + update={ + "landmarks_xyz": { + **report.landmarks_xyz, + "chest_front": _lm("chest_front", x_m=0.0, y_m=-0.13, z_m=1.25), + } + } + ) + pkg = build_blockout_recipe(report, limbs=True, torso="ovals") + gaps = connection_gap_metrics(pkg) + for key, val in gaps.items(): + assert math.isfinite(val), f"{key} not finite: {val}" + # Shoulder gaps: finite numeric inventory (not missing sentinel 1e9). + assert "shoulder_l" in gaps and "shoulder_r" in gaps + assert gaps["shoulder_l"] < 1e8, f"shoulder_l missing/sentinel: {gaps['shoulder_l']}" + assert gaps["shoulder_r"] < 1e8, f"shoulder_r missing/sentinel: {gaps['shoulder_r']}" + # Snapshot magnitudes for B10 inventory (chest rear-bias may shift parent Y-span). + assert gaps["shoulder_l"] == pytest.approx(gaps["shoulder_r"], abs=1e-6) + result = validate_constraints(pkg, report=report) + by_id = {r.id: r for r in result.rules} + axial = by_id.get("C_axial_depth_plane") + assert axial is not None + assert axial.status == "pass", axial.message + + +def test_t11_female_template_torso_waist_taper() -> None: + """T11: female template torso_waist_taper == 0.22.""" + doc = load_body_template("female_adult_athletic") + assert doc.torso_waist_taper == pytest.approx(0.22) + # male fence + male = load_body_template("male_adult_athletic") + assert male.torso_waist_taper == pytest.approx(0.05) + + +def test_t12_anti_equal_triad_rx_ry() -> None: + """T12: anti-regression - not equal triad rx / ry.""" + report = _full_torso_report() + pkg = build_blockout_recipe( + report, + limbs=False, + torso="ovals", + template_applied=_template(taper=0.22), + ) + by = {p.name: p for p in pkg.parts} + rx = [ + float(by[n].rx_m or 0.0) + for n in ( + "RECIPE_torso_oval_chest", + "RECIPE_torso_oval_waist", + "RECIPE_torso_oval_hip", + ) + ] + ry = [ + float(by[n].ry_m or 0.0) + for n in ( + "RECIPE_torso_oval_chest", + "RECIPE_torso_oval_waist", + "RECIPE_torso_oval_hip", + ) + ] + eps = 1e-9 + assert not (abs(rx[0] - rx[1]) < eps and abs(rx[1] - rx[2]) < eps) + assert not (abs(ry[0] - ry[1]) < eps and abs(ry[1] - ry[2]) < eps) + + +def test_t13_two_pass_cap_uses_post_taper_chest_rx() -> None: + """T13: B1 denominator is post-taper chest rx (not pre-taper baseline).""" + # Near-equal shoulder/hip so waist raw stays above 0.80*post-chest at high taper + # (default 0.20/0.14 at taper=0.22 already pinches below the hard max). + ws, wh = 0.25, 0.24 + taper = 0.22 + assert taper >= TORSO_WAIST_PINCH_TAPER_GATE + rx_c_post = _waist_width_at(0.15, ws, wh, taper) + rx_c_pre = _waist_width_at(0.15, ws, wh, 0.0) + rx_w_raw = _waist_width_at(0.50, ws, wh, taper) + expected = min(rx_w_raw, TORSO_WAIST_RX_MAX_FRAC_CHEST * rx_c_post) + wrong = min(rx_w_raw, TORSO_WAIST_RX_MAX_FRAC_CHEST * rx_c_pre) + # Cap must bind, and two-pass must disagree with pre-taper denominator. + assert rx_w_raw > TORSO_WAIST_RX_MAX_FRAC_CHEST * rx_c_post + 1e-9 + assert expected != pytest.approx(wrong, abs=1e-9) + assert rx_c_post < rx_c_pre - 1e-9 + + report = _full_torso_report(shoulder_x=ws, hip_x=wh) + pkg = build_blockout_recipe( + report, + limbs=False, + torso="ovals", + template_applied=_template(taper=taper), + ) + by = {p.name: p for p in pkg.parts} + rx_c = float(by["RECIPE_torso_oval_chest"].rx_m or 0.0) + rx_w = float(by["RECIPE_torso_oval_waist"].rx_m or 0.0) + assert rx_c == pytest.approx(rx_c_post, abs=1e-9) + assert rx_w == pytest.approx(expected, abs=1e-9) + assert rx_w != pytest.approx(wrong, abs=1e-9) + assert rx_w == pytest.approx(TORSO_WAIST_RX_MAX_FRAC_CHEST * rx_c, abs=1e-9)