#!/usr/bin/env python """blocks.py -- parametric garment blocks: worksheet numbers in, `md` config block out. The tailor's half of the lane, as code. You read the reference image with `.claude/skills/marvelous-designer/references/deconstruction.md` (slots -> anchoring -> placement targets -> ease), and this module turns those worksheet numbers into a config skeleton for tools/tailor/draft_garment.py: panels with computed line indices, seams with the correct parity, arrangements with the correct per-family x values, and the proven sim recipe. python tools/tailor/blocks.py fitted_top --name pari_v4 --band-top-z 1.31 --hem-z 1.05 --ease 17 python tools/tailor/blocks.py aline_skirt --name skirt_v2 --waist-z 1.05 --hem-z 0.45 python tools/tailor/blocks.py strand_skirt --name piupiu_v4 --band-top-z 1.05 --hem-z 0.45 python tools/tailor/blocks.py --selftest Writes a full config skeleton (name + `md` + `expect.bands`) to stdout or -o; the worksheet echo goes to stderr. Feed the result to draft_garment.py, or merge it into a shipping config in clothing/configs/. `md.exports` starts EMPTY on purpose -- guard the export until a snapshot looks right, then set all four. WHY BLOCKS INSTEAD OF HAND-TYPED POINT LISTS -- each function bakes in a discovery that cost a session to find: - Panel widths come from the body circumference AT THAT HEIGHT plus ease, interpolated from the measurement card (tools/tailor/lena_measurements.json). The body is stylized; real-world size charts produce clothes that don't fit. - Vertical mm are 1:1 with world metres; straps must reach shoulder_z. - BODICES TAPER (tooling.md 3.4): a rectangle cut for the bust carries ~400 mm of surplus to a waist hem and it can only fold. Default taper is computed from the hem-height circumference; equal on both panels so seam lengths match. - SEAM PARITY (tooling.md 3.3): these blocks draft front/back from the SAME point list offset by dx (identical, NOT mirrored), so side seams take (True, True). (False, False) -- what the original recipes shipped with -- twists the panel; a busy texture concealed exactly that for several rounds. Shoulder seams stay (False, False) (as shipped and working; never implicated). - ARRANGEMENT X IS PER FAMILY (tooling.md 5): Body_*_Center_1 needs the SAME x on both panels (50/50; a 50/0 split folds one shoulder); Leg_Skirt_* needs DIFFERENT x (50/0; same-x drops the skirt on the floor). Never harmonise. - Skirts arrange on Leg_Skirt_Front/Back y=92 -- Body_*_Waist put a skirt 8-14 cm high. Bottoms hold by TENSION (waist cut ~0.91 x hip circ); elastic only mid-settle, which draft_garment.py now emits. - Strand skirts are comb outlines with symmetric half-gaps at BOTH panel edges; an asymmetric first tooth made one band edge slanted and notched the waistband (the 0.075 mm "rounding" that wasn't). Exit codes: 0 ok - 2 bad arguments - 1 selftest failure. """ import argparse import json import math import os import sys import tempfile REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) DEFAULT_MEASUREMENTS = os.path.join(REPO_ROOT, "tools", "tailor", "lena_measurements.json") DEFAULT_AVATAR = os.path.join(REPO_ROOT, "tools", "tailor", "avatar", "Lena_QuatSkin_Avatar.fbx") DEFAULT_ZFAB = (r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric" r"\(Default for Simulation).zfab") PANEL_GAP = 250.0 # 2D spacing between front and back panel bounding boxes class BlockError(Exception): """Raised when worksheet numbers can't make a sane panel -- exits 2.""" # ------------------------------------------------------------------ measurements def load_measurements(path=DEFAULT_MEASUREMENTS): with open(path, "r", encoding="utf-8") as fh: m = json.load(fh) for key in ("shoulder_z", "chest_circ", "chest_z", "waist_circ", "waist_z", "hip_circ", "hip_z", "neck_circ", "neck_z"): if key not in m: raise BlockError("measurement card {} is missing {!r} -- regenerate with " "tools/tailor/measure_body.py".format(path, key)) return m def body_circ_at(m, z): """Body circumference (m) at height z, piecewise-linear between the card's landmarks. NEVER interpolates below the hip -- the legs bifurcate there and a slice measures nonsense -- it clamps to the hip value instead.""" knots = sorted([(m["neck_z"], m["neck_circ"]), (m["chest_z"], m["chest_circ"]), (m["waist_z"], m["waist_circ"]), (m["hip_z"], m["hip_circ"])], reverse=True) if z >= knots[0][0]: return knots[0][1] for (z_hi, c_hi), (z_lo, c_lo) in zip(knots, knots[1:]): if z >= z_lo: t = (z_hi - z) / (z_hi - z_lo) return c_hi + t * (c_lo - c_hi) return knots[-1][1] # below the hip: clamp, don't interpolate def _max_circ_over(m, z_lo, z_hi, samples=9): """Max body circumference over [z_lo, z_hi]. The landmark knots inside the span are sampled EXPLICITLY -- a uniform grid straddles the bust peak and undersizes the panel by ~2 cm.""" zs = [z_lo + (z_hi - z_lo) * i / (samples - 1.0) for i in range(samples)] zs += [z for z in (m["neck_z"], m["chest_z"], m["waist_z"], m["hip_z"]) if z_lo <= z <= z_hi] return max(body_circ_at(m, z) for z in zs) def _mm(metres): return round(metres * 1000.0, 1) # ----------------------------------------------------------------------- blocks def fitted_top(m, band_top_z, hem_z, ease_mm=100.0, back_scoop_mm=45.0, strap_width_mm=90.0, neck_gap_mm=None, armhole_depth_mm=165.0, taper_mm=None, shoulder_drop_mm=25.0, name="top"): """Tank / tee / fitted bodice / bandeau-with-straps. Proven: tee, pari. band_top_z / hem_z: placement targets in metres (worksheet row 4). The front scoop is derived so the visible band top lands at band_top_z while the straps reach the shoulders (they must, or the garment settles at the underbust -- straps beat tubes, always). """ if not band_top_z > hem_z: raise BlockError("band_top_z must be above hem_z") H = _mm(m["shoulder_z"] - hem_z) # strap top = shoulder band_top_h = _mm(band_top_z - hem_z) if band_top_h >= H - shoulder_drop_mm: raise BlockError("band top {}m is at/above the shoulders".format(band_top_z)) front_scoop = H - band_top_h circ = _max_circ_over(m, hem_z, band_top_z) W = round((circ * 1000.0 + ease_mm) / 2.0, 1) # per-panel width G = neck_gap_mm if neck_gap_mm is not None else min(260.0, round(0.44 * W, 1)) c = W / 2.0 tip_l = c - G / 2.0 - strap_width_mm if tip_l < 20.0: raise BlockError("neck gap {} + straps {} don't fit a {} panel" .format(G, strap_width_mm, W)) tip_y = H - shoulder_drop_mm arm_y = tip_y - armhole_depth_mm if arm_y <= 0: raise BlockError("armhole depth {} exceeds the panel height {}".format( armhole_depth_mm, H)) if taper_mm is None: # Bodices must taper: hem width from the circumference AT the hem. hem_w = (body_circ_at(m, hem_z) * 1000.0 + ease_mm) / 2.0 taper_mm = max(0.0, min(round((W - hem_w) / 2.0, 1), round(0.2 * W, 1))) def outline(scoop): neck_l, neck_r = c - G / 2.0, c + G / 2.0 if scoop > 0.5: ny = H mids = [(c - 0.3 * G, H - 0.75 * scoop), (c, H - scoop), (c + 0.3 * G, H - 0.75 * scoop)] else: # straight (tee back) ny = tip_y mids = [(c - 0.3 * G, tip_y), (c, tip_y), (c + 0.3 * G, tip_y)] pts = ([(tip_l, tip_y), (neck_l, ny)] + mids + [(neck_r, ny), (c + G / 2.0 + strap_width_mm, tip_y), (W, arm_y), (W - taper_mm, 0.0), (taper_mm, 0.0), (0.0, arm_y)]) return [(round(x, 2), round(y, 2)) for x, y in pts] # Lines: 0 shoulder_L | 1-4 neckline (open) | 5 shoulder_R | 6 armhole_R # 7 side_R | 8 hem | 9 side_L | 10 armhole_L (closing edge) lines = {"shoulder_l": 0, "shoulder_r": 5, "side_r": 7, "hem": 8, "side_l": 9} md = { "panels": [ {"name": "front", "dx": 0.0, "note": "fitted_top block: W {} (circ {} + ease {}), H {}, scoop {}, taper {}" .format(W, round(circ * 1000, 1), ease_mm, H, round(front_scoop, 1), taper_mm), "points": outline(front_scoop)}, {"name": "back", "dx": round(W + PANEL_GAP, 1), "note": "same block, back scoop {}; lines 0 shL | 1-4 neck | 5 shR | " "6 armR | 7 sideR | 8 hem | 9 sideL | 10 armL".format(back_scoop_mm), "points": outline(back_scoop_mm)}, ], "seams": [ # Shoulders: (False, False) as shipped and working -- never implicated. {"a": "front", "b": "back", "lines": [lines["shoulder_l"], lines["shoulder_r"]]}, # Sides: identical (not mirrored) panels need BOTH edges reversed -- # (False, False) here is the twist that a taniko print concealed. {"a": "front", "a_line": lines["side_r"], "b": "back", "b_line": lines["side_r"], "reverse_a": True, "reverse_b": True}, {"a": "front", "a_line": lines["side_l"], "b": "back", "b_line": lines["side_l"], "reverse_a": True, "reverse_b": True}, ], "arrangements": [ # Body_*_Center_1 wants the SAME x on both panels (sweep-verified); # the shipped 50/0 split folds one shoulder and exposes the reverse face. {"panel": "front", "point": "Body_Front_Center_1", "offset": [50, 55, 50]}, {"panel": "back", "point": "Body_Back_Center_1", "offset": [50, 55, 50]}, ], # Sides fully sewn -> strengthen through the WHOLE settle, relax at the end. # (If you cut the side seams down to partial coverage, DROP strengthen: # it rotates an under-constrained garment.) "sim": {"strengthen": True, "settle_frames": 250, "relax_frames": 50}, } bands = {name: {"top_m": round(band_top_z, 3), "bottom_m": round(hem_z, 3), "tol_m": 0.03, "from": "cover"}} worksheet = {"block": "fitted_top", "panel_w_mm": W, "panel_h_mm": H, "body_circ_mm": round(circ * 1000, 1), "ease_mm": ease_mm, "front_scoop_mm": round(front_scoop, 1), "back_scoop_mm": back_scoop_mm, "taper_mm": taper_mm, "lines": lines} return {"md": md, "expect_bands": bands, "worksheet": worksheet} def aline_skirt(m, waist_z, hem_z, tension=0.91, flare=1.5, waist_elastic=False, elastic_ratio=0.9, arrange_y=92, name="skirt"): """A-line / straight skirt -- the most forgiving garment. Proven: skirt, piupiu v2. Held up by TENSION: the waist edge is cut tension x hip circumference (smaller than the hips it must pass over). flare = hem width / waist width. """ if not waist_z > hem_z: raise BlockError("waist_z must be above hem_z") if tension >= 0.97: raise BlockError("tension {} won't grip -- the waist must be cut smaller " "than the hips (0.90-0.92 proven)".format(tension)) H = _mm(waist_z - hem_z) waist_w = round(m["hip_circ"] * 1000.0 * tension / 2.0, 1) hem_w = round(waist_w * flare, 1) inset = round((hem_w - waist_w) / 2.0, 1) pts = [(inset, H), (inset + waist_w, H), (hem_w, 0.0), (0.0, 0.0)] lines = {"waist": 0, "side_r": 1, "hem": 2, "side_l": 3} md = { "panels": [ {"name": "front", "dx": 0.0, "note": "aline_skirt block: waist {} (= {} x {} hip), hem {}, H {}" .format(waist_w, tension, _mm(m["hip_circ"]), hem_w, H), "points": [(round(x, 2), round(y, 2)) for x, y in pts]}, {"name": "back", "dx": round(hem_w + PANEL_GAP, 1), "note": "identical block; lines 0 waist | 1 side_R | 2 hem | 3 side_L", "points": [(round(x, 2), round(y, 2)) for x, y in pts]}, ], "seams": [ # Identical (not mirrored) panels: side seams take (True, True). {"a": "front", "a_line": lines["side_r"], "b": "back", "b_line": lines["side_r"], "reverse_a": True, "reverse_b": True}, {"a": "front", "a_line": lines["side_l"], "b": "back", "b_line": lines["side_l"], "reverse_a": True, "reverse_b": True}, ], "arrangements": [ # Leg_Skirt_*, NOT Body_*_Waist (which places 8-14 cm high), and the # two x values must DIFFER -- 50/50 drops the skirt on the floor. {"panel": "front", "point": "Leg_Skirt_Front", "offset": [50, arrange_y, 50]}, {"panel": "back", "point": "Leg_Skirt_Back", "offset": [0, arrange_y, 50]}, ], "sim": {"strengthen": True, "settle_frames": 250, "relax_frames": 50}, } if waist_elastic: md["sim"]["elastic"] = [ {"panel": "front", "line": lines["waist"], "total_length": round(waist_w * elastic_ratio, 1)}, {"panel": "back", "line": lines["waist"], "total_length": round(waist_w * elastic_ratio, 1)}, ] md["sim"]["elastic_frames"] = 80 bands = {name: {"top_m": round(waist_z, 3), "bottom_m": round(hem_z, 3), "tol_m": 0.03, "bottom_tol_m": 0.04, "from": "cover"}} worksheet = {"block": "aline_skirt", "waist_w_mm": waist_w, "hem_w_mm": hem_w, "panel_h_mm": H, "tension": tension, "flare": flare, "lines": lines} return {"md": md, "expect_bands": bands, "worksheet": worksheet} def strand_skirt(m, band_top_z, hem_z, band_height_mm=60.0, strands_per_panel=16, gap_fraction=0.25, tension=0.91, waist_elastic=True, elastic_ratio=0.9, arrange_y=92, particle_distance_mm=20.0, name="strandskirt"): """Strand / fringe skirt (piupiu, hula, fur trim): a waistband with teeth cut into the outline -- NEVER partial seams. Only the two band side edges sew. Light garments do not slide into place: arrangement height IS the placement, so arrange_y is load-bearing. Elastic defaults ON (mid-settle) -- that is what locked the piupiu waistband. """ if not band_top_z > hem_z: raise BlockError("band_top_z must be above hem_z") H = _mm(band_top_z - hem_z) if band_height_mm >= H: raise BlockError("band height {} swallows the whole {} panel".format( band_height_mm, H)) band_bot = round(H - band_height_mm, 2) # teeth run band_bot -> 0 W = round(m["hip_circ"] * 1000.0 * tension / 2.0, 1) pitch = W / strands_per_panel gap = gap_fraction * pitch tooth = pitch - gap # Symmetric half-gaps: tooth i spans [gap/2 + i*pitch, gap/2 + i*pitch + tooth], # so BOTH band side edges are true verticals of exactly band_height_mm. The # asymmetric version (first tooth flush) slants one edge and notches the band. pts = [(0.0, H), (W, H), (W, band_bot)] for i in reversed(range(strands_per_panel)): x_l = gap / 2.0 + i * pitch x_r = x_l + tooth pts += [(x_r, band_bot), (x_r, 0.0), (x_l, 0.0), (x_l, band_bot)] pts.append((0.0, band_bot)) pts = [(round(x, 2), round(y, 2)) for x, y in pts] side_l_line = len(pts) - 1 # closing edge back to (0, H) lines = {"waist": 0, "side_r": 1, "side_l": side_l_line} panel_note = ("strand_skirt block: band {} tall + {} strands of {} (gap {}) over " "{} wide (= {} x hip); teeth are OUTLINE, not partial seams" .format(band_height_mm, strands_per_panel, round(tooth, 1), round(gap, 1), W, tension)) md = { "panels": [ {"name": "front", "dx": 0.0, "note": panel_note, "points": pts, "particle_distance": particle_distance_mm}, {"name": "back", "dx": round(W + PANEL_GAP, 1), "note": "identical comb; only the two band side edges sew", "points": pts, "particle_distance": particle_distance_mm}, ], "seams": [ {"a": "front", "a_line": lines["side_r"], "b": "back", "b_line": lines["side_r"], "reverse_a": True, "reverse_b": True}, {"a": "front", "a_line": lines["side_l"], "b": "back", "b_line": lines["side_l"], "reverse_a": True, "reverse_b": True}, ], "arrangements": [ {"panel": "front", "point": "Leg_Skirt_Front", "offset": [50, arrange_y, 50]}, {"panel": "back", "point": "Leg_Skirt_Back", "offset": [0, arrange_y, 50]}, ], "sim": { "strengthen": True, "settle_frames": 250, "relax_frames": 50, "elastic_frames": 80, }, } if waist_elastic: md["sim"]["elastic"] = [ {"panel": "front", "line": lines["waist"], "total_length": round(W * elastic_ratio, 1)}, {"panel": "back", "line": lines["waist"], "total_length": round(W * elastic_ratio, 1)}, ] bands = {name: {"top_m": round(band_top_z, 3), "bottom_m": round(hem_z, 3), "tol_m": 0.03, "bottom_tol_m": 0.04, "from": "cover"}} worksheet = {"block": "strand_skirt", "band_w_mm": W, "panel_h_mm": H, "band_height_mm": band_height_mm, "strands": strands_per_panel, "tooth_mm": round(tooth, 1), "gap_mm": round(gap, 1), "tension": tension, "lines": lines, "qc_note": "gappy silhouette: qc_placement.py pixel classifier is " "INVALID here -- measure the exported OBJ instead"} return {"md": md, "expect_bands": bands, "worksheet": worksheet} BLOCKS = {"fitted_top": fitted_top, "aline_skirt": aline_skirt, "strand_skirt": strand_skirt} # ------------------------------------------------------------- config skeleton def config_skeleton(name, block_out, texture=None, texture_dpi=None): """Wrap a block's md fragment in a full, runnable config: scene defaults, QC snapshot paths (front, back, AND pre-sim), and a guarded publish stage.""" tmp = tempfile.gettempdir().replace("\\", "/") md = { "reset": "new_project", "avatar_fbx": DEFAULT_AVATAR.replace("\\", "/"), "avatar_scale": 10.0, "add_arrangement_points": True, "auto_translate": True, "zfab": DEFAULT_ZFAB.replace("\\", "/"), } if texture: md["texture"] = texture if texture_dpi: md["texture_dpi"] = texture_dpi md.update(block_out["md"]) md.update({ "cam_viewpoint": 2, "presim_snapshot": "{}/tinqs_md_{}_presim.png".format(tmp, name), "snapshot": "{}/tinqs_md_{}.png".format(tmp, name), "back_snapshot": "{}/tinqs_md_{}_back.png".format(tmp, name), "export_dir": os.path.join(REPO_ROOT, "tools", "tailor").replace("\\", "/"), "export_basename": "lena_{}".format(name), # Guard the export: [] until a snapshot looks right, then all four. "exports": [], }) return { "name": name, "_worksheet": block_out["worksheet"], "md": md, "expect": {"bands": block_out["expect_bands"]}, } # ----------------------------------------------------------- offline validation def _edge_len(points, i): ax, ay = points[i] bx, by = points[(i + 1) % len(points)] return math.hypot(bx - ax, by - ay) def verify_seam_symmetry(md, tol=1e-6): """Offline mirror of the generated script's GetLineLength check: paired seam edges must be EQUAL, not close. Returns a list of mismatch strings.""" panels = {p["name"]: p["points"] for p in md["panels"]} bad = [] for s in md.get("seams", []): pairs = ([(int(n), int(n)) for n in s["lines"]] if "lines" in s else [(int(s["a_line"]), int(s["b_line"]))]) for la, lb in pairs: fa = _edge_len(panels[s["a"]], la) fb = _edge_len(panels[s["b"]], lb) if abs(fa - fb) > tol: bad.append("{}.{}={:.4f} vs {}.{}={:.4f}".format( s["a"], la, fa, s["b"], lb, fb)) return bad # ---------------------------------------------------------------------- selftest def _selftest(): sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import draft_garment m = load_measurements() fails = [] def check(label, cond, detail=""): print(" {} {}{}".format("ok " if cond else "FAIL", label, " -- " + str(detail) if detail else "")) if not cond: fails.append(label) print("[1/4] fitted_top reproduces the shipped tee from placement targets") tee = fitted_top(m, band_top_z=1.257, hem_z=0.917, ease_mm=97) ws = tee["worksheet"] check("panel width ~590", abs(ws["panel_w_mm"] - 590) < 15, ws["panel_w_mm"]) check("panel height ~480", abs(ws["panel_h_mm"] - 480) < 10, ws["panel_h_mm"]) check("scoop ~140", abs(ws["front_scoop_mm"] - 140) < 12, ws["front_scoop_mm"]) check("no taper at hip hem", ws["taper_mm"] == 0.0, ws["taper_mm"]) print("[2/4] fitted_top reproduces the shipped pari -- WITH the taper it lacked") pari = fitted_top(m, band_top_z=1.295, hem_z=1.05, ease_mm=17) ws = pari["worksheet"] check("panel width ~550", abs(ws["panel_w_mm"] - 550) < 15, ws["panel_w_mm"]) check("panel height ~350", abs(ws["panel_h_mm"] - 350) < 10, ws["panel_h_mm"]) check("scoop ~105", abs(ws["front_scoop_mm"] - 105) < 10, ws["front_scoop_mm"]) check("waist hem tapers (3.4: bodices must)", 50 < ws["taper_mm"] < 110, ws["taper_mm"]) # Regression: a uniform sample grid can straddle the bust knot and undersize # the panel ~2 cm; the knots must be sampled explicitly. ws2 = fitted_top(m, band_top_z=1.31, hem_z=1.05, ease_mm=17)["worksheet"] check("bust peak captured at any band span", abs(ws2["body_circ_mm"] - 1083.4) < 1, ws2["body_circ_mm"]) print("[3/4] aline_skirt reproduces the shipped piupiu v2") piu = aline_skirt(m, waist_z=1.05, hem_z=0.45, tension=0.913, flare=1.64) ws = piu["worksheet"] check("waist ~500/panel", abs(ws["waist_w_mm"] - 500) < 5, ws["waist_w_mm"]) check("hem ~820", abs(ws["hem_w_mm"] - 820) < 10, ws["hem_w_mm"]) check("height 600", abs(ws["panel_h_mm"] - 600) < 1, ws["panel_h_mm"]) print("[4/4] every block: symmetric seams, valid config, emitted script compiles") strand = strand_skirt(m, band_top_z=1.05, hem_z=0.45) n_pts = len(strand["md"]["panels"][0]["points"]) check("comb outline point count", n_pts == 4 * 16 + 4, n_pts) tmpdir = tempfile.mkdtemp(prefix="tinqs_blocks_selftest_") for label, out in (("tee", tee), ("pari", pari), ("piupiu", piu), ("strand", strand)): bad = verify_seam_symmetry(out["md"]) check("{}: paired seam edges equal".format(label), not bad, "; ".join(bad)) cfg = config_skeleton("selftest_" + label, out) cfg_path = os.path.join(tmpdir, label + ".json") with open(cfg_path, "w", encoding="utf-8") as fh: json.dump(cfg, fh, indent=2) try: spec = draft_garment.parse_md(cfg, cfg_path) text = draft_garment.emit(spec, "all", cfg_path) compile(text, cfg_path, "exec") check("{}: parse+emit+compile".format(label), True) except Exception as exc: # noqa: BLE001 check("{}: parse+emit+compile".format(label), False, exc) print("\n{} -- artifacts in {}".format( "ALL PASS" if not fails else "{} FAILURE(S)".format(len(fails)), tmpdir)) return 1 if fails else 0 # -------------------------------------------------------------------------- cli def main(argv=None): argv = list(sys.argv[1:] if argv is None else argv) if "--selftest" in argv: return _selftest() ap = argparse.ArgumentParser( description="Emit a garment config skeleton from a tailoring block. " "Read references/deconstruction.md first: these arguments ARE " "the worksheet.") common = argparse.ArgumentParser(add_help=False) common.add_argument("--measurements", default=DEFAULT_MEASUREMENTS, help="measurement card (default: Lena's)") common.add_argument("--name", required=True, help="garment name, e.g. pari_v4") common.add_argument("-o", "--out", default="-", help="config path ('-' = stdout)") common.add_argument("--texture", help="optional texture PNG (DPI sets physical size)") common.add_argument("--texture-dpi", type=float) sub = ap.add_subparsers(dest="block", required=True) top = sub.add_parser("fitted_top", parents=[common], help="tank/tee/bodice (proven)") top.add_argument("--band-top-z", type=float, required=True) top.add_argument("--hem-z", type=float, required=True) top.add_argument("--ease", type=float, default=100.0, help="total ease mm: 15-20 snug, ~100 regular") top.add_argument("--back-scoop", type=float, default=45.0) top.add_argument("--strap-width", type=float, default=90.0) top.add_argument("--neck-gap", type=float) top.add_argument("--armhole-depth", type=float, default=165.0) top.add_argument("--taper", type=float, help="per-side hem taper mm (default: computed)") sk = sub.add_parser("aline_skirt", parents=[common], help="A-line/straight skirt (proven)") sk.add_argument("--waist-z", type=float, required=True) sk.add_argument("--hem-z", type=float, required=True) sk.add_argument("--tension", type=float, default=0.91) sk.add_argument("--flare", type=float, default=1.5) sk.add_argument("--waist-elastic", action="store_true") sk.add_argument("--arrange-y", type=int, default=92) st = sub.add_parser("strand_skirt", parents=[common], help="strand/fringe skirt (comb outline)") st.add_argument("--band-top-z", type=float, required=True) st.add_argument("--hem-z", type=float, required=True) st.add_argument("--band-height", type=float, default=60.0) st.add_argument("--strands", type=int, default=16, help="strands per panel") st.add_argument("--gap-fraction", type=float, default=0.25) st.add_argument("--tension", type=float, default=0.91) st.add_argument("--no-waist-elastic", action="store_true") st.add_argument("--arrange-y", type=int, default=92) args = ap.parse_args(argv) try: m = load_measurements(args.measurements) if args.block == "fitted_top": out = fitted_top(m, args.band_top_z, args.hem_z, ease_mm=args.ease, back_scoop_mm=args.back_scoop, strap_width_mm=args.strap_width, neck_gap_mm=args.neck_gap, armhole_depth_mm=args.armhole_depth, taper_mm=args.taper, name=args.name) elif args.block == "aline_skirt": out = aline_skirt(m, args.waist_z, args.hem_z, tension=args.tension, flare=args.flare, waist_elastic=args.waist_elastic, arrange_y=args.arrange_y, name=args.name) else: out = strand_skirt(m, args.band_top_z, args.hem_z, band_height_mm=args.band_height, strands_per_panel=args.strands, gap_fraction=args.gap_fraction, tension=args.tension, waist_elastic=not args.no_waist_elastic, arrange_y=args.arrange_y, name=args.name) except BlockError as exc: print("error: {}".format(exc), file=sys.stderr) return 2 bad = verify_seam_symmetry(out["md"]) if bad: print("error: block produced unequal seam pairs (bug): {}".format(bad), file=sys.stderr) return 2 cfg = config_skeleton(args.name, out, texture=args.texture, texture_dpi=args.texture_dpi) text = json.dumps(cfg, indent=2) print("worksheet: {}".format(json.dumps(out["worksheet"])), file=sys.stderr) if args.out == "-": print(text) else: with open(args.out, "w", encoding="utf-8") as fh: fh.write(text + "\n") print("wrote {}".format(args.out), file=sys.stderr) print("next: python tools/tailor/draft_garment.py --config {} --emit " "work/{}/md_script.py".format(args.out, args.name), file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())