feat: clothing lane, character sources, and DCC bridges

Bulk import of the working lanes that were living untracked on the PC.

Content:
- characters/  Lena/male body lanes, bakes, texture work, run logs
- clothing/    garment pipeline, configs, gates, contract docs
- garments/    MD-authored garment sources (.zprj/.zpac)
- UAL-Lib/     Universal Animation Library 2 source (.blend/.fbx/.glb)
- tools/       blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent
- docs/, plans/, dev/, .agents/plans/

Repo hygiene:
- .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the
  Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the
  ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out
  on purpose — ~250 are already tracked raw and converting them would
  rewrite every one without shrinking history.
- .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable
  from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies
  dropped by a `>/dev/null` redirect on Windows).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:55:43 -07:00
parent 3363209cac
commit 3ba86b2ea8
558 changed files with 68622 additions and 8 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.
@@ -0,0 +1,53 @@
# Backup — kapa haka MD work, taken 2026-07-31 before the piupiu v3 re-cut
Snapshot of the **shipped** kapa haka authoring work, taken because none of
`tools/tailor/` is tracked in git and the next step edits the piupiu pattern.
Nothing in here is referenced by any script — it exists purely to restore from.
## What's inside
| Folder | Contents |
|---|---|
| `tailor/` | Every `lena_piupiu_*`, `lena_pari_*`, `lena_kapahaka_outfit_*` (`.zprj` / `.fbx` / `.obj` / `.mtl` / `.zpac` / colorway PNG), plus `md_piupiu.py`, `md_pari.py`, `lena_measurements.json` |
| `screenshots/` | Review renders for piupiu v1v2, pari v1v3, outfit v1v3 |
| `textures/` | `taniko.png`, `piupiu.png` (generated fabric maps) |
| `configs/` | `piupiu.json`, `pari.json`, `piupiu_sb.json` as they stood |
| `game-outfit/` | Shipped `Female_Kapahaka_{Body,Legs}.{gltf,bin}` from `ariki-game/assets/quaternius/outfits/kapahaka/` |
55 files, 86 MB. Copied with timestamps preserved; SHA1-verified for the four
`.zprj`/`.fbx` sources and the shipped `Legs.bin`.
## Deliberately NOT backed up
- `clothing/work/piupiu*/` — 92102 MB each of Blender stage checkpoints. Fully
regenerable (`census..export` ran in 26 s in the acceptance test), so not worth
the disk.
- `Female_KapahakaSB_*` — owned by the concurrent skirt-bones session; not mine
to snapshot mid-write.
- tee / A-line skirt garments — unrelated to this change, untouched.
## To restore
```bash
cd C:/Users/Jeremy/tinqs/animation
B=tools/tailor/backup/2026-07-31-pre-v3
cp -p $B/tailor/* tools/tailor/
cp -p $B/screenshots/* tools/tailor/screenshots/
cp -p $B/textures/* tools/tailor/textures/
cp -p $B/configs/* clothing/configs/
cp -p $B/game-outfit/* ../ariki-game/assets/quaternius/outfits/kapahaka/
```
After restoring the game-outfit files, force a Godot reimport of the two `.gltf`
files (`ariki-game/tools/targeted_reimport.sh`) or the old `.godot/imported/`
entries will keep serving the newer mesh.
## Why v3 is being cut
The piupiu is cut tighter than the hip it passes over (waist edge 500 mm/panel =
1000 mm total vs Lena's 1095 mm hips), so it drapes as a taut pencil wrap on the
thighs with ~12 cm clearance. `fit`/G3 measures 8 mm penetration **at rest**;
G5 measures 216231 verts up to 8.5 cm deep in Idle/Walk, because
`weights: "dress"` makes the sub-hip skirt pelvis-only — the thigh swings, the
skirt doesn't. v3 re-cuts for real clearance and adds MD collision thickness.
See the session discussion and `.agents/plans/clothing-pipeline-unification-2026-07-31.md` §8.
@@ -0,0 +1,47 @@
{
"name": "pari",
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_pari_v3_garment.fbx",
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
"weld_threshold": 0.0006,
"shell_mm": 4,
"align": {
"top_bone": "neck_01",
"scale_xy": 0.1,
"scale_z": 0.1,
"z_nudge": 0.0483,
"xy_nudge": [
0.0,
0.0
]
},
"parts": {
"Pari": {
"slot": "Body",
"islands": [
0
],
"tris": 4000,
"planar_deg": 5,
"fit": {
"target": "BODY_SHELL",
"mask": "full",
"offset_mm": 3,
"wrap_mode": "OUTSIDE"
},
"weights": "dress",
"color": [
0.5,
0.2,
0.15
],
"texture": "../tools/tailor/textures/taniko.png"
}
},
"export": {
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
"gender": "Female",
"set": "Kapahaka",
"note": "T\u00c4\u0081niko bodice (pari) - Body slot"
},
"min_island_verts": 40
}
@@ -0,0 +1,48 @@
{
"name": "piupiu",
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_piupiu_v2_garment.fbx",
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
"weld_threshold": 0.0006,
"shell_mm": 4,
"align": {
"top_bone": "spine_01",
"scale_xy": 0.1,
"scale_z": 0.1,
"z_nudge": 0.037,
"xy_nudge": [
0.0,
0.0
]
},
"parts": {
"Piupiu": {
"slot": "Legs",
"islands": [
0
],
"tris": 8000,
"planar_deg": 5,
"fit": {
"target": "BODY_SHELL",
"mask": "full",
"offset_mm": 3,
"wrap_mode": "OUTSIDE",
"hem_mm": 14
},
"weights": "dress",
"color": [
0.45,
0.35,
0.15
],
"texture": "../tools/tailor/textures/piupiu.png"
}
},
"export": {
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
"gender": "Female",
"set": "Kapahaka",
"note": "Flax skirt (piupiu) - Legs slot"
},
"min_island_verts": 40
}
@@ -0,0 +1,48 @@
{
"name": "piupiu_sb",
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_piupiu_v2_garment.fbx",
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin_SkirtRig.glb",
"weld_threshold": 0.0006,
"shell_mm": 4,
"align": {
"top_bone": "spine_01",
"scale_xy": 0.1,
"scale_z": 0.1,
"z_nudge": 0.037,
"xy_nudge": [
0.0,
0.0
]
},
"parts": {
"Piupiu": {
"slot": "Legs",
"islands": [
0
],
"tris": 8000,
"planar_deg": 5,
"fit": {
"target": "BODY_SHELL",
"mask": "full",
"offset_mm": 3,
"wrap_mode": "OUTSIDE",
"hem_mm": 14
},
"weights": "skirt_bones",
"color": [
0.45,
0.35,
0.15
],
"texture": "../tools/tailor/textures/piupiu.png"
}
},
"export": {
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
"gender": "Female",
"set": "KapahakaSB",
"note": "Flax skirt (piupiu) on the skirt-boned SkirtRig Lena copy - Legs slot. Same garment as piupiu.json; differs only in body + weights mode."
},
"min_island_verts": 40
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

@@ -0,0 +1,24 @@
{
"source": "C:\\Users\\Jeremy\\tinqs\\ariki-game\\assets\\quaternius\\derived-bodies\\Ariki_Female_QuatSkin.glb",
"units": "meters (glTF)",
"height_total": 1.777,
"chest_circ": 1.0834,
"chest_z": 1.2636,
"waist_circ": 0.6747,
"waist_z": 1.0888,
"hip_circ": 1.0947,
"hip_z": 0.9455,
"thigh_circ": 0.8421,
"thigh_z": 0.8652,
"neck_circ": 0.3923,
"neck_z": 1.4568,
"bicep_circ": 0.417,
"shoulder_width": 0.3193,
"arm_len_shoulder_to_wrist": 0.5148,
"nape_to_pelvis": 0.4808,
"crotch_height": 0.9455,
"pelvis_height": 0.9318,
"knee_height": 0.5171,
"ankle_height": 0.1063,
"shoulder_z": 1.3973
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

@@ -0,0 +1,10 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_19892
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
map_Ka C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
map_Kd C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

@@ -0,0 +1,10 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_34682
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
map_Ka C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
map_Kd C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

@@ -0,0 +1,10 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_41289
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
map_Ka C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
map_Kd C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

@@ -0,0 +1,10 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_18617
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
map_Ka C:/Users/Jeremy/tinqs/animation/tools/tailor/piupiu.png
map_Kd C:/Users/Jeremy/tinqs/animation/tools/tailor/piupiu.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

@@ -0,0 +1,10 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_32185
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
map_Ka C:/Users/Jeremy/tinqs/animation/tools/tailor/piupiu.png
map_Kd C:/Users/Jeremy/tinqs/animation/tools/tailor/piupiu.png
@@ -0,0 +1,61 @@
# Pari v2 (kapa haka taniko bodice) — tube-with-straps construction.
# Straps solve the v1 slide-down (tube settled underbust) AND cover the
# avatar's baked-in bra straps. Uses the proven tee shoulder-seam recipe.
# QC targets: band top ~1.31 m (above bust), hem ~1.05 m (waist) +-3 cm.
# python tools/md_bridge.py --file tools/tailor/md_pari.py --timeout 500
import os
import tempfile
report = {}
AVATAR_FBX = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx"
ZFAB = r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab"
TEX = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\textures\taniko.png"
utility_api.NewProject()
op = ApiTypes.ImportExportOption()
op.scale = 10.0
op.bAddArrangementPoints = True
op.bAutoTranslate = True
report["avatar"] = import_api.ImportFBX(AVATAR_FBX, op)
# Tank-style: shoulder->waist 350 tall, 550/panel (snug over 1083 bust),
# straps 90 wide, wide-deep front scoop so the taniko band reads as a tube.
# y+ UP. Lines: 0 shoulder_L | 1-4 scoop (open) | 5 shoulder_R |
# 6 armhole_R | 7 side_R | 8 hem | 9 side_L | 10 armhole_L
def panel(scoop, dx):
top = 350.0
band = 350.0 - 105.0 # scoop floor -> band top ~ z 1.30
pts = [(65.0, top - 25.0), (155.0, top),
(205.0, top - scoop * 0.75), (275.0, top - scoop),
(345.0, top - scoop * 0.75), (395.0, top), (485.0, top - 25.0),
(550.0, 185.0), (550.0, 0.0), (0.0, 0.0), (0.0, 185.0)]
return pattern_api.CreatePatternWithPoints([(x + dx, y, 0) for (x, y) in pts])
pf = panel(105.0, 0.0) # front scoop floor = band top
pb = panel(45.0, 800.0) # shallower back
report["ids"] = [pf, pb]
for line in (0, 5, 7, 9):
pattern_api.AddSeamlinePairGroup(pf, line, pb, line, False, False)
fab = fabric_api.AddFabric(ZFAB)
fabric_api.SetBaseTextureMapImageGivenFilePath(TEX, fab)
for p in (pf, pb):
pattern_api.SetPatternPieceFabricIndex(p, fab)
arr = {a["ArrangementName"]: int(a["ArrangementIndex"])
for a in pattern_api.GetArrangementList()}
pattern_api.SetArrangement(pf, arr["Body_Front_Center_1"])
pattern_api.SetArrangement(pb, arr["Body_Back_Center_1"])
pattern_api.SetArrangementPosition(pf, 50, 55, 50)
pattern_api.SetArrangementPosition(pb, 0, 55, 50)
utility_api.ResetClothArrangement()
report["sim1"] = utility_api.Simulate(300)
utility_api.Refresh3DWindow()
snap = os.path.join(tempfile.gettempdir(), "tinqs_md_pari_v2.png")
export_api.ExportSnapshot3D(snap)
report["snap"] = snap
result = report
@@ -0,0 +1,56 @@
# Piupiu v2 (kapa haka flax skirt) — placement-targeted rebuild.
# QC targets (from reference photo, tools/tailor/qc_placement.py verifies):
# waistband top 1.05 m +-3 cm, hem 0.45 m +-4 cm (just below knee)
# python tools/md_bridge.py --file tools/tailor/md_piupiu.py --timeout 500
import os
import tempfile
report = {}
AVATAR_FBX = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx"
ZFAB = r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab"
TEX = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\textures\piupiu.png" # dpi 43.3 = 600 mm
utility_api.NewProject()
op = ApiTypes.ImportExportOption()
op.scale = 10.0
op.bAddArrangementPoints = True
op.bAutoTranslate = True
report["avatar"] = import_api.ImportFBX(AVATAR_FBX, op)
# length 600 (waist 1.05 -> hem 0.45); tension waist 500/panel; A-line hem.
def panel(dx):
pts = [(160.0, 600.0), (660.0, 600.0), (820.0, 0.0), (0.0, 0.0)]
return pattern_api.CreatePatternWithPoints([(x + dx, y, 0) for (x, y) in pts])
sf = panel(0.0)
sb = panel(1000.0)
report["ids"] = [sf, sb]
pattern_api.AddSeamlinePairGroup(sf, 1, sb, 1, False, False)
pattern_api.AddSeamlinePairGroup(sf, 3, sb, 3, False, False)
fab = fabric_api.AddFabric(ZFAB)
fabric_api.SetBaseTextureMapImageGivenFilePath(TEX, fab)
for p in (sf, sb):
pattern_api.SetPatternPieceFabricIndex(p, fab)
arr = {a["ArrangementName"]: int(a["ArrangementIndex"])
for a in pattern_api.GetArrangementList()}
pattern_api.SetArrangement(sf, arr["Body_Front_Waist"])
pattern_api.SetArrangement(sb, arr["Body_Back_Waist"])
pattern_api.SetArrangementPosition(sf, 50, 30, 50)
pattern_api.SetArrangementPosition(sb, 0, 30, 50)
pattern_api.SetPatternStrengthen(sf, True)
pattern_api.SetPatternStrengthen(sb, True)
utility_api.ResetClothArrangement()
report["sim1"] = utility_api.Simulate(250)
pattern_api.SetPatternStrengthen(sf, False)
pattern_api.SetPatternStrengthen(sb, False)
report["sim2"] = utility_api.Simulate(50)
utility_api.Refresh3DWindow()
snap = os.path.join(tempfile.gettempdir(), "tinqs_md_piupiu_v2.png")
export_api.ExportSnapshot3D(snap)
report["snap"] = snap
result = report
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+591
View File
@@ -0,0 +1,591 @@
#!/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())
+821
View File
@@ -0,0 +1,821 @@
#!/usr/bin/env python
"""draft_garment.py -- ONE template that replaces the four copy-paste MD garment scripts.
python tools/tailor/draft_garment.py --config <resolved.json> --emit <out_script.py>
[--stage draft|drape|publish|all] [--timeout-hint]
GENERATION MODE ONLY. This script never talks to Marvelous Designer: it reads the
`md` block of a garment config and WRITES a self-contained bridge script that
`tools/md_bridge.py --file` executes inside MD. So it runs headless, in CI, with no
session, and the emitted script is a reviewable artifact in `work/<name>/`.
python tools/tailor/draft_garment.py --config work/pari/resolved.json \
--emit work/pari/md_script.py
python tools/md_bridge.py --file work/pari/md_script.py --timeout 500
Replaces (which stay in place until this is proven on a live session):
`md_pari.py`, `md_piupiu.py`, `md_tee_v1.py`, `md_skirt_v1.py`. Their identical
~100% shell is the template below; only panels/seams/arrangements/fabric differed.
`md_recon.py` is an API-introspection tool and does NOT fold in here.
--------------------------------------------------------------------------------
THE `md` CONFIG BLOCK
--------------------------------------------------------------------------------
Everything is optional except `panels`. Relative paths resolve against the config
file's directory, then the repo root; emitted paths are always absolute (MD needs
absolute paths).
```jsonc
"md": {
// --- scene -------------------------------------------------------------
"reset": "new_project", // "new_project" (default) | "clear_patterns" | "none"
// new_project = utility_api.NewProject() [DELETES the avatar]
// clear_patterns = delete every pattern, keep the loaded avatar
"avatar_fbx": "C:/.../Lena_QuatSkin_Avatar.fbx", // omit to keep the avatar already in the scene
"avatar_scale": 10.0, // Blender-exported FBX lands 10x small; default 10.0
"add_arrangement_points": true,// default true -- without it there is nowhere to hang cloth
"auto_translate": true, // default true
// --- fabric ------------------------------------------------------------
"zfab": "C:/Users/Public/Documents/MarvelousDesigner/New Assets/Fabric/(Default for Simulation).zfab",
"texture": "C:/.../textures/taniko.png", // optional; DPI in the PNG sets physical size
"texture_dpi": 96.012, // optional; VERIFIED against the PNG at emit time
"base_color": [0.13, 0.3, 0.75, 1.0], // optional RGBA; alternative to `texture`
// (fabric calls are emitted only when `zfab` is set)
// --- pattern -----------------------------------------------------------
"panels": [ // 2D pattern, millimetres, +y is UP in the 3D mapping
{"name": "front", "dx": 0.0, "dy": 0.0, "note": "front scoop floor = band top",
"points": [[65.0, 325.0], [155.0, 350.0], ...], // line i = point[i] -> point[i+1]
"particle_distance": 20.0, // optional; sim mesh resolution (mm) -- light/gappy panels
"thickness_collision": 3.0} // optional; stand-off from the skin (mm), set BEFORE the settle
],
"seams": [ // whole-edge pairs only
{"a": "front", "b": "back", "lines": [0, 5, 7, 9]}, // SAME index both panels (safe form)
{"a": "front", "a_line": 1, "b": "back", "b_line": 3, // explicit form
"reverse_a": false, "reverse_b": false}
],
"seam_check": true, // default true: fingerprint every seam pair with GetLineLength
// and FAIL the draft on any inequality > 0.1 mm -- paired edges
// must be EQUAL, not close (a 0.075 mm "rounding" gap was a
// slanted edge that notched a waistband)
"arrangements": [ // looked up BY NAME at run time, never by index
{"panel": "front", "point": "Body_Front_Center_1", "offset": [50, 55, 50]}
],
// --- drape -------------------------------------------------------------
"sim": {
"strengthen": true, // true = every panel | ["front"] = those panels | false/absent = none
"settle_frames": 250, // main settle, cloth still stiff
"elastic": [ // optional; applied MID-SETTLE (after settle_frames), never frame 0
{"panel": "front", "line": 0, "total_length": 450.0, "strength": null}
],
"elastic_frames": 80, // sim frames after enabling elastic (default 80 when elastic set)
"relax_frames": 50 // 0/absent = no relax pass (strengthen stays as-is)
},
"cam_viewpoint": 2, // optional utility_api.SetCamViewPoint(2) = front, before snapshots
"presim_snapshot": "C:/.../pari_presim.png", // optional; THE diagnostic (see lessons below)
"snapshot": "C:/.../tinqs_md_pari_v2.png", // ExportSnapshot3D target for the drape stage
"back_snapshot": "C:/.../pari_back.png", // optional; rear view via ExportTurntableImages(4)
// -- ExportSnapshot3D CANNOT show the back at all
// --- publish -----------------------------------------------------------
"export_dir": "C:/Users/Jeremy/tinqs/animation/tools/tailor", // default: tools/tailor/
"export_basename": "lena_pari_v3", // versioned; <base>.zprj / <base>_garment.fbx /
// <base>_garment.obj / <base>.zpac
"exports": ["zprj", "fbx", "obj", "zpac"] // default: all four
}
```
--------------------------------------------------------------------------------
STAGES (`--stage`, matching the contract's three MD stages)
--------------------------------------------------------------------------------
| stage | emits |
|-----------|------------------------------------------------------------------------|
| `draft` | reset, avatar import, panels, seams, fabric, arrangement assignment |
| | (+ `ResetClothArrangement` and a 0-frame snapshot iff `presim_snapshot`)|
| `drape` | strengthen, `ResetClothArrangement`, `Simulate`, relax, `Refresh3DWindow`, `ExportSnapshot3D` |
| `publish` | `ExportZPrj` / `ExportFBX` / `ExportOBJ` / `ExportZPac` |
| `all` | all three, in one script -- **the default, and the recommended path** |
Split stages work because MD's exec namespace persists WITHIN a session: `draft`
stores `{"garment", "patterns", "fabric"}` in a `TINQS_GARMENT` dict that `drape`
and `publish` read back, and they fail loudly if it is missing or belongs to a
different garment. It does NOT persist across sessions, so a session restart means
re-running `draft`. `--stage all` sidesteps all of that and reproduces exactly the
call order of the four originals -- prefer it unless you are iterating on a drape.
--------------------------------------------------------------------------------
HARD-WON MD FACTS (also emitted into every generated script)
--------------------------------------------------------------------------------
- 2D pattern space: units are mm, +y is UP in the 3D mapping. Straps/neck at HIGH
y, hem at y=0. (Panels built y-down drape upside-down over the head and tangle
-- it looks like a seam bug, it isn't. Seven "seam" iterations were this.)
- MD internal 3D unit is mm (gravity default -9800). Blender-exported FBX avatars
land 10x small -- import with op.scale = 10.
- ImportAvatar() is .avt ONLY and returns False on FBX; use import_api.ImportFBX.
- op.bAddArrangementPoints = True auto-generates ~98 named arrangement points.
- CreatePatternWithPoints takes (x, y, type) triples; type 0 = corner. The returned
id is the pattern index; line i = edge point[i] -> point[i+1].
- Seams: AddSeamlinePairGroup(patA, lineA, patB, lineB, False, False) with the SAME
line index on mirrored front/back panels; cross-pairing sews the garment over the
face. Whole-edge sewing only -> build neck gaps and armholes into the outline as
extra points, not as partial seams.
- SetArrangement() only ASSIGNS; utility_api.ResetClothArrangement() APPLIES it
(ReDrape3DArrangement only materializes not-yet-draped cloth).
- Arrangement INDICES REGENERATE on every avatar import -- always look up by name
from GetArrangementList(). Offsets aren't stable either: verify with a 0-frame
snapshot.
- utility_api.NewProject() DELETES the avatar -- re-import after.
- fabric_api.AddFabric() needs a .zfab FILE PATH; a name string silently fails.
Fabric index 0 is the shared default -- coloring it dyes every garment.
- SetBaseTextureMapImageGivenFilePath(path, fabricIdx) -- PATH IS ARG 0.
- AssignFabricToPattern() returns False for every arg order tried; use
pattern_api.SetPatternPieceFabricIndex(pattern, fabric).
- PNG DPI sets a texture's physical size in MD (1024 px @ 54.2 dpi = 480 mm).
Control tiling by setting dpi in PIL, not by scaling the image.
- SetViewPoint() does nothing; utility_api.SetCamViewPoint(2) = front view.
- Strengthen through the WHOLE settle, relax only at the end -- soft fabric from
frame 0 rolls into a bunch at the waist.
- Elastic mid-settle, never from the start; bottoms stay up by TENSION (waist cut
smaller than hips), not elastic.
- One garment per scene: a second garment, even frozen, grabs and inverts the new one.
- utility_api.Simulate(n) is synchronous, ~1 min per 300 frames -- raise the bridge
client --timeout accordingly.
- GetClothPositions() stays empty from Python: there is NO mesh introspection, so
every drape judgement is image-based (that is what gate G1 measures).
Exit codes: 0 emitted · 2 invalid config · 3 could not write.
"""
import argparse
import datetime
import json
import os
import sys
STAGES = ("draft", "drape", "publish", "all")
EXPORT_KINDS = ("zprj", "fbx", "obj", "zpac")
# three dirnames: this file lives at <repo>/tools/tailor/draft_garment.py
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DEFAULT_EXPORT_DIR = os.path.join(REPO_ROOT, "tools", "tailor")
# Emitted verbatim into every generated script -- the lessons must travel with the
# code a human actually reads in work/<name>/md_script.py, not only live here.
LESSONS = """\
# HARD-WON MD FACTS (do not rediscover -- see tools/tailor/draft_garment.py):
# - 2D pattern space: units are mm, +y is UP in the 3D mapping. Straps/neck at
# HIGH y, hem at y=0. Panels built y-down drape upside-down over the head and
# tangle -- it looks like a seam bug, it isn't.
# - MD's 3D unit is mm (gravity -9800). Blender-exported FBX avatars land 10x
# small -- import with op.scale = 10. ImportAvatar() is .avt only (False on FBX).
# - op.bAddArrangementPoints = True generates the ~98 named arrangement points;
# without it there is nowhere to hang cloth.
# - CreatePatternWithPoints takes (x, y, type) triples, type 0 = corner. Returned
# id is the pattern index; line i = edge point[i] -> point[i+1].
# - Seams are WHOLE-EDGE only: AddSeamlinePairGroup(a, lineA, b, lineB, False,
# False) with the SAME line index on mirrored front/back panels. Cross-pairing
# sews the garment over the face. Neck gaps and armholes are extra points in
# the outline, not partial seams.
# - SetArrangement() only ASSIGNS; utility_api.ResetClothArrangement() APPLIES it
# (ReDrape3DArrangement only materializes not-yet-draped cloth).
# - Arrangement INDICES REGENERATE on every avatar import -- always look up by
# NAME from GetArrangementList(). Offsets aren't stable either; verify with a
# 0-frame snapshot (the diagnostic that unsticks everything).
# - utility_api.NewProject() DELETES the avatar -- re-import after.
# - AddFabric() needs a .zfab FILE PATH (a name string silently fails); fabric 0
# is the shared default, coloring it dyes every garment. Assign with
# SetPatternPieceFabricIndex -- AssignFabricToPattern() never worked.
# - SetBaseTextureMapImageGivenFilePath(PATH, fabricIdx) -- path is arg 0.
# - PNG DPI sets a texture's physical size in MD; control tiling via dpi in PIL.
# - Strengthen through the WHOLE settle, relax only at the end; elastic (if any)
# mid-settle, never from frame 0. One garment per scene.
# - Simulate(n) is synchronous, ~1 min per 300 frames -- raise the client timeout.
# - GetClothPositions() stays empty: no mesh introspection, QC is image-based.
"""
class ConfigError(Exception):
"""Raised for anything the `md` block gets wrong -- exits 2."""
# --------------------------------------------------------------------------- io
def _load_config(path):
with open(path, "r", encoding="utf-8") as fh:
return json.load(fh)
def _resolve(path, config_dir):
"""Config paths -> absolute. Try as-given, then config-dir-relative, then repo root."""
if not path:
return path
p = os.path.expanduser(str(path))
if os.path.isabs(p):
return os.path.normpath(p)
for base in (config_dir, REPO_ROOT):
cand = os.path.normpath(os.path.join(base, p))
if os.path.exists(cand):
return cand
return os.path.normpath(os.path.join(config_dir, p))
def _warn(msg):
print("warning: {}".format(msg), file=sys.stderr)
# ---------------------------------------------------------------------- schema
def _ident(name, used):
"""Panel name -> a safe, unique python identifier for the generated script."""
safe = "".join(c if (c.isalnum() or c == "_") else "_" for c in str(name))
if not safe or safe[0].isdigit():
safe = "p_" + safe
base = "p_" + safe
out, n = base, 2
while out in used:
out, n = "{}_{}".format(base, n), n + 1
used.add(out)
return out
def parse_md(config, config_path):
"""Validate + normalise the `md` block. Raises ConfigError. Returns a plain dict."""
config_dir = os.path.dirname(os.path.abspath(config_path))
md = config.get("md")
if not isinstance(md, dict):
raise ConfigError("config has no `md` block (this garment has no MD upstream stage)")
spec = {"name": config.get("name") or os.path.splitext(os.path.basename(config_path))[0]}
# --- scene
reset = md.get("reset", "new_project")
if reset not in ("new_project", "clear_patterns", "none"):
raise ConfigError("md.reset must be new_project|clear_patterns|none, got {!r}".format(reset))
spec["reset"] = reset
spec["avatar_fbx"] = _resolve(md.get("avatar_fbx"), config_dir)
if spec["avatar_fbx"] and not os.path.exists(spec["avatar_fbx"]):
_warn("md.avatar_fbx does not exist: {}".format(spec["avatar_fbx"]))
if reset == "new_project" and not spec["avatar_fbx"]:
_warn("md.reset=new_project DELETES the avatar and no md.avatar_fbx is set -- "
"the garment will drape on nothing")
spec["avatar_scale"] = float(md.get("avatar_scale", 10.0))
spec["add_arrangement_points"] = bool(md.get("add_arrangement_points", True))
spec["auto_translate"] = bool(md.get("auto_translate", True))
# --- fabric
spec["zfab"] = _resolve(md.get("zfab"), config_dir)
if spec["zfab"] and not os.path.exists(spec["zfab"]):
_warn("md.zfab does not exist: {} (AddFabric needs a real .zfab path)".format(spec["zfab"]))
spec["texture"] = _resolve(md.get("texture"), config_dir)
spec["texture_dpi"] = md.get("texture_dpi")
if spec["texture"]:
if not spec["zfab"]:
raise ConfigError("md.texture set without md.zfab -- fabric index 0 is the shared "
"default and texturing it dyes every garment in the scene")
_check_texture(spec["texture"], spec["texture_dpi"])
color = md.get("base_color")
if color is not None:
if len(color) not in (3, 4):
raise ConfigError("md.base_color must be [r,g,b] or [r,g,b,a]")
color = [float(c) for c in color]
if len(color) == 3:
color.append(1.0)
spec["base_color"] = color
# --- panels
panels = md.get("panels")
if not isinstance(panels, list) or not panels:
raise ConfigError("md.panels must be a non-empty list")
used, seen = set(), set()
spec["panels"] = []
for i, raw in enumerate(panels):
if not isinstance(raw, dict):
raise ConfigError("md.panels[{}] must be an object".format(i))
name = raw.get("name") or "panel{}".format(i)
if name in seen:
raise ConfigError("duplicate panel name {!r}".format(name))
seen.add(name)
pts = raw.get("points")
if not isinstance(pts, list) or len(pts) < 3:
raise ConfigError("panel {!r} needs at least 3 points".format(name))
clean = []
for j, pt in enumerate(pts):
if len(pt) < 2:
raise ConfigError("panel {!r} point {} must be [x, y]".format(name, j))
clean.append((float(pt[0]), float(pt[1])))
spec["panels"].append({
"name": name, "var": _ident(name, used), "points": clean,
"dx": float(raw.get("dx", 0.0)), "dy": float(raw.get("dy", 0.0)),
"point_type": int(raw.get("point_type", 0)),
"note": raw.get("note", ""),
"particle_distance": (None if raw.get("particle_distance") is None
else float(raw["particle_distance"])),
"thickness_collision": (None if raw.get("thickness_collision") is None
else float(raw["thickness_collision"])),
})
by_name = {p["name"]: p for p in spec["panels"]}
# --- seams
spec["seams"] = []
for i, raw in enumerate(md.get("seams", []) or []):
a, b = raw.get("a"), raw.get("b")
for who in (a, b):
if who not in by_name:
raise ConfigError("md.seams[{}] references unknown panel {!r}".format(i, who))
ra, rb = bool(raw.get("reverse_a", False)), bool(raw.get("reverse_b", False))
if "lines" in raw:
pairs = [(int(n), int(n)) for n in raw["lines"]]
elif "a_line" in raw and "b_line" in raw:
pairs = [(int(raw["a_line"]), int(raw["b_line"]))]
else:
raise ConfigError("md.seams[{}] needs `lines` or both `a_line`/`b_line`".format(i))
na, nb = len(by_name[a]["points"]), len(by_name[b]["points"])
for la, lb in pairs:
if not 0 <= la < na:
raise ConfigError("seam line {} out of range for panel {!r} ({} lines)"
.format(la, a, na))
if not 0 <= lb < nb:
raise ConfigError("seam line {} out of range for panel {!r} ({} lines)"
.format(lb, b, nb))
if a != b and la != lb:
_warn("seam {}.{} <-> {}.{} cross-pairs different line indices; on mirrored "
"front/back panels that sews the garment over the face"
.format(a, la, b, lb))
spec["seams"].append({"a": by_name[a], "a_line": la,
"b": by_name[b], "b_line": lb,
"reverse_a": ra, "reverse_b": rb})
spec["seam_check"] = bool(md.get("seam_check", True))
# --- arrangements
spec["arrangements"] = []
for i, raw in enumerate(md.get("arrangements", []) or []):
panel = raw.get("panel")
if panel not in by_name:
raise ConfigError("md.arrangements[{}] references unknown panel {!r}".format(i, panel))
point = raw.get("point")
if not point:
raise ConfigError("md.arrangements[{}] needs an arrangement point `point` name "
"(indices regenerate; names are the only stable handle)".format(i))
off = raw.get("offset")
if off is not None and len(off) != 3:
raise ConfigError("md.arrangements[{}].offset must be [x, y, z]".format(i))
spec["arrangements"].append({
"panel": by_name[panel], "point": str(point),
"offset": None if off is None else [_num(v) for v in off],
})
# --- sim
sim = md.get("sim") or {}
strengthen = sim.get("strengthen", False)
if strengthen is True:
targets = [p["name"] for p in spec["panels"]]
elif strengthen in (False, None):
targets = []
elif isinstance(strengthen, list):
for n in strengthen:
if n not in by_name:
raise ConfigError("md.sim.strengthen references unknown panel {!r}".format(n))
targets = list(strengthen)
else:
raise ConfigError("md.sim.strengthen must be true, false, or a list of panel names")
spec["strengthen"] = [by_name[n] for n in targets]
spec["settle_frames"] = int(sim.get("settle_frames", sim.get("frames", 250)))
spec["relax_frames"] = int(sim.get("relax_frames", 0) or 0)
if spec["relax_frames"] and not spec["strengthen"]:
_warn("md.sim.relax_frames is set but nothing is strengthened -- the relax pass only "
"makes sense as the 'stiff settle, then soften' recipe")
# --- elastic (mid-settle, NEVER frame 0 -- elastic-first drapes slide off a hip)
spec["elastic"] = []
for i, raw in enumerate(sim.get("elastic", []) or []):
panel = raw.get("panel")
if panel not in by_name:
raise ConfigError("md.sim.elastic[{}] references unknown panel {!r}".format(i, panel))
line = int(raw.get("line", -1))
if not 0 <= line < len(by_name[panel]["points"]):
raise ConfigError("md.sim.elastic[{}] line {} out of range for panel {!r}"
.format(i, line, panel))
if raw.get("total_length") is None:
raise ConfigError("md.sim.elastic[{}] needs total_length (mm) -- the target "
"cinched length of the edge".format(i))
spec["elastic"].append({
"panel": by_name[panel], "line": line,
"total_length": float(raw["total_length"]),
"strength": (None if raw.get("strength") is None else float(raw["strength"])),
})
spec["elastic_frames"] = int(sim.get("elastic_frames", 80)) if spec["elastic"] else 0
# --- snapshots / camera
spec["cam_viewpoint"] = md.get("cam_viewpoint")
spec["presim_snapshot"] = _resolve(md.get("presim_snapshot"), config_dir)
spec["snapshot"] = _resolve(md.get("snapshot"), config_dir)
spec["back_snapshot"] = _resolve(md.get("back_snapshot"), config_dir)
if not spec["snapshot"]:
_warn("md.snapshot is not set -- the drape stage will produce no image, and gate G1 "
"(drape placement) has nothing to measure")
# --- publish
spec["export_dir"] = _resolve(md.get("export_dir") or DEFAULT_EXPORT_DIR, config_dir)
spec["export_basename"] = md.get("export_basename")
kinds = md.get("exports", list(EXPORT_KINDS))
bad = [k for k in kinds if k not in EXPORT_KINDS]
if bad:
raise ConfigError("md.exports has unknown kinds {}; valid: {}"
.format(bad, list(EXPORT_KINDS)))
spec["exports"] = list(kinds)
if spec["exports"] and not spec["export_basename"]:
raise ConfigError("md.export_basename is required to publish (use a versioned name "
"like lena_pari_v3 -- never overwrite a shipped export)")
return spec
def _num(v):
f = float(v)
return int(f) if f == int(f) else f
def _check_texture(path, declared_dpi):
"""DPI is the texture's physical size in MD. Verify the PNG really carries it."""
if not os.path.exists(path):
_warn("md.texture does not exist: {}".format(path))
return
try:
from PIL import Image
except ImportError:
_warn("PIL not available -- skipping the md.texture_dpi check")
return
try:
with Image.open(path) as im:
size, dpi = im.size, im.info.get("dpi")
except Exception as exc: # noqa: BLE001
_warn("could not read md.texture {}: {}".format(path, exc))
return
if declared_dpi is None:
return
if not dpi:
_warn("md.texture {} carries no DPI but md.texture_dpi={} is declared -- MD will size "
"the cloth from the file, not from the config".format(path, declared_dpi))
return
if abs(float(dpi[0]) - float(declared_dpi)) > 0.05:
_warn("md.texture DPI mismatch: {} has {:.3f} dpi, config declares {} "
"(1024 px @ {:.3f} dpi = {:.0f} mm of cloth)"
.format(path, float(dpi[0]), declared_dpi, float(dpi[0]),
size[0] / float(dpi[0]) * 25.4))
# -------------------------------------------------------------------- emission
def _r(path):
"""Windows path -> a python raw-string literal for the generated script."""
return 'r"{}"'.format(str(path).replace('"', '\\"'))
def emit(spec, stage, config_path):
"""Render the bridge script. `stage` in draft|drape|publish|all."""
want = {"draft", "drape", "publish"} if stage == "all" else {stage}
L = []
a = L.append
a("# GENERATED by tools/tailor/draft_garment.py -- DO NOT EDIT.")
a("# garment : {}".format(spec["name"]))
a("# stage : {}".format(stage))
a("# config : {}".format(config_path))
a("# emitted : {}".format(datetime.datetime.now().replace(microsecond=0).isoformat()))
a("# Edit the config's `md` block and re-emit; edits here are lost.")
a("#")
a("# Run inside an MD bridge session (a human must click Plugin > TinqsMDBridge first):")
a("# python tools/md_bridge.py --file <this file> --timeout {}"
.format(_timeout_hint(spec)))
a("#")
a(LESSONS.rstrip())
a("")
a("import os")
a("")
a("report = {{\"garment\": {!r}, \"stage\": {!r}}}".format(spec["name"], stage))
a("")
if "draft" in want:
L.extend(_emit_draft(spec))
else:
L.extend(_emit_state_load(spec))
if "drape" in want:
L.extend(_emit_drape(spec))
if "publish" in want:
L.extend(_emit_publish(spec))
a("result = report")
return "\n".join(L) + "\n"
def _emit_state_load(spec):
"""drape/publish run in a later bridge request: recover the pattern ids draft stored."""
return [
"# The exec namespace persists WITHIN an MD session, so `draft` handed these over.",
"# It does NOT persist across sessions: if MD was restarted, re-run --stage draft.",
"if \"TINQS_GARMENT\" not in globals():",
" raise RuntimeError(\"no TINQS_GARMENT in this MD session -- run --stage draft \"",
" \"first, or emit --stage all\")",
"if TINQS_GARMENT.get(\"garment\") != {!r}:".format(spec["name"]),
" raise RuntimeError(\"this MD session holds garment %r, not {} -- re-run \"".format(
spec["name"]),
" \"--stage draft\" % TINQS_GARMENT.get(\"garment\"))",
"_pat = TINQS_GARMENT[\"patterns\"]",
] + [
"{} = _pat[{!r}]".format(p["var"], p["name"]) for p in spec["panels"]
] + [
"fab = TINQS_GARMENT.get(\"fabric\")",
"",
]
def _emit_draft(spec):
L = []
a = L.append
a("# ---- scene ---------------------------------------------------------------")
if spec["reset"] == "new_project":
a("utility_api.NewProject() # NOTE: this DELETES the avatar; re-import below")
elif spec["reset"] == "clear_patterns":
a("# keep the loaded avatar, drop every pattern (delete back-to-front: ids shift)")
a("for _i in range(pattern_api.GetPatternCount() - 1, -1, -1):")
a(" pattern_api.DeletePatternPiece(_i)")
if spec["avatar_fbx"]:
a("AVATAR_FBX = {}".format(_r(spec["avatar_fbx"])))
a("op = ApiTypes.ImportExportOption()")
a("op.scale = {} # Blender-exported FBX lands 10x small"
.format(_fmt(spec["avatar_scale"])))
if spec["add_arrangement_points"]:
a("op.bAddArrangementPoints = True # ~98 named points to hang cloth on")
if spec["auto_translate"]:
a("op.bAutoTranslate = True")
a("report[\"avatar\"] = import_api.ImportFBX(AVATAR_FBX, op) "
"# ImportAvatar() is .avt only")
a("")
a("# ---- pattern (mm; +y is UP in 3D -- hem at y=0, neck/straps at high y) ----")
for p in spec["panels"]:
if p["note"]:
a("# {}: {}".format(p["name"], p["note"]))
a("_pts_{} = [".format(p["var"]))
for chunk in _chunk(p["points"], 3):
a(" " + " ".join("({}, {}),".format(_fmt(x), _fmt(y)) for x, y in chunk))
a("]")
a("{} = pattern_api.CreatePatternWithPoints("
"[(x + {}, y + {}, {}) for (x, y) in _pts_{}])"
.format(p["var"], _fmt(p["dx"]), _fmt(p["dy"]), p["point_type"], p["var"]))
if p["particle_distance"] is not None:
a("pattern_api.SetParticleDistanceOfPattern({}, {}) # sim mesh resolution (mm)"
.format(p["var"], _fmt(p["particle_distance"])))
if p["thickness_collision"] is not None:
a("pattern_api.SetAddlThicknessCollision({}, {}) "
"# skin stand-off; the sim resolves it, set BEFORE the settle"
.format(p["var"], _fmt(p["thickness_collision"])))
a("report[\"ids\"] = [{}]".format(", ".join(p["var"] for p in spec["panels"])))
a("")
if spec["seams"]:
a("# ---- seams (WHOLE-EDGE only; same line index on mirrored panels) ---------")
for s in spec["seams"]:
a("pattern_api.AddSeamlinePairGroup({}, {}, {}, {}, {}, {})".format(
s["a"]["var"], s["a_line"], s["b"]["var"], s["b_line"],
s["reverse_a"], s["reverse_b"]))
a("")
if spec.get("seam_check", True):
a("# Paired edges must be EQUAL, not close: a 0.075 mm 'rounding' difference was")
a("# a slanted edge that notched a waistband. Fail loudly BEFORE wasting a sim.")
a("_pairs = [{}]".format(", ".join(
"({}, {}, {}, {})".format(s["a"]["var"], s["a_line"], s["b"]["var"], s["b_line"])
for s in spec["seams"])))
a("report[\"seam_lengths\"] = []")
a("_bad = []")
a("for _pa, _la, _pb, _lb in _pairs:")
a(" _fa = pattern_api.GetLineLength(_pa, _la)")
a(" _fb = pattern_api.GetLineLength(_pb, _lb)")
a(" report[\"seam_lengths\"].append((_pa, _la, round(_fa, 3), _pb, _lb, round(_fb, 3)))")
a(" if abs(_fa - _fb) > 0.1:")
a(" _bad.append(\"%s.%s=%.3f vs %s.%s=%.3f\" % (_pa, _la, _fa, _pb, _lb, _fb))")
a("if _bad:")
a(" raise RuntimeError(\"seam length mismatch (construction bug, not rounding): \"")
a(" + \"; \".join(_bad))")
a("")
if spec["zfab"]:
a("# ---- fabric (never touch index 0: it is the scene-wide default) ---------")
a("ZFAB = {}".format(_r(spec["zfab"])))
a("fab = fabric_api.AddFabric(ZFAB) # a name string silently fails")
if spec["texture"]:
dpi = " (dpi {} -> physical size)".format(spec["texture_dpi"]) \
if spec["texture_dpi"] else ""
a("TEX = {}{}".format(_r(spec["texture"]), dpi and " #" + dpi or ""))
a("fabric_api.SetBaseTextureMapImageGivenFilePath(TEX, fab) # path is arg 0")
if spec["base_color"]:
a("fabric_api.SetFabricPBRMaterialBaseColor(fab, 0, {})".format(
", ".join(_fmt(c) for c in spec["base_color"])))
a("for _p in ({},):".format(", ".join(p["var"] for p in spec["panels"])))
a(" pattern_api.SetPatternPieceFabricIndex(_p, fab) "
"# AssignFabricToPattern never worked")
a("")
else:
a("fab = None")
a("")
if spec["arrangements"]:
a("# ---- arrangement (BY NAME: indices regenerate on every avatar import) ----")
a("arr = {a[\"ArrangementName\"]: int(a[\"ArrangementIndex\"])")
a(" for a in pattern_api.GetArrangementList()}")
for ar in spec["arrangements"]:
a("pattern_api.SetArrangement({}, arr[{!r}])".format(ar["panel"]["var"], ar["point"]))
for ar in spec["arrangements"]:
if ar["offset"] is not None:
a("pattern_api.SetArrangementPosition({}, {})".format(
ar["panel"]["var"], ", ".join(_fmt(v) for v in ar["offset"])))
a("")
a("# Hand the pattern ids to a later --stage drape/publish in the SAME session.")
a("TINQS_GARMENT = {{\"garment\": {!r}, \"fabric\": fab, \"patterns\": {{{}}}}}".format(
spec["name"], ", ".join("{!r}: {}".format(p["name"], p["var"]) for p in spec["panels"])))
a("")
if spec["presim_snapshot"]:
a("# THE diagnostic: apply the arrangement and shoot with ZERO sim frames. It shows")
a("# where the panels actually start, before physics muddies it. Reach for this the")
a("# moment a drape misbehaves -- upside-down panels look exactly like seam bugs.")
a("utility_api.ResetClothArrangement()")
L.extend(_emit_snapshot(spec, spec["presim_snapshot"], "presim_snap"))
a("")
return L
def _emit_drape(spec):
L = []
a = L.append
a("# ---- drape ---------------------------------------------------------------")
if spec["strengthen"]:
a("# Stiffen through the WHOLE settle: soft fabric from frame 0 rolls into a bunch.")
for p in spec["strengthen"]:
a("pattern_api.SetPatternStrengthen({}, True)".format(p["var"]))
a("utility_api.ResetClothArrangement() # SetArrangement only assigns; THIS applies it")
a("report[\"sim1\"] = utility_api.Simulate({}) # synchronous, ~1 min / 300 frames"
.format(spec["settle_frames"]))
if spec["elastic"]:
a("# Elastic MID-SETTLE, never from frame 0 -- elastic-first drapes cinch the")
a("# garment off one hip before the cloth has wrapped.")
for e in spec["elastic"]:
a("pattern_api.SetPatternPieceElastic({}, {}, True)".format(
e["panel"]["var"], e["line"]))
a("pattern_api.SetPatternPieceElasticTotalLength({}, {}, {})".format(
e["panel"]["var"], e["line"], _fmt(e["total_length"])))
if e["strength"] is not None:
a("pattern_api.SetPatternPieceElasticStrength({}, {}, {})".format(
e["panel"]["var"], e["line"], _fmt(e["strength"])))
a("report[\"sim_elastic\"] = utility_api.Simulate({})".format(spec["elastic_frames"]))
if spec["relax_frames"]:
a("# Relax only at the end, so the settled shape falls into natural folds.")
for p in spec["strengthen"]:
a("pattern_api.SetPatternStrengthen({}, False)".format(p["var"]))
a("report[\"sim2\"] = utility_api.Simulate({})".format(spec["relax_frames"]))
a("utility_api.Refresh3DWindow()")
if spec["snapshot"]:
L.extend(_emit_snapshot(spec, spec["snapshot"], "snap"))
if spec["back_snapshot"]:
a("# ExportSnapshot3D has NO rear view (SetCamViewPoint 0-7: none is the back).")
a("# Turntable ignores any path arg and reuses the same output names -- copy now.")
a("import shutil")
a("_tt = export_api.ExportTurntableImages(4) # 0 front, 1 side, 2 BACK, 3 side")
a("if _tt and len(_tt) > 2:")
a(" back_snap = {}".format(_r(spec["back_snapshot"])))
a(" os.makedirs(os.path.dirname(back_snap), exist_ok=True)")
a(" shutil.copyfile(_tt[2], back_snap)")
a(" report[\"back_snapshot\"] = back_snap")
a("else:")
a(" report[\"back_snapshot\"] = \"TURNTABLE RETURNED NOTHING -- rear unverified\"")
a("")
return L
def _emit_snapshot(spec, path, key):
L = []
a = L.append
if spec["cam_viewpoint"] is not None:
a("utility_api.SetCamViewPoint({}) # SetViewPoint() does nothing; 2 = front"
.format(spec["cam_viewpoint"]))
a("{} = {}".format(key, _r(path)))
a("os.makedirs(os.path.dirname({}), exist_ok=True)".format(key))
a("export_api.ExportSnapshot3D({})".format(key))
a("report[{!r}] = {}".format(key, key))
return L
def _emit_publish(spec):
L = []
a = L.append
if not spec["exports"]:
return L
base = os.path.join(spec["export_dir"], spec["export_basename"])
a("# ---- publish (all scriptable; the FBX/OBJ flags are the load-bearing part) -")
a("os.makedirs({}, exist_ok=True)".format(_r(spec["export_dir"])))
a("xop = ApiTypes.ImportExportOption()")
a("xop.bExportGarment = True")
a("xop.bExportAvatar = False # the avatar is the game body; ship cloth only")
if "zprj" in spec["exports"]:
a("report[\"zprj\"] = {}".format(_r(base + ".zprj")))
a("export_api.ExportZPrj(report[\"zprj\"]) "
"# editable source of truth; no SaveProjectFile exists")
if "fbx" in spec["exports"]:
a("report[\"fbx\"] = {}".format(_r(base + "_garment.fbx")))
a("export_api.ExportFBX(report[\"fbx\"], xop) # -> clothing/garment_pipeline.py")
if "obj" in spec["exports"]:
a("report[\"obj\"] = {}".format(_r(base + "_garment.obj")))
a("export_api.ExportOBJ(report[\"obj\"], xop)")
if "zpac" in spec["exports"]:
a("report[\"zpac\"] = {}".format(_r(base + ".zpac")))
a("export_api.ExportZPac(report[\"zpac\"]) "
"# for outfit assembly (ImportZpac op.bAdd=True)")
a("")
return L
def _fmt(v):
if isinstance(v, bool):
return "True" if v else "False"
f = float(v)
return repr(int(f)) if f == int(f) and abs(f) < 1e15 and isinstance(v, int) else repr(f)
def _chunk(seq, n):
for i in range(0, len(seq), n):
yield seq[i:i + n]
def _timeout_hint(spec):
"""~1 min per 300 sim frames, plus import/export headroom."""
frames = spec["settle_frames"] + spec["relax_frames"] + spec.get("elastic_frames", 0)
return max(120, int(frames / 300.0 * 60.0 * 2.5) + 120)
# ----------------------------------------------------------------------- entry
def main(argv=None):
ap = argparse.ArgumentParser(
description="Emit a Marvelous Designer bridge script from a garment config's `md` block.")
ap.add_argument("--config", required=True,
help="resolved.json (or any garment config carrying an `md` block)")
ap.add_argument("--emit", required=True,
help="output path for the generated bridge script ('-' = stdout)")
ap.add_argument("--stage", default="all", choices=STAGES,
help="which MD stage(s) to emit (default: %(default)s)")
ap.add_argument("--timeout-hint", action="store_true",
help="print only the suggested md_bridge.py --timeout and exit")
args = ap.parse_args(argv)
try:
config = _load_config(args.config)
except (OSError, ValueError) as exc:
print("error: could not read config {}: {}".format(args.config, exc), file=sys.stderr)
return 3
try:
spec = parse_md(config, args.config)
except ConfigError as exc:
print("error: {}".format(exc), file=sys.stderr)
return 2
if args.timeout_hint:
print(_timeout_hint(spec))
return 0
text = emit(spec, args.stage, os.path.abspath(args.config))
try:
compile(text, args.emit, "exec")
except SyntaxError as exc:
print("error: generated script does not parse ({}) -- this is a template bug"
.format(exc), file=sys.stderr)
return 3
if args.emit == "-":
sys.stdout.write(text)
return 0
try:
out_dir = os.path.dirname(os.path.abspath(args.emit))
if out_dir:
os.makedirs(out_dir, exist_ok=True)
with open(args.emit, "w", encoding="utf-8") as fh:
fh.write(text)
except OSError as exc:
print("error: could not write {}: {}".format(args.emit, exc), file=sys.stderr)
return 3
print("emitted {} ({} stage, {} panels, {} seams) -> {}".format(
spec["name"], args.stage, len(spec["panels"]), len(spec["seams"]), args.emit))
print(" python tools/md_bridge.py --file {} --timeout {}".format(
args.emit, _timeout_hint(spec)))
return 0
if __name__ == "__main__":
sys.exit(main())
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_98912
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_125643
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

+24
View File
@@ -0,0 +1,24 @@
{
"source": "C:\\Users\\Jeremy\\tinqs\\ariki-game\\assets\\quaternius\\derived-bodies\\Ariki_Female_QuatSkin.glb",
"units": "meters (glTF)",
"height_total": 1.777,
"chest_circ": 1.0834,
"chest_z": 1.2636,
"waist_circ": 0.6747,
"waist_z": 1.0888,
"hip_circ": 1.0947,
"hip_z": 0.9455,
"thigh_circ": 0.8421,
"thigh_z": 0.8652,
"neck_circ": 0.3923,
"neck_z": 1.4568,
"bicep_circ": 0.417,
"shoulder_width": 0.3193,
"arm_len_shoulder_to_wrist": 0.5148,
"nape_to_pelvis": 0.4808,
"crotch_height": 0.9455,
"pelvis_height": 0.9318,
"knee_height": 0.5171,
"ankle_height": 0.1063,
"shoulder_z": 1.3973
}
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_19892
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
map_Ka C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
map_Kd C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_34682
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
map_Ka C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
map_Kd C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
newmtl (Default_for_Simulation)_FRONT_41289
Ka 0.257132 0.257132 0.257132
Kd 1.000000 1.000000 1.000000
Ks 0.075000 0.075000 0.075000
Ns 28.763239
illum 2
d 1.000000
map_Ka C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
map_Kd C:/Users/Jeremy/tinqs/animation/tools/tailor/taniko.png
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Some files were not shown because too many files have changed in this diff Show More