Files
animation/tools/tailor/blocks.py
T

592 lines
28 KiB
Python
Raw Normal View History

#!/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())