docs+tools: working-file policy, lane pruner, and a human MD explainer

Two pieces of work.

1. Keep milestones, not steps. Staged NN_*.py lanes were saving a full
   ~80 MB .blend per attempt, so Lena's lane reached 3.0 GB of which 2.2 GB
   was 30 .blend files -- five snapshots to land one crotch fix, five more
   for the bra. The .py recipes are the real history; the blends are cache.

   - .agents/rules/working-files.md: four-tier policy (KEEP / MASTER /
     SCRATCH / UNKNOWN) + how to work a lane.
   - tools/prune_lane.py: classifies a lane and prunes the scratch tier.
     Dry-run by default. Reads a per-lane .lanekeep manifest, flags
     binaries byte-identical to a registered original (canonical name
     always survives a duplicate pair), and never auto-deletes a .blend
     with no step script beside it -- those cannot be rebuilt.
   - .gitignore: scratch patterns can never be committed.

   Dry run on characters/female/lena_nude reports 2.3 GB reclaimable.
   Not applied -- that lane had a live Blender session at the time.

2. .humans/marvelous-designer.html: how we author garments in Marvelous
   Designer, written for people rather than agents -- the six-step process,
   what has been made, the traps that cost hours, and what is still
   unsolved. Matches the .humans/ HTML convention in ariki-game.

Also committing the docs the AGENTS.md knowledge map and the new page
reference, so they are not dangling: the clothing-lane architecture page,
the marvelous-designer skill, and the two screenshots the page embeds.

