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:
+10
-5
@@ -1,7 +1,12 @@
|
|||||||
# rules/ — always-on conventions for this repo
|
# rules/ — always-on conventions for this repo
|
||||||
|
|
||||||
No repo-specific rules beyond what's already always-on in root `AGENTS.md`
|
Beyond what's already always-on in root `AGENTS.md` (git via `tinqs push`/
|
||||||
(git via `tinqs push`/`tinqs pull`, stage-by-explicit-path, naming law) and the
|
`tinqs pull`, stage-by-explicit-path, naming law) and the shared hub rules at
|
||||||
shared hub rules at `tinqs-ltd/docs/.agents/rules/`. Add a file here when a
|
`tinqs-ltd/docs/.agents/rules/`:
|
||||||
convention needs to be enforced repo-wide and doesn't fit in `AGENTS.md`'s
|
|
||||||
thin-entry budget.
|
| file | rule |
|
||||||
|
|---|---|
|
||||||
|
| `working-files.md` | Keep milestones, not steps — the four-tier policy for staged working lanes (`.lanekeep`, scratch pruning, no `.blend1`) |
|
||||||
|
|
||||||
|
Add a file here when a convention needs to be enforced repo-wide and doesn't fit
|
||||||
|
in `AGENTS.md`'s thin-entry budget.
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Working files — keep milestones, not steps
|
||||||
|
|
||||||
|
Applies to every staged working lane under `characters/` and `clothing/work/`:
|
||||||
|
folders where a chain of `NN_*.py` scripts each open a `.blend`, change it, and
|
||||||
|
save. Left unmanaged those lanes grow ~80 MB per attempt. Lena's lane reached
|
||||||
|
**3.0 GB, of which 2.2 GB was 30 `.blend` files** — five 80 MB snapshots to land
|
||||||
|
one crotch fix, five more to land one bra fix.
|
||||||
|
|
||||||
|
## The rule
|
||||||
|
|
||||||
|
**A version is a milestone, not a step.** You get a saved copy when a change is
|
||||||
|
finished and approved — not after every cut that got you there. Fixing the bra
|
||||||
|
produces *one* file at the end, not one per attempt.
|
||||||
|
|
||||||
|
Four tiers:
|
||||||
|
|
||||||
|
| tier | what | policy |
|
||||||
|
|---|---|---|
|
||||||
|
| **KEEP** | the `NN_*.py` recipe, mask/input `.npz`/`.npy`, reference images, registered `_vNN.glb` artifacts | permanent. Governed by `characters/REGISTRY.md` (rule 9: superseded artifacts move to `archive/`, never deleted) |
|
||||||
|
| **MASTER** | the handful of `.blend` files a registered artifact was actually built from, plus the live chain head | pinned by name in the lane's `.lanekeep`. Aim for ≤6 per lane |
|
||||||
|
| **SCRATCH** | per-attempt `.blend`, `.blend1` autosaves, `review*/` and `dbg_*/` render dirs, `*_run.log` | overwritten freely, pruned any time, never committed |
|
||||||
|
| **UNKNOWN** | a `.blend` in a folder with no step scripts — hand-authored, so no recipe rebuilds it | never auto-deleted; decide by hand |
|
||||||
|
|
||||||
|
The key asymmetry: **the `.py` scripts are the history.** They are kilobytes and
|
||||||
|
they regenerate any intermediate state from the pinned master above it. Keeping
|
||||||
|
20 `.blend` files is keeping 20 copies of the same 950k-vertex mesh to avoid
|
||||||
|
re-running a script that takes a few minutes.
|
||||||
|
|
||||||
|
## How to work in a lane
|
||||||
|
|
||||||
|
- **Roll one working file.** Pass the same `work.blend` as the output of each
|
||||||
|
experimental step. Only write a new named `.blend` at a phase boundary
|
||||||
|
(geometry done, texture done) or when a step's result gets registered.
|
||||||
|
- **Name the end state, not the attempt.** `10_welded.blend` earns a name;
|
||||||
|
`10_healed` → `10_rimheal` → `10_seamheal` on the way there do not.
|
||||||
|
- **Reuse one `review/` dir.** QA renders are regenerable; minting `review29/`
|
||||||
|
because 28 exist is 8 MB for nothing.
|
||||||
|
- **No `.blend1` autosaves.** Step scripts set
|
||||||
|
`bpy.context.preferences.filepaths.save_version = 0` before saving.
|
||||||
|
- **Pin as you go.** When an artifact is registered in `characters/REGISTRY.md`,
|
||||||
|
add its master `.blend` to `.lanekeep` in the same edit that adds the registry
|
||||||
|
row. Anything not pinned is scratch by definition.
|
||||||
|
|
||||||
|
## `.lanekeep`
|
||||||
|
|
||||||
|
One filename per line, `#` for comments, inline comments allowed. A `.blend`
|
||||||
|
earns a line only when a registered artifact was built from it, or it is the
|
||||||
|
head of the live chain. Unpin the head's parent once the head is accepted.
|
||||||
|
|
||||||
|
## Pruning
|
||||||
|
|
||||||
|
```
|
||||||
|
python tools/prune_lane.py characters/female/lena_nude --recursive # dry run
|
||||||
|
python tools/prune_lane.py <lane> --apply # delete scratch
|
||||||
|
```
|
||||||
|
|
||||||
|
Dry run by default. It never deletes KEEP, MASTER or UNKNOWN, and it flags
|
||||||
|
binaries that are byte-identical to a registered original in
|
||||||
|
`characters/originals/` (the canonical filename always survives a duplicate
|
||||||
|
pair, never the ` - Copy`).
|
||||||
|
|
||||||
|
**Before `--apply`, check nothing is mid-flight.** These runs take minutes and
|
||||||
|
write their output at the end; a lane can gain a new head while you are looking
|
||||||
|
at it. Confirm no `blender` process is running and that the newest `.blend` is
|
||||||
|
pinned.
|
||||||
|
|
||||||
|
## Committing
|
||||||
|
|
||||||
|
Scratch patterns are in `.gitignore` and must never be committed. Note that
|
||||||
|
`.gitattributes` LFS-tracks `.glb/.fbx/.mp4/.task` but **not `.blend`** — an
|
||||||
|
80 MB master committed today goes into git proper, not LFS. Add `.blend` to
|
||||||
|
`.gitattributes` first if masters are ever to be tracked.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Clothing lane — reference image → garment worn in-game
|
||||||
|
|
||||||
|
The animation lane carries **motion** to ariki-game; this is the parallel lane
|
||||||
|
that carries **clothing**. Both converge on the same 65-bone Quaternius
|
||||||
|
skeleton. Built 2026-07-30 in two halves, by two sessions, that meet at a
|
||||||
|
garment mesh.
|
||||||
|
|
||||||
|
```
|
||||||
|
reference photo ──▶ [ UPSTREAM: Marvelous Designer ] ──▶ garment mesh (FBX/OBJ)
|
||||||
|
or concept author · drape on game body 1–4k verts, fitted
|
||||||
|
texture · QC placement + .zprj source
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[ DOWNSTREAM: clothing/garment_pipeline.py ]
|
||||||
|
census · prepare · fit · reduce · (bake) · skin · export
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ariki-game/assets/quaternius/outfits/<set>/
|
||||||
|
per-slot GLB on the shared skeleton
|
||||||
|
+ OutfitCatalog.Register entry
|
||||||
|
```
|
||||||
|
|
||||||
|
## The two halves
|
||||||
|
|
||||||
|
| | Upstream — authoring | Downstream — game-ification |
|
||||||
|
|---|---|---|
|
||||||
|
| Question it answers | "What is this garment, and what shape is it on *our* body?" | "How does the game wear it?" |
|
||||||
|
| Tool | Marvelous Designer 2026 + `TinqsMDBridge` socket plugin | Headless Blender 5.1, deterministic, staged |
|
||||||
|
| Lives in | `tools/md_bridge*`, `tools/tailor/` | `clothing/` |
|
||||||
|
| Playbook | `.claude/skills/marvelous-designer/SKILL.md` | `clothing/README.md` |
|
||||||
|
| Driven by | Agent over a socket; **needs a human click** per session, freezes MD's UI | Agent, fully headless, no human in the loop |
|
||||||
|
| Output | Draped garment mesh + `.zprj` + QC screenshot | Per-slot skinned GLB + catalog entry |
|
||||||
|
|
||||||
|
Read each half's own doc for operating detail — this page only owns **how they
|
||||||
|
fit together**. (Repo SoT rule: no second source of truth.)
|
||||||
|
|
||||||
|
## The handoff contract
|
||||||
|
|
||||||
|
Upstream hands downstream a **garment mesh in `tools/tailor/`**
|
||||||
|
(`lena_<garment>_v<N>_garment.fbx|.obj`) plus the `.zprj` it came from.
|
||||||
|
Downstream consumes it via a per-garment config
|
||||||
|
(`clothing/configs/<garment>.json`) naming the source file, island→part mapping,
|
||||||
|
slots, and budgets — new garment = new config, not new code.
|
||||||
|
|
||||||
|
Two properties of **MD-authored** meshes change how the downstream config should
|
||||||
|
be written, and the pilot config does not yet reflect them:
|
||||||
|
|
||||||
|
- **Already at game budget.** MD-authored garments export at **1.2k–3.7k verts**.
|
||||||
|
The pilot config was written against a *downloaded* MD dress at **2.49 M verts
|
||||||
|
/ 110 MB**, where `reduce` is the hard stage. For our own garments, set a
|
||||||
|
generous tri budget and only decimate if the test bed complains.
|
||||||
|
- **Already fitted.** They were draped on `Ariki_Female_QuatSkin.glb` itself, so
|
||||||
|
alignment/scale work is largely moot; the `fit` stage still earns its keep for
|
||||||
|
the +4 mm clearance shell that prevents poke-through.
|
||||||
|
|
||||||
|
The `clothing/README.md` independently reached the same conclusion from research:
|
||||||
|
prefer re-exporting from MD over fighting reduction downstream. Our upstream
|
||||||
|
makes that the default rather than the exception.
|
||||||
|
|
||||||
|
## Why this shape
|
||||||
|
|
||||||
|
- **Why MD for authoring:** it is the only tool in the stack that turns flat
|
||||||
|
panels into cloth physically draped on *our* body. Garments come out
|
||||||
|
pre-fitted, low-poly, and reproducible from a `.zprj`.
|
||||||
|
- **Why not Character Creator:** the CC5/Reallusion clothing lane was
|
||||||
|
investigated and **rejected** — CC5 provably refuses clothing tools on AccuRig
|
||||||
|
"Humanoid" characters (verified via its Python API *and* GUI). See
|
||||||
|
`clothing/README.md`.
|
||||||
|
- **Why not MD's own EveryWear** (its auto retopo/rig/GLB toolkit, which would in
|
||||||
|
principle replace the whole downstream half): it is **GUI-only, absent from the
|
||||||
|
scripting API**, so it cannot be driven agentically. Revisit only if
|
||||||
|
Reallusion/CLO exposes it.
|
||||||
|
- **Why the halves are separate processes:** MD can't run headless and freezes
|
||||||
|
during a bridge session; Blender can and doesn't. Splitting keeps everything
|
||||||
|
after the garment mesh fully automatable.
|
||||||
|
|
||||||
|
## Body and rig facts
|
||||||
|
|
||||||
|
- Target body: `ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb`
|
||||||
|
(32.7k verts, 65 bones, 1.777 m).
|
||||||
|
- MD needs it as **FBX**: `tools/tailor/avatar/Lena_QuatSkin_Avatar.fbx`
|
||||||
|
(leaf bones stripped, stray meshes dropped) — regenerate with the converter
|
||||||
|
described in the marvelous-designer skill.
|
||||||
|
- Measurements: `tools/tailor/lena_measurements.json` — she is stylized
|
||||||
|
(108-67-109 cm, 72 cm thighs, 178 cm), so standard size charts don't fit.
|
||||||
|
- The skeleton is the same hub the animation lane targets — see `../ARCHITECTURE.md`.
|
||||||
|
|
||||||
|
## Status (2026-07-31)
|
||||||
|
|
||||||
|
**Proven upstream:** image → decomposition → drafted panels → sewn → draped →
|
||||||
|
textured → QC'd → exported, end to end. Shipped: tee, A-line skirt, and a kapa
|
||||||
|
haka set (tāniko pari + piupiu) matched to a reference photo, all with repo
|
||||||
|
screenshots in `tools/tailor/screenshots/`.
|
||||||
|
|
||||||
|
**Proven downstream:** the dress pilot ran through to an in-game render
|
||||||
|
(`clothing/dress_ingame_2026-07-30.png`).
|
||||||
|
|
||||||
|
**The seam is CROSSED (2026-07-31).** The kapa haka set — MD-authored on our own
|
||||||
|
body — went through `garment_pipeline.py` (`configs/pari.json`,
|
||||||
|
`configs/piupiu.json`) to `Female_Kapahaka_{Body,Legs}.gltf`, and both render on
|
||||||
|
Lena in the game's clothing test bed with the tāniko pattern and piupiu stripes
|
||||||
|
intact. None of the feared surprises materialised: MD's material naming, island
|
||||||
|
splitting, and UV/texture carry-through all worked first time. The predictions on
|
||||||
|
MD-authored meshes above held — both garments arrived at game budget already
|
||||||
|
(4k/8k tri ceilings never bit) and needed no alignment work beyond a z-nudge.
|
||||||
|
|
||||||
|
**What the in-game pass exposed instead** was pose-dependent, not authoring:
|
||||||
|
the bodice neckline opens over the sternum and the piupiu lets a thigh through
|
||||||
|
once the skeleton leaves the rest pose. Both are invisible to the pipeline's own
|
||||||
|
QA, which only ever renders the **rest pose** — the pose the garment was fitted
|
||||||
|
in, i.e. the one pose that cannot fail. Judging cloth in motion is the test bed's
|
||||||
|
job, and until 2026-07-31 the bed could not do it either (its Idle/Walk/Dance
|
||||||
|
buttons were overridden by the AnimationTree; fixed in `ClothingTestBed.PlayClip`).
|
||||||
|
|
||||||
|
**Known gaps:** trousers/shorts don't drape (see the skill's failure catalogue);
|
||||||
|
placement QC's pixel classifier is crude; no skirt bones, so deep leg swings
|
||||||
|
compress hems; garments are flat-coloured until textures are authored.
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
---
|
||||||
|
name: marvelous-designer
|
||||||
|
description: Authoring garments in Marvelous Designer agentically — driving MD's Python API over the TinqsMDBridge socket plugin, drafting patterns from a reference image and Lena's measurements, draping/QC-ing them on the game body, and exporting meshes for the clothing pipeline. Use for any "make/adjust a garment", "match this clothing photo", or "drive Marvelous Designer" task, and read before touching tools/md_bridge* or tools/tailor/.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Marvelous Designer — agentic garment authoring
|
||||||
|
|
||||||
|
The **upstream half** of the clothing lane: reference image (or a description) →
|
||||||
|
sewn, draped, textured garment mesh fitted to the game body. The downstream half
|
||||||
|
(garment mesh → game-ready skinned GLB) is `clothing/` — see
|
||||||
|
`.agents/wiki/architecture/clothing-lane.md` for how they meet.
|
||||||
|
|
||||||
|
Everything here was learned by doing it on 2026-07-30 (tee, skirt, kapa haka
|
||||||
|
pari + piupiu). The API facts are **verified against MD 2026 Personal**, not
|
||||||
|
docs — Reallusion/CLO's published API docs are thin and several signatures in
|
||||||
|
them are wrong.
|
||||||
|
|
||||||
|
## The 60-second model
|
||||||
|
|
||||||
|
- MD is driven through **`tools/md_bridge.py`** → a socket plugin running inside
|
||||||
|
MD (`tools/md_bridge/TinqsMDBridge.py`). Full protocol: `docs/md-bridge.md`.
|
||||||
|
- A garment = **flat 2D panels** (`CreatePatternWithPoints`) + **seams**
|
||||||
|
(`AddSeamlinePairGroup`) + **arrangement points** on the avatar
|
||||||
|
(`SetArrangement`), then **simulate** (`utility_api.Simulate(frames)`) to drape
|
||||||
|
cloth onto the body.
|
||||||
|
- You see results by rendering the viewport to PNG (`ExportSnapshot3D`) and
|
||||||
|
reading the image back. **This vision loop is the whole method** — draft,
|
||||||
|
drape, look, measure, adjust, repeat.
|
||||||
|
- **Which call for which job, and why:** `references/tooling.md`. Read it before any
|
||||||
|
QC or diagnostic work — it carries the verified signatures, the render/measure
|
||||||
|
toolkit, the seam-pairing decision table, and the harness patterns.
|
||||||
|
- Garment identity for traditional wear is mostly **textiles, not tailoring**.
|
||||||
|
Kapa haka / Mexica / Pacific garments are rectangles + blocks; the design lives
|
||||||
|
in generated texture maps. Model the shape simply, spend effort on the pattern.
|
||||||
|
|
||||||
|
## Session protocol (read first — MD's UI freezes)
|
||||||
|
|
||||||
|
MD's embedded Python **does not run background threads**, so the bridge owns the
|
||||||
|
main thread while it serves. Consequences:
|
||||||
|
|
||||||
|
1. **A human must click** Plugin → TinqsMDBridge to start a session. You cannot
|
||||||
|
start one yourself. Ask, then wait.
|
||||||
|
2. **MD's UI is frozen** for the whole session ("Not Responding" is normal).
|
||||||
|
3. End with `python tools/md_bridge.py --stop` — works from the terminal even
|
||||||
|
though the UI is dead. Idle timeout is 4 h (was 300 s; raised so one click
|
||||||
|
lasts a work session).
|
||||||
|
4. **Pause the session whenever Jeremy wants to look at the model.** He inspects
|
||||||
|
in MD's viewport; he can't while you hold the thread. Default to stopping when
|
||||||
|
you hand back a result, unless he said to stay in.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/md_bridge.py --ping # verify + see mode/api modules
|
||||||
|
python tools/md_bridge.py --exec "result = pattern_api.GetPatternCount()"
|
||||||
|
python tools/md_bridge.py --file tools/tailor/md_pari.py --timeout 500
|
||||||
|
python tools/md_bridge.py --stop # give the UI back
|
||||||
|
```
|
||||||
|
|
||||||
|
Long `Simulate()` calls need a raised client `--timeout` (~1 min per 300 frames).
|
||||||
|
The exec namespace persists **within** a session, not across; MD api modules
|
||||||
|
(`pattern_api`, `import_api`, `export_api`, `fabric_api`, `utility_api`,
|
||||||
|
`ApiTypes`) plus a `BRIDGE` info dict are pre-seeded.
|
||||||
|
|
||||||
|
**Introspect, don't trust docs.** Full surface dump lives at
|
||||||
|
`tools/md_bridge/api_surface.json` (688 functions) and harvested docstrings at
|
||||||
|
`tools/md_bridge/api_docs.json`. Overloaded pybind11 functions break
|
||||||
|
`inspect.signature()` — read `__doc__` instead.
|
||||||
|
|
||||||
|
## API facts that cost hours to find
|
||||||
|
|
||||||
|
| Fact | Consequence |
|
||||||
|
|---|---|
|
||||||
|
| **2D pattern y+ maps to UP in 3D** | Panels drafted y-down drape **upside-down over the head** and tangle. Symptom looks like a seam bug; it isn't. Hem at y=0, neckline at high y. |
|
||||||
|
| Units are **mm**; gravity −9800 | A "500" square is 50 cm. |
|
||||||
|
| Blender-exported FBX avatars import **10× small** | Import with `op.scale = 10.0`. |
|
||||||
|
| `ImportAvatar()` is **.avt only** — returns `False` on FBX | Use **`import_api.ImportFBX(path, op)`** for the game body. |
|
||||||
|
| `op.bAddArrangementPoints = True` | Auto-generates ~98 named arrangement points on a custom avatar. Without it you have nowhere to hang cloth. |
|
||||||
|
| `SetArrangement()` only *assigns*; **`utility_api.ResetClothArrangement()` applies** it | Skipping the apply = nothing moves. (`ReDrape3DArrangement` only materializes not-yet-draped cloth.) |
|
||||||
|
| Arrangement **indices regenerate** per avatar import | Always look up by name from `GetArrangementList()`; never hardcode an index. Offsets aren't stable either — verify with a 0-frame snapshot. |
|
||||||
|
| Avatar getters live in **`export_api`** (`GetAvatarCount`, `GetAvatarNameList`) | Not `utility_api`, where you'd look. |
|
||||||
|
| `utility_api.NewProject()` **deletes the avatar** | Re-import after. |
|
||||||
|
| `SetBaseTextureMapImageGivenFilePath(path, fabricIdx)` — **path is arg0** | Reversed args throw a TypeError that reads like a missing overload. |
|
||||||
|
| **PNG DPI sets a texture's physical size in MD** | 1024 px at 54.2 dpi = 480 mm of cloth. Control tiling by setting dpi in PIL, not by scaling the image. |
|
||||||
|
| `fabric_api.AddFabric()` needs a **`.zfab` file path** | A name string silently fails. Stock presets: `C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\`. |
|
||||||
|
| Fabric **index 0 is the shared default** | Coloring it dyes every garment in the scene. Always `AddFabric` a new one. |
|
||||||
|
| `AssignFabricToPattern()` returns False for every arg order tried | Use **`pattern_api.SetPatternPieceFabricIndex(pattern, fabric)`**. |
|
||||||
|
| `SetViewPoint()` does nothing; **`SetCamViewPoint(2)` = front view** | Call before every QC snapshot so shots are comparable. |
|
||||||
|
| **`SetCamViewPoint` has NO REAR VIEW** (0 bottom, 1/3 front ¾, 2 front, 4/6/7 sides, 5 top) | `ExportSnapshot3D` cannot show the back of a garment at all. Use `export_api.ExportTurntableImages(4)` — index 2 is the back. It ignores its path arg and writes into MD's own output folder, so the return value is the only way to find the files. This blind spot shipped four separate defects. |
|
||||||
|
| `SetArrangementPosition` takes **4 ints** | A float raises `TypeError`. The correct **x** depends on the arrangement family: `Body_*_Center_1` needs the two panels to **match** (a 50/0 split folds one shoulder); `Leg_Skirt_*` needs them to **differ** (50/50 drops the skirt on the floor). Sweep with a control before changing it. See `references/tooling.md` §5. |
|
||||||
|
| `ExportOBJ` writes **mm**; the FBX→census path reads **decimetres** | The two exporters disagree. Metres = OBJ × 0.001, FBX census × 0.1 (that's what `align.scale_z` is for). |
|
||||||
|
| `GetClothPositions()` is an out-param that stays empty from Python | No mesh introspection. Export a throwaway OBJ and parse its `v ` lines — exact, and the only option for gappy/strand garments, where `qc_placement.py`'s pixel classifier fails outright. |
|
||||||
|
| No `SaveProjectFile`; no glTF export; no EveryWear in the API | Save = `export_api.ExportZPrj`. Exits: `ExportFBX` / `ExportOBJ` / `ExportZPac`. EveryWear is GUI-only. |
|
||||||
|
|
||||||
|
## Construction doctrine
|
||||||
|
|
||||||
|
**Take the reference apart before drafting anything.** The tailor's method —
|
||||||
|
image → slot split → anchoring → placement targets → ease table → piece plan —
|
||||||
|
is `references/deconstruction.md`. Its output is a worksheet of numbers, and
|
||||||
|
every downstream value (panel dims, `expect.bands`, sim recipe) traces to a row
|
||||||
|
of it. Skipping this step is how garments shipped 4–14 cm off target.
|
||||||
|
|
||||||
|
**Draft from measurements, not guesses.** Lena's card:
|
||||||
|
`tools/tailor/lena_measurements.json` (regenerate for any body with
|
||||||
|
`tools/tailor/measure_body.py`, a headless-Blender mesh slicer). She is
|
||||||
|
stylized — 108-67-109 cm with 72 cm thighs on 178 cm — so real-world size charts
|
||||||
|
produce clothes that don't fit.
|
||||||
|
|
||||||
|
**Build from blocks, not freehand.** The blocks are parametric now —
|
||||||
|
`tools/tailor/blocks.py` turns worksheet numbers into a runnable config skeleton
|
||||||
|
(`python tools/tailor/blocks.py fitted_top --name x --band-top-z 1.31 --hem-z 1.05 --ease 17`),
|
||||||
|
with the seam parity, arrangement-x rules, bodice taper, and tension waists
|
||||||
|
baked in (`--selftest` proves it regenerates the shipped garments). Prefer
|
||||||
|
generate-then-edit over hand-typing point lists. The original per-garment
|
||||||
|
recipes remain as provenance:
|
||||||
|
|
||||||
|
| Block | Generator | Legacy recipe | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Fitted top / tank / bodice | `blocks.fitted_top` | `md_tee_v1.py`, `md_pari.py` | Front+back panels, shoulder+side seams, neck gap and armholes cut into the outline. Straps hold height reliably. Legacy recipes predate the §3.3 seam-parity and §5 arrangement-x discoveries — the generator has the fixes. |
|
||||||
|
| A-line skirt | `blocks.aline_skirt` | `md_skirt_v1.py`, `md_piupiu.py` | 2 panels, side seams only. The most forgiving garment; start here. |
|
||||||
|
| Strand/fringe skirt | `blocks.strand_skirt` | — | Comb outline (teeth, never partial seams), symmetric half-gaps, elastic mid-settle. |
|
||||||
|
| Trousers/shorts | **unsolved** — see failure catalogue | | |
|
||||||
|
|
||||||
|
**Seams:** whole-edge only. Pair the **same line index** on **mirrored** front/back
|
||||||
|
panels with `(False, False)` — *mirrored* is load-bearing. Most recipes here draft both
|
||||||
|
panels from the **identical** point list offset by `dx`, which is NOT mirrored, and
|
||||||
|
those need **`(True, True)`** on the side seams or the panel twists and turns partly
|
||||||
|
inside out. Full decision table in `references/tooling.md` §3.3; cross-pairing every
|
||||||
|
seam makes the garment slide off the shoulders. Build
|
||||||
|
neck gaps and armholes as extra points in the outline, not as partial seams.
|
||||||
|
Fingerprint line indices by length via `GetLineLength(pattern, line)`.
|
||||||
|
|
||||||
|
**Straps beat tubes.** A strapless tube slides down to the narrowest catch
|
||||||
|
(underbust). If a reference garment is strapless, add straps anyway when the
|
||||||
|
target body has baked-in underwear to cover — it fixes placement and coverage at
|
||||||
|
once. It's also what made the pari read correctly.
|
||||||
|
|
||||||
|
**Bottoms stay up by tension, not elastic.** Cut the waist *smaller* than the
|
||||||
|
hips and let fabric stretch hold it.
|
||||||
|
|
||||||
|
## Drape recipe (the sequence that works)
|
||||||
|
|
||||||
|
```python
|
||||||
|
pattern_api.SetPatternStrengthen(p, True) # stiffen so cloth wraps, not crumples
|
||||||
|
utility_api.ResetClothArrangement() # apply arrangement
|
||||||
|
utility_api.Simulate(250) # main settle, still stiff
|
||||||
|
# skirts: enable waist elastic HERE, mid-settle, then Simulate(80) more
|
||||||
|
pattern_api.SetPatternStrengthen(p, False)
|
||||||
|
utility_api.Simulate(50) # relax into natural folds
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Strengthen through the whole settle**, relax only at the end. Soft fabric
|
||||||
|
from frame 0 rolls into a bunch at the waist. This is the anti-bunching fix —
|
||||||
|
**but only for garments whose sides are fully sewn.** On a bodice sewn only over
|
||||||
|
its lower half with 90 mm straps, strengthening flattens the back and **rotates the
|
||||||
|
whole garment**, giving an uneven hem and exposed skin. Where it can't be used,
|
||||||
|
remove the surplus cloth instead.
|
||||||
|
- **Elastic mid-settle, never from the start.** Elastic applied before the cloth
|
||||||
|
has wrapped cinches the garment off one hip. Drape first, cinch second — that
|
||||||
|
is what finally locked the piupiu waistband at the waist.
|
||||||
|
- **Skirts arrange on `Leg_Skirt_Front` / `Leg_Skirt_Back`**, not the body-waist
|
||||||
|
points, with `SetArrangementPosition(p, x, 92, 50)`.
|
||||||
|
- **One garment per scene.** A second garment — even frozen — grabs and inverts
|
||||||
|
the new one. Game exports are per-garment anyway. Assemble outfits only at the
|
||||||
|
end, by reloading finished pieces (below).
|
||||||
|
- **Outfit assembly:** `ImportZprj(base)` + `ImportZpac(other, op.bAdd=True)`.
|
||||||
|
Textures survive **only if you don't re-fabric or re-simulate after merging** —
|
||||||
|
a merge renumbers patterns and fabrics unpredictably. Assemble, shoot, done.
|
||||||
|
|
||||||
|
## QC — the part that was missing and matters most
|
||||||
|
|
||||||
|
Early drapes were accepted because they "looked like clothing". They were
|
||||||
|
bunched around the middle. Fit is judged by **numbers against the reference**:
|
||||||
|
|
||||||
|
1. **Extract placement targets** from the reference photo — where each edge sits
|
||||||
|
relative to body landmarks, as a table with tolerances. Example (kapa haka):
|
||||||
|
pari top 1.31 m ±3 cm (above bust), pari hem / piupiu waist 1.05 m ±3 cm,
|
||||||
|
piupiu hem 0.45 m ±4 cm (below knee).
|
||||||
|
2. **Measure every drape**: `python tools/tailor/qc_placement.py <snapshot.png>`
|
||||||
|
classifies background/skin/garment pixels, calibrates px→m off Lena's known
|
||||||
|
1.777 m height, and reports each garment band's top/bottom in metres against
|
||||||
|
the nearest landmark. Iterate until inside tolerance.
|
||||||
|
3. **Snapshot with `SetCamViewPoint(2)`** so every shot is comparable.
|
||||||
|
4. **Save a screenshot into the repo** for every shipped garment —
|
||||||
|
`tools/tailor/screenshots/<garment>.png`. Required, not optional: it's how
|
||||||
|
Jeremy reviews without opening MD.
|
||||||
|
|
||||||
|
**The diagnostic that unsticks everything:** snapshot *after*
|
||||||
|
`ResetClothArrangement()` with **zero simulation frames**. That shows where the
|
||||||
|
panels actually start, before physics muddies it. Seven "seam bug" iterations
|
||||||
|
were really an upside-down garment; one pre-sim snapshot would have caught it
|
||||||
|
immediately. Reach for it the moment a drape misbehaves.
|
||||||
|
|
||||||
|
`qc_placement.py` is honest but crude: it counts the body's baked-in underwear
|
||||||
|
and floor shadow as garment. Tighten it with per-garment colour masks when
|
||||||
|
precision matters.
|
||||||
|
|
||||||
|
## Textures
|
||||||
|
|
||||||
|
Generate procedurally with PIL and set physical scale via DPI —
|
||||||
|
`tools/tailor/textures/` holds `taniko.png` (concentric woven diamonds) and
|
||||||
|
`piupiu.png` (flax strands with geometric banding), both derived from a reference
|
||||||
|
photo. Pattern-generation scripts are worth keeping when a motif will recur.
|
||||||
|
|
||||||
|
Workflow: `AddFabric(<.zfab>)` → `SetBaseTextureMapImageGivenFilePath(png, fab)`
|
||||||
|
→ `SetPatternPieceFabricIndex(pattern, fab)`. Set the PNG's dpi so the design
|
||||||
|
spans the garment exactly once (dpi = px / (mm/25.4)).
|
||||||
|
|
||||||
|
## Handing off downstream
|
||||||
|
|
||||||
|
Export three ways per garment, into `tools/tailor/`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
export_api.ExportZPrj(".../lena_<garment>_v<N>.zprj") # editable source of truth
|
||||||
|
op = ApiTypes.ImportExportOption(); op.bExportGarment = True; op.bExportAvatar = False
|
||||||
|
export_api.ExportFBX(".../lena_<garment>_v<N>_garment.fbx", op) # for clothing/
|
||||||
|
export_api.ExportOBJ(".../lena_<garment>_v<N>_garment.obj", op)
|
||||||
|
export_api.ExportZPac(".../lena_<garment>_v<N>.zpac") # for outfit assembly
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the garment goes through `clothing/garment_pipeline.py` (fit → reduce →
|
||||||
|
skin → export) to become a game outfit part. **Two advantages MD-authored
|
||||||
|
garments have over downloaded ones**, worth exploiting in the config:
|
||||||
|
|
||||||
|
- **They're already game budget.** These export at 1.2k–3.7k verts; the
|
||||||
|
downloaded MD dress the clothing pipeline was piloted on is 2.49 M verts /
|
||||||
|
110 MB. The brutal `reduce` stage is mostly unnecessary — start with a high
|
||||||
|
tri budget and only decimate if the test bed complains.
|
||||||
|
- **They're already fitted**, having been draped on the actual game body, so
|
||||||
|
`align`/`fit` needs little more than the clearance shell.
|
||||||
|
|
||||||
|
Keep the `.zprj` — regenerating a variant beats re-authoring, and the clothing
|
||||||
|
README explicitly prefers re-exporting from MD over fighting reduction.
|
||||||
|
|
||||||
|
## Failure catalogue (don't rediscover these)
|
||||||
|
|
||||||
|
- **Shorts / trousers are unsolved.** Two-panel crotch-notch construction twists
|
||||||
|
every time — fabric can't thread between Lena's touching thighs, and all seam
|
||||||
|
parities were tried. Next approach: **4 panels arranged on the per-leg points**
|
||||||
|
(`Leg_Front_L/R`, `Leg_Back_L/R`) so cloth starts wrapped around each thigh.
|
||||||
|
- **Upside-down panels** (y-down drafting) — see the y+ = UP rule.
|
||||||
|
- **Elastic-first drapes** slide off a hip; **soft-from-frame-0** drapes bunch.
|
||||||
|
- **Co-draping** two garments inverts one.
|
||||||
|
- **A textured render hides folds and winding.** Judge shape with the texture OFF:
|
||||||
|
cloth is white on its front face, grey on its back, so a panel showing grey from
|
||||||
|
outside is inside out and a white streak is a fold. A busy motif conceals both.
|
||||||
|
- **A sweep without a control case teaches you something false.** If the control
|
||||||
|
(known-good settings) also fails, the harness is broken, not the geometry.
|
||||||
|
- **A light garment does not slide into place.** MD materialises cloth at the
|
||||||
|
arrangement point and does not simulate donning, so for anything light the
|
||||||
|
arrangement height *is* the placement.
|
||||||
|
- **Retexturing after a scene merge** silently repaints the wrong garment.
|
||||||
|
- **Threaded/background socket servers inside MD never answer** — the bridge must
|
||||||
|
own the main thread. Don't "fix" the frozen UI by re-threading it; that was
|
||||||
|
tried and the design is deliberate.
|
||||||
|
- MD 2026's **AI Image Generator / EveryWear are GUI-only** — not scriptable.
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
| Path | What |
|
||||||
|
|---|---|
|
||||||
|
| `tools/md_bridge.py` | Terminal client (`--ping/--exec/--file/--stop`) |
|
||||||
|
| `tools/md_bridge/TinqsMDBridge.py` | The plugin (register once via Plug-in Manager; referenced in place, so edits go live on next click) |
|
||||||
|
| `tools/md_bridge/api_surface.json`, `api_docs.json` | Introspected API truth |
|
||||||
|
| `docs/md-bridge.md` | Protocol, install, threading rationale |
|
||||||
|
| `tools/tailor/measure_body.py`, `lena_measurements.json` | Body measurement tool + Lena's card |
|
||||||
|
| `tools/tailor/blocks.py` | Parametric blocks: worksheet numbers → config skeleton (`--selftest`) |
|
||||||
|
| `tools/tailor/draft_garment.py` | Config `md` block → bridge script (the four recipes, templated) |
|
||||||
|
| `tools/tailor/md_*.py` | Per-garment recipes (each header carries its own lessons) |
|
||||||
|
| `references/deconstruction.md` | The tailor's method: taking a reference image apart into pieces |
|
||||||
|
| `tools/tailor/qc_placement.py` | Placement QC measurement |
|
||||||
|
| `tools/tailor/textures/` | Generated fabric maps |
|
||||||
|
| `tools/tailor/screenshots/` | Required per-garment review renders (front **and back** — see tooling.md §0) |
|
||||||
|
| `references/tooling.md` | Which API call for which job, verified signatures, decision tables |
|
||||||
|
| `tools/tailor/avatar/Lena_QuatSkin_Avatar.fbx` | Game body as an MD avatar (from the GLB, leaf bones stripped) |
|
||||||
|
| `tools/tailor/lena_*_v*.{zprj,fbx,obj,zpac}` | Shipped garments |
|
||||||
@@ -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 4–14 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 2–5 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.90–0.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.9–14 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 | +15–20 mm | pari: 1100 vs 1083 bust |
|
||||||
|
| Regular fitted top | +90–100 mm | tee: 1180 vs 1083 bust |
|
||||||
|
| Loose / drapey | +150 mm and up | (untested above ~150) |
|
||||||
|
| Bottoms waist edge | **negative**: 0.90–0.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 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.
|
||||||
+11
@@ -2,3 +2,14 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Working-lane scratch — see .agents/rules/working-files.md
|
||||||
|
# Regenerable by re-running the lane's NN_*.py recipe from its pinned master.
|
||||||
|
*.blend1
|
||||||
|
*.blend2
|
||||||
|
*_run.log
|
||||||
|
**/review/
|
||||||
|
**/review_*/
|
||||||
|
**/review[0-9]*/
|
||||||
|
**/dbg_*/
|
||||||
|
**/probe_*/
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# .humans/ — pages written for people, not agents
|
||||||
|
|
||||||
|
Self-contained HTML pages explaining how parts of this repo work, for reading in
|
||||||
|
a browser. Same convention as `ariki-game/.humans/`.
|
||||||
|
|
||||||
|
The operating detail lives in `.claude/skills/` and `.agents/` and is written for
|
||||||
|
whoever is driving the tools. These pages are the **explanation** — what we built,
|
||||||
|
why it's shaped that way, and what it cost to learn. Where the two disagree, the
|
||||||
|
skill/wiki is the source of truth.
|
||||||
|
|
||||||
|
| page | about |
|
||||||
|
|---|---|
|
||||||
|
| `marvelous-designer.html` | How we author Ariki's garments as real sewn cloth in Marvelous Designer — the process, what's been made, the traps, what's still unsolved. Written for Özlem and Jeremy. |
|
||||||
|
|
||||||
|
Images are referenced relatively (`../tools/tailor/screenshots/…`), so open these
|
||||||
|
from inside the repo rather than copying the file out on its own.
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Ariki — How We Make Clothes in Marvelous Designer</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--ink: #f4efe6;
|
||||||
|
--muted: #a9a396;
|
||||||
|
--gold: #d9a66c;
|
||||||
|
--aqua: #72d5cb;
|
||||||
|
--rose: #e08585;
|
||||||
|
--line: rgba(255,255,255,.12);
|
||||||
|
--panel: rgba(18, 24, 27, .92);
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem clamp(1rem, 4vw, 3rem) 5rem;
|
||||||
|
color: var(--ink);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 90% 0%, rgba(67,139,132,.22), transparent 28rem),
|
||||||
|
#0b1113;
|
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
color: var(--gold);
|
||||||
|
font: 700 .75rem/1 ui-monospace, Menlo, monospace;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: .4rem 0 1.2rem;
|
||||||
|
font-size: clamp(2rem, 5vw, 3.6rem);
|
||||||
|
letter-spacing: -.04em;
|
||||||
|
line-height: 1.05;
|
||||||
|
max-width: 20ch;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 3rem 0 .8rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--aqua);
|
||||||
|
letter-spacing: -.01em;
|
||||||
|
}
|
||||||
|
h3 { margin: 1.8rem 0 .5rem; font-size: 1rem; color: var(--gold); }
|
||||||
|
p { max-width: 60rem; margin: 0 0 1rem; }
|
||||||
|
p.lede { color: var(--muted); font-size: 1.1rem; max-width: 56rem; margin: 0 0 1.5rem; }
|
||||||
|
ul, ol { max-width: 60rem; padding-left: 1.2rem; }
|
||||||
|
li { margin: .35rem 0; }
|
||||||
|
a { color: var(--aqua); }
|
||||||
|
code {
|
||||||
|
font: .88em/1.4 ui-monospace, Menlo, monospace;
|
||||||
|
background: rgba(255,255,255,.07);
|
||||||
|
padding: .12em .4em;
|
||||||
|
border-radius: .3rem;
|
||||||
|
}
|
||||||
|
strong { color: #fff; }
|
||||||
|
.wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--panel);
|
||||||
|
margin: 1rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
table { width: 100%; border-collapse: collapse; min-width: 640px; font-size: .92rem; }
|
||||||
|
th, td { padding: .8rem 1rem; text-align: left; vertical-align: top; border-bottom: 1px solid var(--line); }
|
||||||
|
th {
|
||||||
|
color: var(--gold);
|
||||||
|
font: 700 .7rem/1.2 ui-monospace, Menlo, monospace;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: rgba(0,0,0,.25);
|
||||||
|
}
|
||||||
|
tr:last-child td { border-bottom: 0; }
|
||||||
|
.num { font: .9rem ui-monospace, Menlo, monospace; color: var(--aqua); white-space: nowrap; }
|
||||||
|
figure { margin: 1.5rem 0; }
|
||||||
|
figure img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 1rem;
|
||||||
|
display: block;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
figcaption { color: var(--muted); font-size: .88rem; margin-top: .6rem; max-width: 52rem; }
|
||||||
|
.shots { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1.5rem; }
|
||||||
|
.shots figure { margin: 0; }
|
||||||
|
.flow {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 1.2rem 1.4rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
.flow pre {
|
||||||
|
margin: 0;
|
||||||
|
font: .82rem/1.7 ui-monospace, Menlo, monospace;
|
||||||
|
color: var(--ink);
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.callout {
|
||||||
|
border-left: 3px solid var(--gold);
|
||||||
|
background: rgba(217,166,108,.08);
|
||||||
|
padding: .9rem 1.2rem;
|
||||||
|
border-radius: 0 .6rem .6rem 0;
|
||||||
|
margin: 1.2rem 0;
|
||||||
|
max-width: 60rem;
|
||||||
|
}
|
||||||
|
.callout.warn { border-left-color: var(--rose); background: rgba(224,133,133,.09); }
|
||||||
|
.callout p:last-child { margin-bottom: 0; }
|
||||||
|
.meta { color: var(--muted); font-size: .85rem; border-top: 1px solid var(--line); margin-top: 3.5rem; padding-top: 1.2rem; }
|
||||||
|
hr { border: 0; border-top: 1px solid var(--line); margin: 3rem 0 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p class="eyebrow">Ariki · Clothing lane · For humans</p>
|
||||||
|
<h1>How we make clothes in Marvelous Designer</h1>
|
||||||
|
|
||||||
|
<p class="lede">
|
||||||
|
We build Ariki's garments as <strong>real sewn cloth</strong> — flat pattern pieces, stitched
|
||||||
|
along seams, dropped onto Lena's actual game body and simulated until they hang. Nothing is
|
||||||
|
sculpted by hand. This page explains what we've made, how the process runs, and the things
|
||||||
|
that took us a long time to learn.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="../tools/tailor/screenshots/lena_kapahaka_outfit_v3.png"
|
||||||
|
alt="Lena in a red, black and white tāniko bodice with a flax-coloured piupiu skirt, arms out in T-pose.">
|
||||||
|
<figcaption>
|
||||||
|
The kapa haka outfit, v3 — a tāniko-patterned pari (bodice) over a piupiu (flax skirt), draped
|
||||||
|
on the game body inside Marvelous Designer. Both the tāniko diamonds and the flax strand
|
||||||
|
banding are textures we generated from reference photos, not painted by hand.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<h2>The short version</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Clothing runs on a two-stage lane. Marvelous Designer is the <strong>upstream</strong> half — it
|
||||||
|
answers "what is this garment, and what shape is it on <em>our</em> body?" A headless Blender
|
||||||
|
pipeline is the <strong>downstream</strong> half — it answers "how does the game wear it?"
|
||||||
|
They meet at a garment mesh.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flow"><pre>reference photo ──▶ [ MARVELOUS DESIGNER ] ──▶ garment mesh (FBX/OBJ)
|
||||||
|
or concept draft · drape · texture 1.2k–4.7k verts, already fitted
|
||||||
|
measure · screenshot + .zprj editable source
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[ clothing/garment_pipeline.py — headless Blender ]
|
||||||
|
census · prepare · fit · reduce · skin · export
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ariki-game/assets/quaternius/outfits/<set>/
|
||||||
|
per-slot GLB on the shared 65-bone skeleton</pre></div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The upstream half needs a human to start each session. The downstream half is fully automatic.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="../clothing/kapahaka_ingame_dance_2026-07-31.png"
|
||||||
|
alt="The same tāniko bodice and piupiu skirt on an animated character in the game's clothing test bed.">
|
||||||
|
<figcaption>
|
||||||
|
The same two garments after the downstream half, worn in-game in the Clothing Test Bed and
|
||||||
|
playing an animation. Note the piupiu clipping through the left thigh — that is the kind of
|
||||||
|
defect the downstream <code>fit</code> stage's clearance shell exists to prevent, and it is
|
||||||
|
why we review in the test bed rather than trusting the MD render.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<h2>How a garment actually gets made</h2>
|
||||||
|
|
||||||
|
<p>Six steps. Every one of them produces numbers that the next one is checked against.</p>
|
||||||
|
|
||||||
|
<h3>1 · Take the reference apart</h3>
|
||||||
|
<p>
|
||||||
|
Before drafting anything, the reference image is deconstructed into a worksheet: which body slot
|
||||||
|
each piece belongs to, where it's anchored, where its edges should sit in metres, and how much
|
||||||
|
ease (slack) it needs. Every downstream number traces back to a row of that worksheet. Skipping
|
||||||
|
this step is how early garments shipped <strong>4–14 cm off target</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>2 · Draft from Lena's measurements, not a size chart</h3>
|
||||||
|
<p>
|
||||||
|
Lena is stylized. Her card reads roughly <span class="num">108 – 67 – 109 cm</span> with
|
||||||
|
<span class="num">72 cm</span> thighs on a <span class="num">178 cm</span> frame. Real-world
|
||||||
|
size charts produce clothes that simply do not fit her. The measurements are extracted straight
|
||||||
|
off the game mesh by a script that slices it at known heights
|
||||||
|
(<code>tools/tailor/measure_body.py</code>), so they describe the body the game actually renders.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>3 · Build from blocks, not freehand</h3>
|
||||||
|
<p>
|
||||||
|
We have parametric <em>blocks</em> — a fitted top, an A-line skirt, a strand/fringe skirt — that
|
||||||
|
turn worksheet numbers into a runnable pattern. You give it the band height, hem height and ease;
|
||||||
|
it emits the panel outlines, the seam pairing and the arrangement rules with the known fixes
|
||||||
|
already baked in. Hand-typing point lists is how the early mistakes happened.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>4 · Drape it on the body</h3>
|
||||||
|
<p>
|
||||||
|
The flat panels get arrangement points on the avatar (shoulders, waist, skirt front/back), then
|
||||||
|
the cloth simulates onto her. The sequence matters more than the settings:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Stiffen the fabric for the whole settle, relax only at the end.</strong> Soft cloth
|
||||||
|
from frame zero rolls into a bunch at the waist.</li>
|
||||||
|
<li><strong>Apply waist elastic mid-settle, never from the start.</strong> Elastic applied before
|
||||||
|
the cloth has wrapped cinches the garment off one hip.</li>
|
||||||
|
<li><strong>Bottoms stay up by tension, not elastic</strong> — cut the waist smaller than the hips
|
||||||
|
and let the fabric stretch hold it.</li>
|
||||||
|
<li><strong>One garment per scene.</strong> A second garment, even frozen, grabs and inverts the
|
||||||
|
new one. Outfits are assembled at the very end from finished pieces.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>5 · Judge the fit by numbers, not by eye</h3>
|
||||||
|
<div class="callout">
|
||||||
|
<p>
|
||||||
|
This is the step that was missing at the start, and it matters most. Early drapes were accepted
|
||||||
|
because they <em>looked like clothing</em>. They were bunched around the middle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Now every drape is measured. A script (<code>qc_placement.py</code>) reads the render, separates
|
||||||
|
background from skin from garment, calibrates pixels-to-metres off Lena's known 1.777 m height,
|
||||||
|
and reports where each garment band's top and bottom actually sit — against a target table pulled
|
||||||
|
from the reference photo. For the kapa haka outfit that table was: pari top at
|
||||||
|
<span class="num">1.31 m ±3 cm</span> (above the bust), pari hem and piupiu waist at
|
||||||
|
<span class="num">1.05 m ±3 cm</span>, piupiu hem at <span class="num">0.45 m ±4 cm</span>
|
||||||
|
(below the knee). We iterate until it's inside tolerance.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Every shipped garment also gets a screenshot committed to
|
||||||
|
<code>tools/tailor/screenshots/</code>, front <em>and</em> back. That's how the work gets reviewed
|
||||||
|
without anyone opening Marvelous Designer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>6 · Export four ways</h3>
|
||||||
|
<div class="wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>File</th><th>Why we keep it</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>.zprj</code></td><td>The editable source of truth. Opens in MD normally. Regenerating a variant from this always beats re-authoring.</td></tr>
|
||||||
|
<tr><td><code>.fbx</code></td><td>What the downstream Blender pipeline consumes.</td></tr>
|
||||||
|
<tr><td><code>.obj</code></td><td>Plain-text backup, and the only reliable way to measure the mesh (see below).</td></tr>
|
||||||
|
<tr><td><code>.zpac</code></td><td>For assembling multi-piece outfits later.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>The unusual part: we drive MD with code</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Marvelous Designer has an embedded Python API. We wrote a small plugin that opens a socket inside
|
||||||
|
MD (<code>TinqsMDBridge</code>), so a script on the outside can send it commands — create these
|
||||||
|
panels, sew this edge to that one, simulate 250 frames, render the viewport to a PNG.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The whole method is a vision loop</strong>: draft, drape, render, look at the image,
|
||||||
|
measure it, adjust, repeat. That's what makes it possible to iterate a garment dozens of times in
|
||||||
|
an afternoon.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="callout warn">
|
||||||
|
<p>
|
||||||
|
<strong>Two things to know if you're sitting at the machine.</strong> MD's embedded Python
|
||||||
|
can't run in the background, so the bridge takes over the main thread: <strong>a human has to
|
||||||
|
click Plugin → TinqsMDBridge to start a session</strong>, and <strong>MD's window freezes for
|
||||||
|
the whole session</strong> — "Not Responding" is normal, not a crash. Ending the session with
|
||||||
|
<code>python tools/md_bridge.py --stop</code> gives the UI straight back. If you want to look at
|
||||||
|
the model yourself, we stop the session first.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
None of this locks the work up. The <code>.zprj</code> files are ordinary MD projects — open them,
|
||||||
|
edit patterns, re-drape, re-export by hand any time. The bridge is just how the automation drives
|
||||||
|
the same buttons.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>What we've made so far</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Authored on and after 2026-07-30. Vertex counts are the exported garment mesh.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Garment</th><th>What it is</th><th>Versions</th><th class="num">Verts</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Tee</td><td>First test — fitted top block</td><td>v1</td><td class="num">2,335</td></tr>
|
||||||
|
<tr><td>Skirt</td><td>First test — A-line block, the most forgiving garment</td><td>v1</td><td class="num">1,988</td></tr>
|
||||||
|
<tr><td>Pari</td><td>Kapa haka bodice, tāniko pattern</td><td>v1 → v4</td><td class="num">1,406</td></tr>
|
||||||
|
<tr><td>Piupiu</td><td>Kapa haka flax skirt, strand block</td><td>v1 → v3</td><td class="num">3,022</td></tr>
|
||||||
|
<tr><td>Cape</td><td>Shoulder garment</td><td>v1, v2</td><td class="num">4,664</td></tr>
|
||||||
|
<tr><td>Kapa haka outfit</td><td>Pari + piupiu assembled together</td><td>v1 → v3</td><td class="num">—</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout">
|
||||||
|
<p>
|
||||||
|
<strong>Our own garments are far cheaper than downloaded ones.</strong> These export at
|
||||||
|
<span class="num">1.2k–4.7k</span> verts. The downloaded MD dress the clothing pipeline was
|
||||||
|
first piloted on was <span class="num">2.49 million</span> verts / 110 MB. So the brutal
|
||||||
|
<em>reduce</em> stage downstream is mostly unnecessary for our work — and because they were
|
||||||
|
draped on the real game body, they arrive already fitted.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Textiles do more work than tailoring</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
For traditional wear, garment identity lives in the <strong>pattern</strong>, not the cut. Kapa
|
||||||
|
haka, Mexica and Pacific garments are largely rectangles and simple blocks. So we model the shape
|
||||||
|
simply and spend the effort on the cloth: <code>taniko.png</code> (concentric woven diamonds) and
|
||||||
|
<code>piupiu.png</code> (flax strands with geometric banding) are both generated procedurally from
|
||||||
|
reference photos, and both are reusable.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
One non-obvious control: in MD, <strong>a PNG's DPI sets its physical size on the cloth</strong>.
|
||||||
|
1024 px at 54.2 dpi is 480 mm of fabric. You tile a motif by changing the DPI, not by scaling the
|
||||||
|
image.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Things that cost us hours</h2>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>What happens</th><th>Why</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>The garment drapes <strong>upside-down over her head</strong> and tangles</td>
|
||||||
|
<td>In MD's 2D pattern window, <strong>y+ is UP in 3D</strong>. Panels drafted y-down come out inverted. It looks exactly like a seam bug and isn't — seven iterations were lost to this once.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>A strapless top <strong>slides down to the underbust</strong></td>
|
||||||
|
<td>Cloth falls to the narrowest catch. MD doesn't simulate <em>putting a garment on</em> — it materialises the cloth where you arranged it. Add straps; it fixes placement and coverage at once.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>The garment <strong>slides off the shoulders</strong> or twists partly inside out</td>
|
||||||
|
<td>Seam pairing. Panels drafted as identical copies offset sideways are <em>not</em> mirrored, and need the opposite flip setting from mirrored ones.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Everything in the scene <strong>changes colour at once</strong></td>
|
||||||
|
<td>Fabric slot 0 is the shared default. Colouring it dyes every garment. Always add a new fabric.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>A defect ships because <strong>nobody saw the back</strong></td>
|
||||||
|
<td>MD's snapshot camera has no rear view at all — bottom, front, ¾, sides, top, and that's it. Four separate defects shipped through this blind spot before we started using turntable renders for the back.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>A busy texture <strong>hides folds and inside-out panels</strong></td>
|
||||||
|
<td>Judge shape with the texture off: cloth renders white on its front face and grey on its back, so a grey patch seen from outside is inside out and a white streak is a fold.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout">
|
||||||
|
<p>
|
||||||
|
<strong>The one diagnostic that unsticks everything:</strong> render the scene right after
|
||||||
|
arranging the panels but with <em>zero</em> simulation frames. That shows where the cloth
|
||||||
|
actually starts, before physics muddies the picture. Reach for it the moment a drape
|
||||||
|
misbehaves.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>What doesn't work yet</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Trousers and shorts are unsolved.</strong> Two-panel construction with a crotch notch
|
||||||
|
twists every time — fabric can't thread between Lena's touching thighs, and every seam
|
||||||
|
configuration has been tried. The next idea is four panels arranged on the per-leg points, so
|
||||||
|
the cloth starts already wrapped around each thigh.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>The placement measurement is honest but crude.</strong> It counts the body's baked-in
|
||||||
|
underwear and the floor shadow as garment. Fine for band positions; needs per-garment colour
|
||||||
|
masks when precision matters.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>MD's AI Image Generator and EveryWear are GUI-only</strong> — they can't be scripted, so
|
||||||
|
they're outside this workflow entirely.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Where everything lives</h2>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Path</th><th>What's there</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>tools/tailor/</code></td><td>The garment workshop: per-garment recipes, the parametric blocks, Lena's measurement card, QC scripts, generated textures, and every shipped <code>.zprj</code>/<code>.fbx</code>/<code>.obj</code></td></tr>
|
||||||
|
<tr><td><code>tools/tailor/screenshots/</code></td><td>The review renders — start here to see what exists</td></tr>
|
||||||
|
<tr><td><code>tools/md_bridge.py</code> + <code>tools/md_bridge/</code></td><td>The socket bridge into MD, and a dump of MD's real API surface (688 functions, introspected — the published docs are thin and several signatures in them are wrong)</td></tr>
|
||||||
|
<tr><td><code>clothing/</code></td><td>The downstream half: garment mesh → game-ready skinned GLB</td></tr>
|
||||||
|
<tr><td><code>.claude/skills/marvelous-designer/</code></td><td>The full operating playbook, written for whoever (or whatever) is driving</td></tr>
|
||||||
|
<tr><td><code>.agents/wiki/architecture/clothing-lane.md</code></td><td>How the two halves fit together</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p class="meta">
|
||||||
|
Animation repo · <code>.humans/marvelous-designer.html</code> · written 2026-08-06.<br>
|
||||||
|
Everything here was learned by doing it, and verified against Marvelous Designer 2026 Personal on
|
||||||
|
the PC. If a detail here disagrees with the playbook in
|
||||||
|
<code>.claude/skills/marvelous-designer/</code>, the playbook is the source of truth — this page
|
||||||
|
is the explanation, not the spec.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After
|
@@ -18,6 +18,9 @@ this file stays thin; the operational detail lives in `.claude/skills/` and
|
|||||||
| Batch workflow, canonical retarget command, naming, loop QC, gotchas, repo etiquette | `.claude/skills/animation/SKILL.md` | always-on — read before any batch or clip work |
|
| Batch workflow, canonical retarget command, naming, loop QC, gotchas, repo etiquette | `.claude/skills/animation/SKILL.md` | always-on — read before any batch or clip work |
|
||||||
| Architecture (pipeline stages, canonical rig, tool provenance) | `.agents/wiki/ARCHITECTURE.md` | consult when touching the pipeline |
|
| Architecture (pipeline stages, canonical rig, tool provenance) | `.agents/wiki/ARCHITECTURE.md` | consult when touching the pipeline |
|
||||||
| Per-system architecture detail | `.agents/wiki/architecture/` | consult-when |
|
| Per-system architecture detail | `.agents/wiki/architecture/` | consult-when |
|
||||||
|
| **Clothing lane** — how a reference image becomes a worn in-game outfit (both halves, and where they meet) | `.agents/wiki/architecture/clothing-lane.md` | read before any garment/outfit work |
|
||||||
|
| Garment authoring in Marvelous Designer (bridge, drafting, draping, QC) | `.claude/skills/marvelous-designer/SKILL.md` | always-on for garment authoring |
|
||||||
|
| Garment mesh → game-ready skinned outfit GLB (headless Blender) | `clothing/README.md` | always-on for the game-ification half |
|
||||||
| In-flight work + recently shipped | `.agents/wiki/master-plan.md` | check before starting new work |
|
| In-flight work + recently shipped | `.agents/wiki/master-plan.md` | check before starting new work |
|
||||||
| Dance/clip naming registry (source of truth for names) | `.agents/wiki/dances/REGISTRY.md` | always-on when naming or shipping a clip |
|
| Dance/clip naming registry (source of truth for names) | `.agents/wiki/dances/REGISTRY.md` | always-on when naming or shipping a clip |
|
||||||
| PC-lane routing stub | `.agents/wiki/iclone-bridge.md` | consult-when |
|
| PC-lane routing stub | `.agents/wiki/iclone-bridge.md` | consult-when |
|
||||||
@@ -26,7 +29,9 @@ this file stays thin; the operational detail lives in `.claude/skills/` and
|
|||||||
| Other operator playbooks (iClone/mocap, clip authoring, pose estimation) | `.claude/skills/` (index: `.agents/skills/README.md`) | consult-when |
|
| Other operator playbooks (iClone/mocap, clip authoring, pose estimation) | `.claude/skills/` (index: `.agents/skills/README.md`) | consult-when |
|
||||||
| Loop-QC sub-agent | `.claude/agents/loop-qc.md` | consult-when |
|
| Loop-QC sub-agent | `.claude/agents/loop-qc.md` | consult-when |
|
||||||
| Repo-wide conventions | `.agents/rules/` | always-on |
|
| Repo-wide conventions | `.agents/rules/` | always-on |
|
||||||
|
| **Working files** — keep milestones, not steps: which `.blend`/render dirs survive a staged lane, and how to prune the rest | `.agents/rules/working-files.md` | read before running any staged `NN_*.py` lane |
|
||||||
| Exchange-folder operator guide (which folder, when files move/archive) | `exchange/GUIDE.md` | consult-when doing a batch |
|
| Exchange-folder operator guide (which folder, when files move/archive) | `exchange/GUIDE.md` | consult-when doing a batch |
|
||||||
|
| Human-facing explainer pages (HTML, for Jeremy/Özlem — not a source of truth) | `.humans/` | consult-when explaining the repo to a person |
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -35,11 +40,23 @@ AGENTS.md ← you are here
|
|||||||
.agents/ ← agent context: SOUL, wiki (architecture/master-plan/registry/
|
.agents/ ← agent context: SOUL, wiki (architecture/master-plan/registry/
|
||||||
devops-reports), plans, rules, skills-pointer
|
devops-reports), plans, rules, skills-pointer
|
||||||
.claude/ ← Claude-Code-native skills + agents (auto-discovered)
|
.claude/ ← Claude-Code-native skills + agents (auto-discovered)
|
||||||
tools/ ← Blender-headless retargeters, loop tools, prop export
|
tools/ ← Blender-headless retargeters, loop tools, prop export,
|
||||||
|
md_bridge* (drive Marvelous Designer), tailor/ (garment
|
||||||
|
authoring: recipes, measurements, QC, screenshots)
|
||||||
|
characters/ ← character body models: originals/ (read-only Tripo sources) +
|
||||||
|
per-character working folders (male/mako, female/lena).
|
||||||
|
Naming law in characters/REGISTRY.md — applies to all new
|
||||||
|
derived models
|
||||||
|
clothing/ ← garment mesh → game-ready outfit GLB (staged Blender pipeline)
|
||||||
|
garments/ ← raw incoming garment sources + design advice notes
|
||||||
exchange/ ← files in flight between Mac and PC (see exchange/GUIDE.md)
|
exchange/ ← files in flight between Mac and PC (see exchange/GUIDE.md)
|
||||||
archive/ ← retired takes — never deleted, never renumbered
|
archive/ ← retired takes — never deleted, never renumbered
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Two lanes run through this repo: the **animation lane** (motion → clips) and the
|
||||||
|
**clothing lane** (garments → outfits). They converge on the same 65-bone
|
||||||
|
Quaternius skeleton.
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
- Branch is **`main`**. Sync via **`tinqs push`/`tinqs pull`** (not raw git —
|
- Branch is **`main`**. Sync via **`tinqs push`/`tinqs pull`** (not raw git —
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 998 KiB |
@@ -0,0 +1,266 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Classify and prune a character working lane.
|
||||||
|
|
||||||
|
A "lane" is a staged working folder like
|
||||||
|
`characters/female/lena_nude/hires_claude/` where each step script writes a full
|
||||||
|
`.blend` snapshot. Left alone those snapshots accumulate at ~80 MB per attempt.
|
||||||
|
This sorts a lane into three tiers and, with --apply, deletes the scratch tier.
|
||||||
|
|
||||||
|
KEEP recipe + registered artifacts. Never touched.
|
||||||
|
MASTER the few .blend files a registered artifact was built from,
|
||||||
|
listed explicitly in the lane's `.lanekeep`, plus the chain head.
|
||||||
|
SCRATCH per-attempt .blend, .blend1 autosaves, QA render dirs, run logs,
|
||||||
|
and byte-identical duplicate binaries. Regenerable from KEEP.
|
||||||
|
UNKNOWN a .blend with no step script beside it, so no recipe can rebuild
|
||||||
|
it -- probably hand-authored. Reported, never auto-deleted.
|
||||||
|
|
||||||
|
Dry-run by default. Nothing is deleted without --apply.
|
||||||
|
|
||||||
|
python tools/prune_lane.py characters/female/lena_nude/hires_claude
|
||||||
|
python tools/prune_lane.py characters/female/lena_nude --recursive
|
||||||
|
python tools/prune_lane.py <lane> --apply
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import fnmatch
|
||||||
|
import hashlib
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# --- classification rules -------------------------------------------------
|
||||||
|
|
||||||
|
# Extensions that are the recipe or a registered artifact. Never scratch.
|
||||||
|
KEEP_SUFFIXES = {".py", ".md", ".json", ".txt", ".npz", ".npy", ".jpg", ".jpeg"}
|
||||||
|
|
||||||
|
# Directories that hold regenerable QA renders / probe dumps.
|
||||||
|
SCRATCH_DIR_GLOBS = ("review", "review_*", "review[0-9]*", "dbg_*",
|
||||||
|
"probe_*", "beauty", "beauty_*")
|
||||||
|
|
||||||
|
# Files that are always regenerable output.
|
||||||
|
SCRATCH_FILE_GLOBS = ("*.blend1", "*.blend2", "*.log")
|
||||||
|
|
||||||
|
LANEKEEP = ".lanekeep"
|
||||||
|
|
||||||
|
|
||||||
|
def load_lanekeep(lane: Path) -> tuple[set[str], list[str]]:
|
||||||
|
"""Read the lane's keep manifest. Returns (names, comment lines)."""
|
||||||
|
f = lane / LANEKEEP
|
||||||
|
if not f.exists():
|
||||||
|
return set(), []
|
||||||
|
names, notes = set(), []
|
||||||
|
for line in f.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
# Strip trailing inline comment: "06_final.blend # geometry master"
|
||||||
|
name = line.split("#", 1)[0].strip()
|
||||||
|
if name:
|
||||||
|
names.add(name)
|
||||||
|
return names, notes
|
||||||
|
|
||||||
|
|
||||||
|
def human(n: int) -> str:
|
||||||
|
for unit in ("B", "KB", "MB", "GB"):
|
||||||
|
if abs(n) < 1024 or unit == "GB":
|
||||||
|
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
||||||
|
n /= 1024.0
|
||||||
|
return f"{n:.1f} GB"
|
||||||
|
|
||||||
|
|
||||||
|
def sha256(path: Path, chunk: int = 1 << 20) -> str:
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with path.open("rb") as fh:
|
||||||
|
for block in iter(lambda: fh.read(chunk), b""):
|
||||||
|
h.update(block)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def matches(name: str, globs) -> bool:
|
||||||
|
return any(fnmatch.fnmatch(name, g) for g in globs)
|
||||||
|
|
||||||
|
|
||||||
|
def has_step_scripts(lane: Path) -> bool:
|
||||||
|
"""True if this folder holds NN_*.py step scripts -- i.e. its .blend files
|
||||||
|
are script output that can be rebuilt, not hand-authored work."""
|
||||||
|
return any(lane.glob("[0-9][0-9]*_*.py"))
|
||||||
|
|
||||||
|
|
||||||
|
def classify(lane: Path, keep_names: set[str], repo: Path, exclude=()):
|
||||||
|
"""Walk the lane and bucket every entry.
|
||||||
|
|
||||||
|
Returns (keep, master, scratch, unknown). `exclude` names subdirectories
|
||||||
|
scanned as lanes in their own right, so they are not counted twice.
|
||||||
|
"""
|
||||||
|
keep, master, scratch, unknown = [], [], [], []
|
||||||
|
scripted = has_step_scripts(lane)
|
||||||
|
|
||||||
|
# Scratch directories are pruned whole; don't descend into them.
|
||||||
|
entries = []
|
||||||
|
for path in sorted(lane.rglob("*")):
|
||||||
|
parts = path.relative_to(lane).parts
|
||||||
|
if parts[0] in exclude:
|
||||||
|
continue # belongs to a nested lane
|
||||||
|
if any(matches(p, SCRATCH_DIR_GLOBS) for p in parts[:-1]):
|
||||||
|
continue # inside a dir already bucketed as scratch
|
||||||
|
entries.append(path)
|
||||||
|
|
||||||
|
for path in entries:
|
||||||
|
rel = path.relative_to(lane).as_posix()
|
||||||
|
if path.is_dir():
|
||||||
|
if matches(path.name, SCRATCH_DIR_GLOBS) and rel not in keep_names:
|
||||||
|
size = sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
|
||||||
|
scratch.append((rel + "/", size, "QA render dir"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if path.name == LANEKEEP:
|
||||||
|
keep.append((rel, path.stat().st_size, "keep manifest"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
size = path.stat().st_size
|
||||||
|
|
||||||
|
if rel in keep_names or path.name in keep_names:
|
||||||
|
master.append((rel, size, "pinned in .lanekeep"))
|
||||||
|
elif path.suffix.lower() in KEEP_SUFFIXES:
|
||||||
|
keep.append((rel, size, "recipe/input"))
|
||||||
|
elif matches(path.name, SCRATCH_FILE_GLOBS):
|
||||||
|
why = "autosave" if path.suffix.startswith(".blend") else "run log"
|
||||||
|
(scratch if scripted else unknown).append((rel, size, why))
|
||||||
|
elif path.suffix.lower() == ".blend":
|
||||||
|
if scripted:
|
||||||
|
scratch.append((rel, size, "per-attempt snapshot"))
|
||||||
|
else:
|
||||||
|
unknown.append((rel, size, "no step script here - cannot rebuild"))
|
||||||
|
else:
|
||||||
|
keep.append((rel, size, "binary (checked for dupes)"))
|
||||||
|
|
||||||
|
scratch += find_duplicates(lane, keep, repo)
|
||||||
|
keep = [k for k in keep if k[0] not in {s[0] for s in scratch}]
|
||||||
|
return keep, master, scratch, unknown
|
||||||
|
|
||||||
|
|
||||||
|
def find_duplicates(lane: Path, keep, repo: Path):
|
||||||
|
"""Flag lane binaries that are byte-identical to a registered original.
|
||||||
|
|
||||||
|
Also flags within-lane duplicate groups, keeping the oldest member.
|
||||||
|
"""
|
||||||
|
dupes = []
|
||||||
|
binaries = [lane / rel for rel, _, why in keep if why.startswith("binary")]
|
||||||
|
if not binaries:
|
||||||
|
return dupes
|
||||||
|
|
||||||
|
# Reference set: the read-only registered originals.
|
||||||
|
originals = {}
|
||||||
|
orig_dir = repo / "characters" / "originals"
|
||||||
|
if orig_dir.is_dir():
|
||||||
|
for f in orig_dir.rglob("*"):
|
||||||
|
if f.is_file() and f.suffix.lower() in (".glb", ".fbx"):
|
||||||
|
originals[sha256(f)] = f.relative_to(repo).as_posix()
|
||||||
|
|
||||||
|
by_hash = defaultdict(list)
|
||||||
|
for f in binaries:
|
||||||
|
by_hash[sha256(f)].append(f)
|
||||||
|
|
||||||
|
for digest, files in by_hash.items():
|
||||||
|
if digest in originals:
|
||||||
|
for f in files:
|
||||||
|
dupes.append((f.relative_to(lane).as_posix(), f.stat().st_size,
|
||||||
|
f"byte-identical to {originals[digest]}"))
|
||||||
|
elif len(files) > 1:
|
||||||
|
# The canonical name survives: registry grammar is lowercase
|
||||||
|
# [a-z0-9_] only, so anything with " - Copy", " (1)" or spaces is
|
||||||
|
# the accidental duplicate regardless of which one is older.
|
||||||
|
def rank(p: Path):
|
||||||
|
stem = p.stem.lower()
|
||||||
|
junk = (" " in p.stem or "copy" in stem or "(1)" in stem)
|
||||||
|
return (junk, p.stat().st_mtime)
|
||||||
|
files.sort(key=rank)
|
||||||
|
for f in files[1:]:
|
||||||
|
dupes.append((f.relative_to(lane).as_posix(), f.stat().st_size,
|
||||||
|
f"byte-identical to {files[0].name}"))
|
||||||
|
return dupes
|
||||||
|
|
||||||
|
|
||||||
|
def report(lane: Path, keep, master, scratch, unknown, notes, repo: Path) -> int:
|
||||||
|
def block(title, rows, show_all):
|
||||||
|
total = sum(r[1] for r in rows)
|
||||||
|
print(f"\n {title} ({len(rows)} entries, {human(total)})")
|
||||||
|
shown = rows if show_all else sorted(rows, key=lambda r: -r[1])[:12]
|
||||||
|
for rel, size, why in sorted(shown, key=lambda r: r[0]):
|
||||||
|
print(f" {human(size):>9} {rel:<46} {why}")
|
||||||
|
if len(shown) < len(rows):
|
||||||
|
print(f" {'':>9} ... and {len(rows) - len(shown)} more")
|
||||||
|
return total
|
||||||
|
|
||||||
|
try:
|
||||||
|
shown_lane = lane.relative_to(repo).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
shown_lane = str(lane)
|
||||||
|
print(f"\n=== {shown_lane} ===")
|
||||||
|
for n in notes:
|
||||||
|
print(f" note: {n}")
|
||||||
|
block("KEEP recipe + artifacts", keep, show_all=False)
|
||||||
|
block("MASTER pinned .blend", master, show_all=True)
|
||||||
|
if unknown:
|
||||||
|
block("UNKNOWN decide by hand -- never auto-deleted", unknown, show_all=True)
|
||||||
|
freed = block("SCRATCH regenerable", scratch, show_all=False)
|
||||||
|
return freed
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("lane", type=Path, help="working lane directory")
|
||||||
|
ap.add_argument("--apply", action="store_true",
|
||||||
|
help="actually delete the scratch tier (default: dry run)")
|
||||||
|
ap.add_argument("--recursive", action="store_true",
|
||||||
|
help="also treat each immediate subdirectory as its own lane")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
lane = args.lane.resolve()
|
||||||
|
if not lane.is_dir():
|
||||||
|
print(f"error: not a directory: {lane}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
repo = Path(__file__).resolve().parent.parent
|
||||||
|
sublanes = ([d for d in sorted(lane.iterdir()) if d.is_dir()]
|
||||||
|
if args.recursive else [])
|
||||||
|
# Parent scanned first, with its sublanes excluded so nothing is counted twice.
|
||||||
|
plan = [(lane, {d.name for d in sublanes})] + [(d, set()) for d in sublanes]
|
||||||
|
|
||||||
|
total_freed, all_scratch, unknown_total = 0, [], 0
|
||||||
|
for ln, exclude in plan:
|
||||||
|
keep_names, notes = load_lanekeep(ln)
|
||||||
|
if not keep_names and any(ln.glob("*.blend")) and has_step_scripts(ln):
|
||||||
|
notes.append(f"no {LANEKEEP} - every .blend here is treated as scratch")
|
||||||
|
keep, master, scratch, unknown = classify(ln, keep_names, repo, exclude)
|
||||||
|
if not (keep or master or scratch or unknown):
|
||||||
|
continue
|
||||||
|
total_freed += report(ln, keep, master, scratch, unknown, notes, repo)
|
||||||
|
all_scratch += [(ln, rel) for rel, _, _ in scratch]
|
||||||
|
unknown_total += sum(u[1] for u in unknown)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"scratch tier: {len(all_scratch)} entries, {human(total_freed)} reclaimable")
|
||||||
|
if unknown_total:
|
||||||
|
print(f"unknown tier: {human(unknown_total)} held back for a manual call")
|
||||||
|
|
||||||
|
if not args.apply:
|
||||||
|
print("DRY RUN - nothing deleted. Re-run with --apply to prune.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print("APPLYING - deleting scratch tier...")
|
||||||
|
for ln, rel in all_scratch:
|
||||||
|
target = ln / rel.rstrip("/")
|
||||||
|
if target.is_dir():
|
||||||
|
shutil.rmtree(target)
|
||||||
|
elif target.exists():
|
||||||
|
target.unlink()
|
||||||
|
print(f"done - freed {human(total_freed)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 510 KiB |
Reference in New Issue
Block a user