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:
@@ -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 8–14 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 **8–14 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.
|
||||
Reference in New Issue
Block a user