characters/ is deliberately untracked and stays that way -- it holds GBs of
blends and GLBs, and .gitattributes does not LFS-track .blend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 12:05:46 -07:00
parent 854dbb5ce4
commit d1030def4e
13 changed files with 1729 additions and 6 deletions
@@ -0,0 +1,214 @@
# Deconstruction — taking a reference apart like a tailor
Companion to `../SKILL.md`. That file is drafting/draping doctrine; **this file is
the step before it**: how to read a clothing image (or description) and take the
garment apart into pieces our MD lane can actually build. Skipping this step is
how garments shipped 414 cm off their own targets — the drafting was fine, the
*analysis* had never been written down.
The output is a **worksheet of numbers**, not geometry. Panel point lists come
last, and mostly from `tools/tailor/blocks.py`, not by hand.
---
## 0. The worksheet (output contract)
Fill this in full **before** asking for a bridge session. Every number the drape
and QC stages use traces back to a row here.
| # | Item | Feeds |
|---|---|---|
| 1 | Slot split — which separate garments is this outfit? | one config per garment |
| 2 | Per garment: class + block choice | `blocks.py` function |
| 3 | Anchoring — what holds it up | construction + arrangement |
| 4 | Placement targets — each edge's z ± tol vs landmarks | `expect.bands` (gate G1) |
| 5 | Edge circumference + ease table | panel widths, taper |
| 6 | Piece list + seam plan | `md.panels` / `md.seams` |
| 7 | Textile plan — what is texture, not geometry | texture generation |
| 8 | Fabric read — weight/stiffness | `.zfab` choice + sim recipe |
| 9 | Risks — unsolved/untested features | scope call before starting |
`blocks.py` turns rows 25 into a config skeleton (`md` block + `expect.bands`)
in one call. If no block fits, hand-draft under `tooling.md` §3 rules (named
landmarks, computed line indices) — never freehand a point list.
## 1. Split the outfit into garments by slot
The game wears **per-slot parts** (Body, Legs, …) and MD drapes **one garment per
scene** — a second garment, even frozen, grabs and inverts the new one. So a
"dress over leggings with a belt" reference is *three* worksheets, three configs,
three drape sessions. Decide the split first:
- One garment per clothing slot it occupies. A dress is one garment (Body slot,
or whatever slot spans it) even though it covers both regions.
- Belts, sashes, armbands: texture if flat against the host garment; separate
garment only if they hang or swing.
- Outfit photos are assembled at the end from finished `.zpac`s (SKILL.md
"Outfit assembly") — never co-draped.
## 2. Anchoring — decide what holds it up before what it looks like
Anchoring determines construction more than silhouette does. MD materialises
cloth at the arrangement point and does **not** simulate donning, so nothing
"slides into place".
| What the image shows | Anchor | Construction consequence |
|---|---|---|
| Straps, sleeves, or a shoulder line | shoulder-hung | bodice block; straps reach `shoulder_z` or the garment settles at the underbust |
| Strapless top | (unreliable) | **add straps anyway** — a tube slides to the narrowest catch; straps also cover the body's baked-in bra straps |
| Skirt/trousers at waist or hip | waist tension | cut the waist **smaller** than the hips (0.900.92 × hip circ) and let stretch hold it; elastic only mid-settle, never frame 0 |
| Light/gappy garment (strands, fringe, open weave) | arrangement itself | arrangement height **is** the placement — a light band stays exactly where arranged |
Always check what the anchor must *cover*: the target body has baked-in
underwear, and any neckline or scoop that drops below it reads as the garment
failing. Check the **back** neckline explicitly — the reference photo almost
never shows it, `ExportSnapshot3D` cannot show it (`tooling.md` §1.1), and that
blind spot has shipped defects. If the image doesn't show the back, *decide* the
back (scoop depth, coverage) and write it on the worksheet rather than letting it
default.
## 3. Read the seams, then discard most of them
A real tailor's deconstruction finds every seam. Ours finds them and then
**collapses almost all of them**, because the MD lane sews whole edges only, has
no darts, and garment identity for our targets is mostly textiles (SKILL.md).
Translation table:
| Feature in the image | Our move |
|---|---|
| Side seams, shoulder seams | keep — these are the block's real seams |
| Darts (bust, waist) | **no darts.** Taper the panel side edges + stretch ease carries the shaping |
| Princess seams, yokes | collapse into the panel; if visible, draw them in the texture |
| Waistband | merge into the panel top; separate band only for strand/fringe skirts |
| Set-in sleeves | **untested** — no sleeve block yet; treat as scope risk (row 9) |
| Collars, hoods | **untested/unsolved** — same |
| Trousers/shorts crotch | **unsolved** (failure catalogue); next approach is 4 panels on the per-leg points |
| Plackets, buttons, zips, pockets, topstitching | texture, always |
| Gathers, pleats, ruffles | texture unless the *silhouette* depends on them |
| Fringe / strands / fur edge | teeth cut into the panel outline (comb panel), never partial seams |
| Belt/sash flat against the garment | texture |
The test for "geometry or texture?": does it change the **silhouette** or the
**edge positions**? If not, it's texture.
## 4. Placement targets — numbers before points
Extract where every garment edge sits **before drafting anything**. This is the
step that was skipped when shipped garments landed 3.914 cm off targets that
existed only as prose.
Landmark card for the current body (`tools/tailor/lena_measurements.json`,
regenerate per body with `measure_body.py`; metres, floor = 0):
| Landmark | z | Landmark | z |
|---|---|---|---|
| top of head | 1.777 | hip (widest) / crotch | 0.946 |
| neck base | 1.457 | thigh | 0.865 |
| shoulder | 1.397 | knee | 0.517 |
| chest (bust) | 1.264 | ankle | 0.106 |
| waist | 1.089 | | |
Method, per garment edge (top of band, hem, waistline…):
1. Find the edge in the image relative to the two nearest **visible** landmarks
(e.g. "hem lands mid-thigh, about ⅓ of the knee→hip span above the knee").
2. Interpolate a z from the card. The body is stylized — always place against
*these* landmarks, never against real-world garment-length conventions.
3. Assign a tolerance: ±3 cm for fitted edges, ±4 cm for free-hanging hems.
4. Write the rows into `expect.bands` — gate G1 measures every drape against
them from then on.
Sanity anchor (proven): a pari band 1.31→1.05 m and piupiu 1.05→0.45 m came from
exactly this read of the kapa haka reference.
## 5. Ease — how much bigger than the body
For each **horizontal** edge, the panel width comes from the body circumference
at that z plus ease. Interpolate circumference linearly between the card's
(z, circ) pairs — neck 0.392, chest 1.083, waist 0.675, hip 1.095 — and **never
interpolate below the hip** (the legs bifurcate; a slice there measures nonsense).
| Fit read from the image | Total ease | Proven case |
|---|---|---|
| Snug / bandeau / activewear | +1520 mm | pari: 1100 vs 1083 bust |
| Regular fitted top | +90100 mm | tee: 1180 vs 1083 bust |
| Loose / drapey | +150 mm and up | (untested above ~150) |
| Bottoms waist edge | **negative**: 0.900.92 × hip circ | piupiu: 1000 vs 1095 hip |
Bottoms are the inversion to internalise: the waist edge is cut *smaller* than
the hips it must pass over, because tension is the anchor (§2).
## 6. Vertical spans are 1:1
Pattern millimetres map 1:1 to world metres — drape shrinkage is negligible for
our fabrics. So:
- panel cloth height = `(top_z hem_z) × 1000`
- a shoulder-hung garment's straps must reach the shoulder: total panel height
= `(shoulder_z hem_z) × 1000`, and the front scoop depth is what's left
between strap top and the visible band top: `scoop = panel_h (band_top_z
hem_z) × 1000`.
Proof this math is the real one: it reproduces all three shipped garments —
tee 480 (shoulder 1.397 → hip-ish hem 0.917), pari 350/scoop 105 (shoulder →
waist 1.05, band top 1.30), piupiu 600 (1.05 → 0.45).
## 7. Blocks — what we can build today
| Garment class | Block | Status |
|---|---|---|
| Tank / tee / fitted bodice / bandeau-with-straps | `blocks.fitted_top` | **proven** (tee, pari) |
| A-line / straight skirt | `blocks.aline_skirt` | **proven** (skirt, piupiu v2) |
| Strand / fringe skirt (piupiu, hula, fur trim) | `blocks.strand_skirt` | proven construction (comb outline), parameters per garment |
| Dress | `fitted_top` + skirt geometry in one panel pair | **untested** — try taper-through-waist first |
| Trousers / shorts | — | **unsolved**; next: 4 panels on `Leg_Front_L/R`, `Leg_Back_L/R` |
| Sleeves, collars, hoods | — | untested; scope risk |
| Capes / cloaks / rectangles (traditional wear) | plain panels | rectangles + blocks; identity is the textile |
Every block bakes in the expensive discoveries — seam parity for
identical-offset panels, the arrangement-x rules per point family, bodice taper,
tension waists, strengthen/relax sim recipes — so **prefer a block over a
hand-typed point list even when the block needs post-editing**. Generate, then
edit the config, not the other way round.
## 8. Textiles carry the identity
For traditional wear especially (kapa haka, Mexica, Pacific), the garment reads
as *itself* because of the textile, not the cut. Spend the effort there:
generate the pattern procedurally (PIL), set physical size via DPI
(`dpi = px / (mm / 25.4)`), and keep the generator script — motifs recur.
Model the shape as simply as the silhouette allows.
## 9. Fabric read → sim plan
From the image, judge weight and stiffness, then pick levers (`tooling.md` §4):
- **Stiffness ladder** (`.zfab` presets): `V2_Woven_Canvas_1` <
`V2_Woven_Denim_1` < `V2_Non-Fabric_Tyvek_1`. `fabric_api` has no physics
setters — the `.zfab` *is* the stiffness choice.
- Fully-sewn sides → strengthen through the whole settle, relax at the end.
Under-constrained (half-sewn sides, thin straps) → **don't** strengthen
(it rotates the garment); remove surplus cloth instead.
- Light/gappy → `SetParticleDistanceOfPattern` ~20 mm and remember §2:
arrangement height is placement.
- Skin-tight → `SetAddlThicknessCollision` a few mm before the settle.
## 10. Worked example — the kapa haka reference, as this method
What was done by trial and error in 2026-07-30/31, restated as the worksheet:
| Row | Pari (bodice) | Piupiu (skirt) |
|---|---|---|
| Slot | Body | Legs |
| Class/block | bandeau → `fitted_top` | strand skirt → `strand_skirt` (v2 shipped as `aline_skirt`) |
| Anchoring | photo shows strapless → **straps anyway** (covers bra straps, fixes height) | waist tension, 1000 vs 1095 hip; light strands → arrangement = placement |
| Targets | band top 1.31 ±0.03, hem 1.05 ±0.03 | waist 1.05 ±0.03, hem 0.45 ±0.04 (below knee) |
| Ease | snug: +17 mm over bust | waist 0.91 × hip |
| Seam plan | shoulders + sides; scoop/armholes in outline | band side edges only; strands are outline teeth |
| Textile | tāniko band → generated `taniko.png`, DPI-sized to span once | flax strands + geometric banding → `piupiu.png` |
| Fabric | default sim fabric, strengthen full settle | default, strengthen + (v3) elastic mid-settle |
| Risks | back neckline never visible in photo — decided, then verified via turntable | hem vs leg-swing envelope (skirt bones downstream) |
The lesson the example carries: **nothing in the finished configs is a guess.**
Every number is a worksheet row, and every worksheet row is checkable — by G1
against the render, or by `GetLineLength` against the drafted panel.
@@ -0,0 +1,310 @@
# MD tooling — which call to reach for, when, and why
Companion to `../SKILL.md`. That file is **doctrine** (how to author a garment).
This file is **tool selection**: for a given question, which API answers it, why that
one rather than the obvious alternative, and what it costs you when you guess.
Everything here is verified against **MD 2026 Personal** by running it. Where a
belief was disproved by experiment, the disproof is kept — those are the expensive
entries. Published Reallusion/CLO docs are thin and several signatures in them are
wrong; `tools/md_bridge/api_surface.json` (688 functions) is the name truth,
`api_docs.json` holds only ~48 harvested docstrings, so **read `__doc__` live** for
anything not listed below.
---
## 0. The diagnostic loop — the part that actually determines whether you succeed
The vision loop in SKILL.md is right but incomplete. Four passes, in this order,
because each one can only see what the previous one can't hide:
| Pass | Call | Catches |
|---|---|---|
| 1. **Pre-sim, zero frames** | `ExportSnapshot3D` right after `ResetClothArrangement()` | Upside-down panels, tangles, self-intersecting outlines, wrong start height. Physics hasn't muddied anything yet. |
| 2. **Untextured** | skip `SetBaseTextureMapImageGivenFilePath` | Folds and **winding**. See §1.3 — this is non-negotiable for shape work. |
| 3. **The back** | `ExportTurntableImages(4)`, index 2 | Anything on the rear. `ExportSnapshot3D` **cannot** show the back at all (§1.1). |
| 4. **Numbers** | export a throwaway OBJ, parse it (§2) | Placement, span, coverage. Eyeballing placement is how garments shipped 814 cm off their own written targets. |
A defect that survives to production is almost always one that pass 2 or 3 would
have caught. Every screenshot in this repo before 2026-07-31 was a **textured front
view** — pass 1 only — and a twisted seam, an inside-out panel, an asymmetric
shoulder fold and an exposed back neckline all shipped underneath that blind spot.
---
## 1. Looking at the garment
### 1.1 `SetCamViewPoint` HAS NO REAR VIEW
Verified by shooting all eight:
| n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| view | bottom | front ¾ | **front** | front ¾ | side | **top** | side | side |
There is no back. So `ExportSnapshot3D` is structurally incapable of showing the
rear of a garment, no matter how you drive it. Use:
```python
paths = export_api.ExportTurntableImages(4) # 0 front, 1 side, 2 BACK, 3 side
```
- The `(int)` overload **ignores any path you pass** and writes into MD's own output
folder (`%LOCALAPPDATA%\CLO Virtual Fashion\Marvelous Designer Personal\<n>\output*.png`).
The **return value is the only way to find the files**.
- The documented `(path, count, w, h, startIndex)` overload returned `[]` — didn't work.
- `ExportCustomViewSnapshot(folder, w, h, prefix)` also returned `[]`.
- Turntable **reuses the same `output*.png` names every call**, so if you are
sweeping variants, `shutil.copyfile` each frame out immediately or you lose it.
- `SetCamViewPoint(5)` (top-down) is the clearest angle on a **shoulder join**
nothing else shows whether front and back actually meet over the shoulder.
- `SetViewPoint()` does nothing. Don't confuse the two.
- Always set the viewpoint before a snapshot so shots are comparable.
### 1.2 Zoom in, and build contact sheets
The turntable renders 2500×2500. Crop to the region of interest and upscale with
PIL before looking, or you will miss centimetre-scale defects. When comparing
variants, tile them into **one** image with labels — one look at four labelled
tiles beats four separate looks, and it makes the winner obvious.
### 1.3 Untextured renders show face orientation — the single best shape diagnostic
With the default sim fabric and no texture map, cloth renders **WHITE on its front
face and GREY on its back face**.
- A panel showing **grey from outside is inside out**.
- A **white streak on an otherwise grey panel** is a fold exposing the true front face.
- A busy motif hides both **completely**. A tāniko print concealed a twisted side
seam through several rounds of wrong diagnosis; the untextured pass identified it
in one look.
Judge shape untextured, then re-enable the texture only for the shipping render.
---
## 2. Measuring — you cannot introspect the mesh, so export and parse
`GetClothPositions()` is an out-param that **stays empty from Python**. There is no
mesh access. So:
```python
op = ApiTypes.ImportExportOption(); op.bExportGarment = True; op.bExportAvatar = False
export_api.ExportOBJ(throwaway_path, op) # then parse the 'v ' lines yourself
```
**Units are a trap.** The two exporters disagree:
| Path | Units | Conversion to metres |
|---|---|---|
| `ExportOBJ` | **millimetres** | `× 0.001` |
| `ExportFBX` → clothing pipeline census | **decimetres** | `× 0.1` (this is what `align.scale_z: 0.1` is doing) |
Verified: an OBJ y of 1086.9 is 1.0869 m, checked against the known 1.777 m body.
Getting this wrong wastes a full sim round-trip.
**`tools/tailor/qc_placement.py` only works on solid silhouettes.** Its pixel
classifier needs a filled shape; on a strand skirt (mostly gaps) it reported a
1.71 m span, which is nonsense. For anything gappy, layered, or strand-based,
measure the exported geometry instead. Useful derived numbers: z-span (band top,
hem), fraction of verts above a bone-ring height, and per-height radius
percentiles for ease.
**`GetLineLength(pattern, line)`** is the cheap way to confirm you are about to sew
the edges you think you are. Fingerprint by expected length before every seam call
— see §3.2.
---
## 3. Constructing patterns
### 3.1 Outlines, not partial seams
`CreatePatternWithPoints` with **y+ = UP in 3D**. Drafting y-down drapes the garment
upside-down over the head; the symptom reads like a seam bug and isn't.
Cut features **into the outline** — neck gaps, armholes, and the teeth of a strand
skirt. Partial seams twist unpredictably. A 32-strand piupiu was built as two
comb-shaped panels with the strands cut into the outline and only the two band side
edges sewn; the alternative (32 partial seams) is in the failure catalogue for a
reason.
### 3.2 Line indices shift — compute them, never hardcode
Line `i` runs `pts[i] → pts[i+1]`. **Inserting a point shifts every index after it.**
Hardcoded `0/5/7/9` broke this repo's bodice recipe twice. Build the point list with
named landmarks and derive the indices:
```python
n_arm = 1 if arm else 0
side_r = 6 + n_arm + 1
idx = {"shoulder_l": 0, "shoulder_r": 5,
"side_r": side_r, "hem": side_r + 1, "side_l": side_r + 2}
```
Then verify against known lengths (`shoulders ≈ 93 mm`, `sides ≈ 208 mm`). If those
drift, your index maths is wrong — not the cloth. This check turns a silent
mis-sew into an immediate, obvious failure.
**Paired edges must be EQUAL, not close.** A strand skirt fingerprinted as
`[60.0, 60.075, 60.0, 60.075]` and that 0.075 mm was dismissed as rounding. It wasn't:
one band side edge was a true vertical and the other was **slanted**, because the
outline gave a half-gap to the leftmost tooth but not the rightmost (the loop skipped
the band-bottom step point on its first iteration, leaving that tooth flush with the
panel edge and half a gap wider than every other). Sewing a flush tooth to a
half-gapped one across a slanted seam notched the waistband and exposed the reverse
face. **Treat any inequality between paired edges as a construction bug.**
### 3.3 Seam pairing — a decision table, because the naive rule is incomplete
`AddSeamlinePairGroup(pf, lineF, pb, lineB, flagA, flagB)`. The two booleans reverse
edge traversal. SKILL.md's rule — "same line index on **mirrored** front/back panels
with `(False, False)`" — is correct but the word *mirrored* is load-bearing, and
most recipes here draft both panels from the **identical** point list offset by `dx`,
which is **not** mirrored.
| Panels | Pairing | Flags | Result |
|---|---|---|---|
| Mirrored | same index | `(False, False)` | correct (the documented case) |
| **Identical, not mirrored** | same index | **`(True, True)`** | correct — both edges reversed |
| Identical, not mirrored | same index | `(False,False)` / `(0,1)` / `(1,0)` | **twists the panel**; part turns inside out and rucks up |
| Mirrored coords, list order kept | same index | any | **garment falls off** — mirroring permutes the indices |
| Mirrored coords + **remapped** indices (`shoulder_l``shoulder_r`, `side_r``side_l`) | crossed | `(False, False)` | correct, and the only thing that fixes an inside-out back panel |
| Cross every pair (sides **and** shoulders) | crossed | any | **slides to the hips** — cross-wired straps cancel and it falls off the shoulders |
Proven by sweeping all four flag combinations at fixed geometry. Two lessons worth
internalising: **a twisted seam and surplus cloth look identical when textured**, and
mirroring is only correct if you remap the pair indices with it.
### 3.4 Shape rules that are geometry, not physics
- **Bodices must taper.** A rectangular panel cut for the bust carries its full bust
width down to a hem sitting on a much smaller waist (1083 mm bust vs 675 mm waist
here — ~400 mm of surplus) and the excess can only fold. Take it off each side
edge at the hem. Use the **same** taper on both panels: side-seam length is
`sqrt(taper² + side_y²)`, so equal tapers keep whole-edge pairing matched and
unequal ones skew it.
- **A pelvis-parented skirt must contain the whole leg-swing envelope.** At a hem
0.5 m below the hip pivot, 30° of hip flexion sweeps the leg ~25 cm forward — more
than any believable silhouette clears. Clearance alone cannot fix a knee-length
skirt; discrete strands or a runtime bone rig has to do the rest.
- **Straps exist to cover the target body's baked-in underwear.** If a neckline or
scoop drops below the body's painted-on tank, the tank shows and reads as the
garment failing to connect. Check the back neckline specifically — nothing was
looking at it.
---
## 4. Fit and physics levers
| Goal | Call | Why this one |
|---|---|---|
| Stand cloth off the skin | `pattern_api.SetAddlThicknessCollision(p, mm)` | The sim **resolves** the clearance, so it holds everywhere. Better than the downstream Blender normal-push, which self-intersects in concave regions — exactly between touching thighs. Set it **before** the settle. |
| Mesh / sim resolution | `SetParticleDistanceOfPattern(p, mm)` | Drives vert count and how many rows sit between skeleton joints. 20 mm gave ~29 rows on a 530 mm strand; 15 mm blew the vert budget. |
| Stop crumpling during the settle | `SetPatternStrengthen(p, True)` → relax at the end | **Conditional — see §6.** |
| Hold a waistband up | `SetPatternPieceElastic(p, line, bool)` + `SetPatternPieceElasticTotalLength(p, line, mm)` | Apply **mid-settle**, never from frame 0. Not needed if the band is cut smaller than the hips — the hip blocks it. |
| Fabric stiffness | **a different `.zfab`** | `fabric_api` has **no physics setters at all** — it is textures and metadata only. Stiffness ladder: `V2_Woven_Canvas_1` < `V2_Woven_Denim_1` < `V2_Non-Fabric_Tyvek_1`. Stock presets in `C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\`. |
Signatures verified live (all `(patternIdx, …)`, and the elastic family takes a
**line index** as its second arg):
```
SetAddlThicknessCollision(int, float) -> None GetAddlThicknessCollisionValue(int) -> float
SetParticleDistanceOfPattern(int, float) -> None SetPatternPieceSolidifyStrengthen(int, float) -> None
SetPatternPieceElastic(int, int, bool) -> None SetPatternPieceElasticTotalLength(int, int, float) -> None
SetPatternPieceElasticStrength(int, int, float) SetPatternPieceElasticSegmentLength(int, int, float)
SetSimulationSelfCollisionAvoidanceStiffness(float) SetAvatarSoftBodyStiffness(int, float)
GetLineLength(int, int) -> float ImportZprj(str, ApiTypes.ImportZPRJOption) -> bool
```
---
## 5. Arrangement — placement is a separate system from physics
| Fact | Consequence |
|---|---|
| `SetArrangement()` only **assigns**; `utility_api.ResetClothArrangement()` **applies** | Skipping the apply = nothing moves. |
| Arrangement **indices regenerate** per avatar import | Always look up by name from `GetArrangementList()`. Never hardcode. |
| `SetArrangementPosition` takes **4 ints** | A float raises `TypeError`. |
| **The x argument's correct value DEPENDS ON THE ARRANGEMENT POINT FAMILY — verified both ways by sweep** | `Body_*_Center_1` (bodice): the two panels must use the **SAME** x. Front `50` / back `0` folds one shoulder only and exposes the reverse face. `Leg_Skirt_Front`/`Back` (skirt): the two panels must use **DIFFERENT** x. `50/0` and `0/50` both drape correctly (band top 1.093 / 1.095) while `50/50` and `0/0` drop the skirt **on the floor** (0.10 / 0.14). So the split is load-bearing for skirts and a bug for bodices. **Never harmonise the two recipes** — and sweep with a control before changing this value on a new garment. |
| Skirts belong on `Leg_Skirt_Front` / `Leg_Skirt_Back`, y=92 | Using `Body_*_Waist` instead put a skirt **814 cm high**. Switching fixed placement to within 9 mm. |
| **A light garment does not slide into place** | MD materialises cloth at the arrangement point; it does not simulate donning. A heavy solid panel settles onto the hips, but a 60 mm waistband with light strands **stays exactly where it is arranged** — the first strand-skirt drape sat at the chest. For light garments, **arrangement height IS the placement**. |
| `utility_api.NewProject()` **deletes the avatar** | Re-import after. |
---
## 6. Rules in SKILL.md that need a condition attached
- **"Soft-from-frame-0 drapes bunch"** → true, and strengthening does flatten a
bunched back. But `SetPatternStrengthen` **rotates an under-constrained garment**:
a bodice whose sides are sewn only over the lower half and whose straps are 90 mm
wide came out flat *and twisted*, with an uneven hem and skin showing. Pre-sim was
clean, so it develops during the settle. **The rule applies to garments whose sides
are fully sewn.** Where it doesn't, remove the surplus instead of stiffening.
- **"Pair the same line index with `(False, False)`"** → only for **mirrored**
panels. See the table in §3.3.
---
## 7. Harness patterns for iterating
**Sweep variants in ONE bridge call.** Each `--file` round trip costs a session
turn; a loop with `NewProject()` per iteration costs one. Four 250-frame sims ≈ 4
minutes and answers a question that four guesses would not.
```python
for value in SWEEP:
utility_api.NewProject(); import_api.ImportFBX(AVATAR_FBX, op) # avatar first
...build, sew, arrange, Simulate(250)...
shots = export_api.ExportTurntableImages(4)
shutil.copyfile(shots[2], f"...{value}_back.png") # names are reused — copy now
```
**Read pass/fail from GEOMETRY, not images, where you can.** A garment on the floor
measures `band_top < 0.3 m`. Have the sweep parse its own probe OBJ and self-report
which cells even stayed on the body — then you only open images for the survivors.
**Always include a control case that reproduces the known-good result.** A sweep
whose control also fails tells you the *harness* is broken, not the geometry —
`tools/tailor/md_pari_armsweep.py` dropped the garment on the floor in all four
cells including its control, so every one of its results was void.
`md_pari_seamsweep.py` had a valid control and produced a decisive answer. Without a
control you cannot tell those two situations apart, and you will "learn" something
false.
**Guard the export.** Keep `EXPORT = False` until a snapshot looks right, so a
look-first run cannot overwrite shipped files.
**Timeouts:** roughly 1 minute per 300 simulated frames; pass `--timeout 880` for
anything with several sims.
**Session mechanics:** a human must click Plugin → TinqsMDBridge; MD's UI is frozen
for the whole session ("Not Responding" is normal); `python tools/md_bridge.py --stop`
hands it back and works even though the UI is dead. Stop when you hand back a
result — Jeremy inspects in the viewport and can't while the bridge owns the thread.
**Writing recipe files from Python:** these files contain box-drawing and em-dash
characters. Always `io.open(path, encoding='utf-8')`. A default-codec round trip
(cp1252) corrupted a recipe and the bridge then failed to read it at all.
---
## 8. Exits
`ExportZPrj` (editable source of truth), `ExportFBX`, `ExportOBJ`, `ExportZPac`
(outfit assembly), `ExportAlembic` / `ExportUSD`, `ExportAnimationVideo`.
No glTF export. No `SaveProjectFile` — saving *is* `ExportZPrj`. **EveryWear and the
AI Image Generator are GUI-only**, absent from the API, so the downstream
Blender half of the lane cannot be replaced by them.
Avatar getters live in **`export_api`** (`GetAvatarCount`, `GetAvatarNameList`), not
`utility_api` where you would look for them.
There is an **animation surface worth knowing about but unverified**:
`SetStartAnimationFrame` / `SetEndAnimationFrame` / `SetCurrentAnimationFrame` /
`RunAnimationRecording` / `GetAnimationLayerFrameRange`, plus `ExportAlembic`. If a
game clip's motion can be driven onto the avatar, this yields a per-frame cloth cache
with zero penetration by construction — the reference a skinned garment should be
scored against. **Not yet tested**: whether `ImportFBX`'s options can bring animation
in with the avatar is unknown.