feat: clothing lane, character sources, and DCC bridges

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:55:43 -07:00
parent 3363209cac
commit 3ba86b2ea8
558 changed files with 68622 additions and 8 deletions
+108
View File
@@ -0,0 +1,108 @@
# Blender Bridge (TinqsBlenderBridge)
Live control of a **running, interactive** Blender session from the terminal —
third sibling of the iClone (`docs/iclone-bridge.md`) and MD
(`docs/md-bridge.md`) bridges: a socket server inside the app + a thin
JSON-over-TCP client, same wire protocol and `result` convention.
Distinct from the headless pipeline scripts (`tools/cc_retarget.py`,
`tools/tailor/*`), which spawn `blender --background` and exit. Those never
touch the open GUI. This bridge is for driving the window Jeremy is
*looking at* — inspecting a selection, nudging a mesh, checking a drape.
**The good news vs. MD:** Blender has `bpy.app.timers`, so the server polls a
non-blocking socket from a main-thread timer. **The UI stays fully
interactive** — no frozen-window session like MD. The UI only stalls for the
duration of a single `exec`.
| | iClone bridge | MD bridge | Blender bridge |
|---|---|---|---|
| Server source | `tools/iclone_bridge/TinqsBridge/main.py` | `tools/md_bridge/TinqsMDBridge.py` | `tools/blender_bridge/tinqs_blender_bridge.py` |
| Client | `tools/iclone_bridge.py` | `tools/md_bridge.py` | `tools/blender_bridge.py` |
| Port | 18800 | 18900 | **19000** (`TINQS_BLENDER_BRIDGE_PORT` overrides) |
| Install | ps1 → OpenPlugin, auto-loads | manual register, click = session | ps1 → `scripts/startup/`, auto-starts |
| Executor | QTimer on Qt main thread | main-thread loop, **UI frozen** | `bpy.app.timers`, **UI stays live** |
| Session end | app exit | `--stop` / 4 h idle | `--stop`, or app exit (no idle timeout) |
| Embedded Python | 3.8 | 3.11.8 | 3.13.9 (Blender 5.1.2) |
| Log | `%TEMP%\tinqs_iclone_bridge.log` | `%TEMP%\tinqs_md_bridge.log` | `%TEMP%\tinqs_blender_bridge.log` |
## Install (one-time)
```powershell
powershell -File tools\install_blender_bridge.ps1
```
Copies the server into every `%APPDATA%\Blender Foundation\Blender\<ver>\scripts\startup\`.
Blender imports startup modules at launch and calls their `register()`, so
**the bridge auto-starts on port 19000 from the next launch onward** — zero
clicks, forever.
**To start it in an ALREADY-OPEN Blender** (a session that predates the
install — the common case, and one that matters when the window has unsaved
work you don't want to lose to a restart):
> Scripting workspace → **Open** → `tools\blender_bridge\tinqs_blender_bridge.py` → **Run Script** (▶)
Re-running is safe: a singleton on `builtins` closes the previous socket and
retires its timer before rebinding, so no leak and no "port busy" on reload.
## Workflow
```bash
python tools/blender_bridge.py --ping # blender ver, open .blend
python tools/blender_bridge.py --exec "result = [o.name for o in bpy.data.objects]"
python tools/blender_bridge.py --file edit_mesh.py
python tools/blender_bridge.py --timeout 300 --file long_bake.py
python tools/blender_bridge.py --stop # server off, Blender lives on
```
**Use `--file`, not `--exec`, for anything with quotes.** PowerShell strips
inner double quotes from native-command args, so
`--exec "bpy.data.objects[\"Cube\"]"` arrives as `bpy.data.objects[Cube]`
`NameError: name 'Cube' is not defined`. Write a scratchpad .py and use
`--file`.
The exec namespace persists across requests within a Blender session and is
pre-seeded with `bpy`, `view3d_override()`, and a `BRIDGE` info dict.
## The context trap (read this before using bpy.ops)
Timer callbacks run with **no window/area in `bpy.context`**, so operators
that need one fail with *"context is incorrect"*. Two ways out:
1. **Prefer the data API**`bpy.data`, `obj.location`, `mesh.vertices[i].co`,
or `bmesh` for topology. Context-free, and the right tool anyway.
2. **Override when an operator truly needs a viewport** — the seeded helper
returns the kwargs:
```python
with bpy.context.temp_override(**view3d_override()):
bpy.ops.object.shade_smooth()
```
## Verified (2026-08-04, Blender 5.1.2, Python 3.13.9)
Smoke-tested end to end in a throwaway GUI instance: socket up ~3 s after
launch; `--ping`; direct vertex edit; `bmesh` subdivide (8 → 26 verts);
`shade_smooth` via `temp_override`; `--stop` closed the port cleanly and
**Blender stayed alive and responsive**.
## Two bugs found and fixed during that verification
Both are the kind that silently produce "connection refused", so they're
worth remembering if the bridge is ever ported or rewritten:
1. **`bpy.data` is restricted during startup registration.** Modules in
`scripts/startup/` register while `bpy.data` is still a `_RestrictData`
stub — reading `bpy.data.filepath` there raises
`AttributeError: '_RestrictData' object has no attribute 'filepath'`,
which aborts `register()` and the socket never binds, leaving *no log
file at all*. Fix: `register()` only schedules a one-shot
`bpy.app.timers` callback; the real start runs after boot, when the full
API is live. Diagnose this class of failure with
`blender --background --python-expr "print('x')"` — startup tracebacks
print to stdout. (Do **not** pass `--factory-startup`; it disables user
scripts and hides the very thing you're testing.)
2. **Unregistering a timer from inside its own callback is an error.** The
`__STOP__` path called `stop()` from within `_poll`. Fix: `stop(in_timer=True)`
just closes the socket and lets the callback's `return None` retire the timer.
@@ -0,0 +1,195 @@
# FBX→GLB Converter Research — what to learn from external tools & skills
Date: 2026-07-30
Scope: survey of established FBX→GLB converters, retargeting tools, and published Claude
skills, cross-referenced against our pipeline (`tools/cc_retarget.py`, `mixamo_retarget.py`,
`kevin_retarget.py` + `loop_qc.py`/`loop_fix.py`). Complements — does not replace —
`plans/fbx-pipeline-plan-2026-07-21.md`, which this research largely validates.
---
## TL;DR — ranked recommendations
| # | Change | Fixes | Effort | Source of technique |
|---|--------|-------|--------|---------------------|
| 1 | Enforce quaternion neighborhood in the baker (negate q when dot(qᵢ,qᵢ₊₁)<0) | latent rotation-flip pops at runtime | ~10 lines | Khronos glTF #1395/#2073, Magnum |
| 2 | Post-export `gltf-transform resample` pass (slerp-aware, tol 1e-4, lossless) | dense 1800-frame bakes, GLB size | 1 script hook (Node stack already in repo) | glTF-Transform / keyframe-resample-wasm |
| 3 | `gltf-validator` JSON output as a machine gate after export | silent malformed output | small | Khronos glTF-Validator (no published skill does this — we'd be first) |
| 4 | Unmapped-bone diagnostics + mapping-quality score before retarget | silently degraded clips | small | blender-toolkit skill (Excellent/Good/Fair/Poor gate) |
| 5 | Explicit rest-pose alignment step (A-pose→T-pose correction) | the open shoulders-~45°-off QA item | medium | avatar-asset-pipeline pose configs, ARP "Redefine Rest Pose", Godot Rest Fixer |
| 6 | Bake twist-bone rotation into parent instead of dropping it | candy-wrapper forearms/thighs | medium | ufbx helper-node insight, standard practice |
| 7 | Unified retargeter + JSON rig maps (already planned as P2/FR-3) | 3 divergent forks | planned | validated by avatar-asset-pipeline's declarative component design |
| 8 | Motion QC thresholds beyond the loop seam (ground penetration, scale drift, heading jumps) | "green-verify-is-not-correct" | medium | blender-motion-state-inspection skill |
| 9 | Evaluate soupday cc_blender_tools importer for CC FBX | stock-importer risk we already documented | evaluation spike | soupday docs: "do not use the standard Blender FBX importer" |
| 10 | If GLB size ever matters: meshopt (not Draco) + gltfpack-style quantization | — | later | gltfpack (12-bit rot / 16-bit trans / 30 Hz defaults) |
Items 14 are cheap, independent, and zero-risk to output quality. Item 5 closes our
single biggest open correctness question.
---
## 1. What the established converters teach
### FBX2glTF (Facebook, archived; Godot fork)
- Pattern worth keeping (we already follow it): **correctness by evaluation, not
translation** — never convert FBX curves directly; sample the evaluated transform per
frame. FBX2glTF bakes everything at a fixed rate exactly like our depsgraph-evaluate loop.
- Its failure modes (GeometricTransformation pivots mishandled — [fork #56](https://github.com/godotengine/FBX2glTF/issues/56);
skeleton defects that made Godot abandon it) confirm Blender-headless was the right lane.
- Blend-shape lesson: morph normals/tangents in FBX are "rarely correctly present" — if we
ever carry facial morphs, don't export morph normals unless verified.
### glTF-Blender-IO (what our export actually runs through)
- Armature export is always sampled; rest pose is the baseline for the relative TRS it
writes. **If our armature rest ≠ the game skeleton rest, the export silently differs**
reinforces the plan's QC-1 `verify_target.py` (pinned rig vs live game rig, tol 1e-5).
- Known issues we should pin settings against: sampling keys all bones (#1432/#2657),
stepped keys become linear (#842). Action: set `export_force_sampling`,
`export_frame_step`, `export_optimize_animation_size` and the `keep_anim_*` flags
explicitly per Blender version instead of trusting defaults (we already set the first
and last — add the rest).
### ufbx — the best-documented catalog of FBX semantics
- **Rest pose ≠ bind pose is legal FBX.** Correct converters trust skin-cluster matrices
(`geometry_to_bone`), never the node hierarchy at time 0. (assimp gets this wrong —
[assimp #4015](https://github.com/assimp/assimp/issues/4015) — which is why it's
disqualified for rigged characters.) Relevant to our A-pose question: iClone's
"T-pose export" only prepends a T-pose *frame*; the skin bind stays A-pose.
- Scale-inheritance and geometric-transform mismatches with glTF are solved by inserting
**helper nodes** — the explanation to reach for if iClone props/twist bones ever explode.
- Baking niceties: don't re-resample tracks that are already dense
(`minimum_sample_rate` guard); reduce keys *after* baking, not by blind resampling.
### glTF-Transform (directly usable — Node + gltf-transform already in repo via `bake_run_punch.mjs`)
- **`resample()` is lossless keyframe dedup**: drop interior keys exactly reproduced by
interpolating neighbors, using the track's real mode — **slerp-aware for rotations**,
default tolerance 1e-4 (algorithm: [keyframe-resample-wasm](https://github.com/donmccurdy/keyframe-resample-wasm)).
Perfect complement to our bake-every-frame approach; free size win, zero quality risk.
- Compression fact: **Draco compresses mesh primitives only. meshopt
(EXT_meshopt_compression) also compresses animation samplers** — meshopt is the only
correct choice for our animation-only GLBs, if size ever matters (needs gzip/brotli
outer layer to pay off; Godot must support the extension — verify before adopting).
- Community warning (blender-kiln skill): never run `gltf-transform optimize` as one
blob — its simplify pass can destroy assets; apply individual steps (`resample`,
`prune`, `dedup`) sequentially.
### Keyframe reduction done right — ozz-animation
- ozz's `AnimationOptimizer` measures error as **world-space distance at the end of the
joint's child hierarchy** (default tolerance 1 mm, probe distance 0.1 m, per-joint
overrides). Far better than per-channel epsilons: tighten fingers/feet, loosen spine.
The metric to copy if we ever add lossy reduction on top of `resample()`.
- gltfpack's shipping defaults — 30 Hz resample, 12-bit rotation / 16-bit translation
quantization — calibrate "what players don't notice."
### Quaternion continuity — a latent bug we likely have
- glTF runtimes disagree on >180° rotation steps (lerp+normalize vs slerp, shortest-path
ambiguity — Khronos [#1395](https://github.com/KhronosGroup/glTF/issues/1395),
[#2073](https://github.com/KhronosGroup/glTF/issues/2073)). Blender's matrix
decomposition does **not** guarantee quaternion sign continuity frame-to-frame.
- Fix at bake time in `retarget_one()`: keep the previous frame's quaternion per bone;
if `dot(prev, cur) < 0`, negate `cur` before `keyframe_insert`. ~10 lines, applies to
all three retargeters (or the future unified one).
- Also: never use CUBICSPLINE samplers for rotations (overshoot — Khronos #2008). We
export sampled/linear, so we're fine there.
---
## 2. Retargeting techniques
### Godot's fixed-skeleton retargeting (closest published design to our Quaternius lane)
- `SkeletonProfileHumanoid` + BoneMap + **Rest Fixer** with:
- *Overwrite Axis* — rewrite all bone rests to canonical profile rests (their version of
our "rig onto the clip rest" move).
- *Fix Silhouette* — A-pose sources corrected toward the T-pose profile (documented
limitation: can't fully fix bone roll).
- *Remove Unimportant Positions* — strip position tracks from everything except
root/hips. **We already comply** (rotation-only except pelvis/root) — validated.
- *Normalize Position Tracks* — scale hip translation by source/target hips-height
ratio (`motion_scale`). **Our `tscale` is the same idea** — validated, keep it.
- Docs: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/retargeting_3d_skeletons.html
### Rest-pose alignment (our open weakness #1)
Every serious retargeter has an explicit rest-alignment step; we have none:
- **Auto-Rig Pro Remap**: map → *Redefine Rest Pose* (snapshot a corrected source pose as
the retarget basis) → retarget → per-bone offsets → bake. The proven order of operations.
- **avatar-asset-pipeline** ([infosia](https://github.com/infosia/avatar-asset-pipeline)):
declarative JSON pipelines with an A-pose↔T-pose component driven by a **pose config
file** (quaternion per bone). Closest architectural analog to our planned
`pipeline/retarget/rigs/*.json` — extend the rig-map JSON schema with an optional
`rest_correction` block of per-bone quaternions applied to the source rest before
computing `C`, and the A-pose problem becomes data, not code.
- **soupday cc_blender_tools** maps skin bones **by iClone ID rather than name** and
explicitly warns against Blender's stock FBX importer ("Automatic Bone Orientation"
alters joint rotations and breaks round-tripping). It retargets armature *and*
shape-key animation. Worth a spike: import one known-problem CC FBX both ways and diff
the baked curves.
### Twist bones (our weakness #2)
We currently drop `*Twist01/02` rotation entirely → candy-wrapper forearms/thighs.
Standard fix: compose the twist bone's captured rotation into its mapped parent
(upperarm/lowerarm/thigh) before writing the target key — the twist joints exist to
*distribute* roll, so discarding them loses real roll. (ufbx's helper-node machinery is
the deep version; composing into the parent is the cheap correct-enough one for a rig
with no twist joints on the target.)
### Further afield (noted, not recommended now)
- Unity Mecanim: retarget in normalized "muscle space," then IK-correct hands/feet — the
principled fix for foot slide, heavyweight.
- ossos (Ubisoft IK-Rig style): motion as normalized IK targets re-solved on the target —
the modern answer when proportions differ wildly. Prototype-quality.
- Root Motionist add-on: hip→root channel migration pattern, if we ever need root-motion
variants (currently banned in-game).
---
## 3. Published Claude skills — what exists, what to borrow
Nothing published covers our exact lane (FBX skeletal retarget → game GLB with numeric
animation QC); our `loop-qc` agent is already ahead of public art. Reusable pieces:
- **blender-motion-state-inspection** ([SKILL.md](https://github.com/ksmithRenweb/everything-claude-code/blob/main/skills/blender-motion-state-inspection/SKILL.md)) —
"measure, don't eyeball" QC playbook with thresholds worth adopting into a broader
motion-QC gate alongside loop-qc: ground penetration >12 cm visible; scale drift >5% =
rig problem; root heading jump >30°/frame suspicious. Sample frames *likely to expose
errors* (contact, airborne, extremes), check source integrity before blaming the
retarget, report facts (frame numbers, coordinates) before verdicts.
- **blender-toolkit** ([kevinbadi/blender-skills](https://github.com/kevinbadi/blender-skills)) —
only published skill with a real retargeting workflow. Borrow: named bone-map presets as
skill assets, and a **mapping-quality score** (Excellent/Good/Fair/Poor by critical-bone
coverage) that gates whether the pipeline may proceed autonomously — direct fit for the
plan's FR-2 (reject with unmapped-bone list).
- **blender-kiln** ([elithril](https://github.com/elithril/blender-kiln)) — best-structured
3D skill: thin SKILL.md dispatcher + deep `references/` files, phase-gated pipeline with
mandatory state-read before each phase, scale sanity via reference dimensions
(character ≈1.75 m — same trick as our 1.700 m AccuRig rule), batch manifest pattern.
- **VibeCAD render-glb** ([rawwerks](https://github.com/rawwerks/VibeCAD)) — GLB→PNG so the
agent can *see* its output. Rendering 34 keyframes of a retargeted clip to PNG would
partially automate the mandatory human-eyeball QC-3 step (catches upside-down/T-posed/
exploded output; still not a substitute for motion judgment).
- **anthropics/skills docx pattern** — the canonical file-conversion skill structure our
`animation` skill could adopt more fully: decision table routing by source type
(Mixamo / CC / Kevin → script + bone map), explicit Gotchas section (AccuRig 4 cm
offset, Dummy export, External Motion skating), pinned script invocations, and an
explicit SHIP / NO-SHIP verdict at the end.
- **gltf-validator gap**: no published skill or agent workflow wires
[Khronos glTF-Validator](https://github.com/KhronosGroup/glTF-Validator) in as a
machine-checkable gate — everyone stops at `gltf-transform inspect` + eyeballing.
Adding `gltf-validator -o json` after export is cheap and genuinely novel.
---
## 4. What this validates in the existing plan
`plans/fbx-pipeline-plan-2026-07-21.md` is independently confirmed by this research:
- Unified retargeter + JSON rig maps (P2/FR-3) = avatar-asset-pipeline's architecture.
- Reject-with-unmapped-list (FR-2) = blender-toolkit's mapping-quality gate.
- `verify_target.py` pinned-rig check (QC-1) = the glTF-Blender-IO rest-pose-baseline risk.
- Intake T-pose WARN (IC-3) = every retargeter's rest-alignment prerequisite.
- Fail-loud + atomic writes + provenance (NFR-1/2) = table stakes in every surveyed tool.
Two additions this research argues for beyond the plan:
1. A **`rest_correction` per-bone quaternion block** in the rig-map JSON schema (turns the
A-pose fix into data).
2. A **post-export normalize step** in FR-1's single command: quaternion-neighborhood fix
(in-baker) → `gltf-transform resample``gltf-validator` gate → optional render-to-PNG
contact sheet.
+144
View File
@@ -0,0 +1,144 @@
# iClone bridge
A terminal-to-iClone bridge: drive iClone 8 from the shell (query the scene,
run RLPy code, trigger exports) without clicking through the UI.
iClone 8's Python API (`RLPy`) only runs *inside* iClone -- there is no
external/network API. TinqsBridge is a small auto-load iClone plugin that
opens a localhost TCP socket server inside iClone and executes whatever
Python source is sent to it, on iClone's own Qt main thread.
## Pieces
- `tools/iclone_bridge/TinqsBridge/main.py` -- the plugin source of truth
(lives in this repo, gets copied into iClone).
- `tools/install_iclone_bridge.ps1` -- copies the plugin into iClone's
auto-load folder.
- `tools/iclone_bridge.py` -- the terminal client (Python 3.12, stdlib only).
## Install / update
```powershell
powershell -File tools\install_iclone_bridge.ps1
```
This copies `tools/iclone_bridge/TinqsBridge/` to
`A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\`, overwriting
any existing copy. It is idempotent and only ever touches the `TinqsBridge`
subfolder -- other plugins under `OpenPlugin\` (AIStudio, MotionLIVE,
VideoMocap) are never touched.
**iClone must be restarted** after every install/update to load the new
plugin code -- there is no live-reload of `main.py` on disk. (You *can*
hot-swap runtime *behavior* once the bridge is up, by `--exec`-ing new code
through it -- see "Persistent namespace" below -- but that only affects the
running session, not what gets loaded on the next restart.)
If iClone prompts with a Windows Firewall dialog for a `127.0.0.1` bind,
allow it -- the bind is localhost-only either way.
## CLI usage
```
python tools/iclone_bridge.py --ping
python tools/iclone_bridge.py --exec "result = 1 + 1"
python tools/iclone_bridge.py --file some_script.py
python tools/iclone_bridge.py --port 18801 --timeout 120 --exec "..."
```
- `--ping` checks the bridge is alive and prints the product name, iClone
version, and the embedded Python version (via `RApplication` +
`sys.version`).
- `--exec CODE` runs a snippet of Python source inside iClone.
- `--file PATH` runs a `.py` file's contents inside iClone.
- `--port` (default `18800`) must match `TINQS_ICLONE_BRIDGE_PORT` if you've
overridden it for the iClone process's environment.
- `--timeout` (default `30` seconds) is the client socket timeout.
On a connection failure the client exits `1` with a hint to check that
iClone is running and was restarted after install. On an exec-level error
(an exception raised by your code inside iClone) it also exits `1`, printing
the traceback captured from iClone.
### Example: list scene objects
```
python tools/iclone_bridge.py --exec "result = [o.GetName() for o in RLPy.RScene.GetAvatars()] + [o.GetName() for o in RLPy.RScene.GetProps()]"
```
## The `result` convention
Whatever code you send is `exec()`'d inside iClone. If it sets a variable
named `result`, that becomes the JSON `result` field of the response
(falling back to `repr()` if it isn't JSON-serializable -- RLPy objects
generally aren't). If your code doesn't set `result`, the response's
`result` is `null`. `stdout`/`stderr` printed during execution are captured
and returned as the response's `stdout` field.
## Persistent namespace
All code you send runs against **one shared namespace dict** that persists
for the life of the iClone session (until iClone is restarted or the plugin
reloaded). This means:
```
python tools/iclone_bridge.py --exec "x = 41"
python tools/iclone_bridge.py --exec "result = x + 1" # -> 42
```
variables and imports from one `--exec`/`--file` call are visible to the
next one. `RLPy` is pre-imported into the namespace. Each new client
connection is a fresh TCP connection, but the *iClone-side* namespace is not
reset per-connection.
## Error resilience
An exception in your code (e.g. `1/0`) produces `ok: false` with the full
traceback in `error` -- the bridge itself keeps running and will serve the
next request normally; you don't need to restart iClone after a bad
`--exec`.
## Log file
`%TEMP%\tinqs_iclone_bridge.log` -- append-only, timestamped lines for:
plugin startup, the bind result (and port), each request's id + ok/error
status, and shutdown. If `--ping` can't connect, or the plugin doesn't seem
to be running after a restart, check this file first. If it's not being
created at all, the plugin likely failed to load -- check iClone's
**Script > Console Log** window too.
## Threading rule (read this before extending the plugin)
`RLPy` is **not thread-safe** and must only be touched on iClone's Qt main
thread. In `main.py`:
- The socket-accept thread and per-connection handler threads
(`_accept_loop`, `_handle_conn`) only move bytes and put/get on a
`queue.Queue`. They never call `RLPy` or touch Qt objects.
- A `QTimer` created in `initialize_plugin()` (which itself runs on the main
thread) polls that queue every ~50 ms and does all the actual `exec()` /
`RLPy` work in `_drain_queue()` / `_execute()`.
If you add new functionality to the plugin, keep any RLPy-touching code
inside that QTimer callback path. Calling RLPy from a socket thread can
crash or corrupt iClone.
## Long-running code freezes iClone's UI
Submitted code runs **synchronously on the Qt main thread**. iClone's UI
(and the bridge itself, since the same timer that answers other requests is
blocked) will freeze for the duration of your `--exec`/`--file` call. This
is fine for anything that takes a few seconds (an FBX export blocks the UI
anyway) but avoid submitting code with long sleeps or unbounded loops.
## Environment variable
- `TINQS_ICLONE_BRIDGE_PORT` -- set in iClone's process environment before
launch to override the default port `18800`. Pass the matching `--port` to
the client.
## Security note
The bridge executes arbitrary Python with full `RLPy` access. It only binds
to `127.0.0.1` -- there is no authentication beyond "only processes on this
machine can connect." Do not change the bind address.
+93
View File
@@ -0,0 +1,93 @@
# Marvelous Designer Bridge (TinqsMDBridge)
Live control of Marvelous Designer's Python scripting API from the terminal,
sibling of the iClone bridge (`docs/iclone-bridge.md`): a socket server
plug-in inside the app + a thin JSON-over-TCP client.
**Key architectural difference from the iClone bridge, found the hard way
(2026-07-30, MD 2026 Personal):** MD's embedded Python (3.11.8) does NOT
schedule background threads while MD idles, and ships no Python Qt binding —
so both the daemon-thread server and the QTimer executor patterns are dead on
arrival (a threaded server binds, the OS accepts connections, and every
request times out). Instead the bridge is a **blocking main-thread serve
loop**: clicking the plugin starts a *bridge session* during which **MD's UI
is frozen** ("Not Responding" is normal) and all api calls run on the main
thread. The session ends via `--stop` or an idle timeout (default 14400 s / 4 h —
the escape hatch, since a frozen UI can't be clicked).
| | iClone bridge | MD bridge |
|---|---|---|
| Plug-in source | `tools/iclone_bridge/TinqsBridge/main.py` | `tools/md_bridge/TinqsMDBridge.py` |
| Client | `tools/iclone_bridge.py` | `tools/md_bridge.py` |
| Port | 18800 | **18900** (`TINQS_MD_BRIDGE_PORT` overrides) |
| Install | ps1 copies to OpenPlugin, auto-loads | **manual register**, click = start session |
| Executor | QTimer on Qt main thread, app stays live | main-thread serve loop, **UI frozen during session** |
| Session end | app exit | `--stop`, or idle `TINQS_MD_BRIDGE_IDLE` (4 h default) |
| Embedded Python | 3.8 | 3.11.8 |
| Log | `%TEMP%\tinqs_iclone_bridge.log` | `%TEMP%\tinqs_md_bridge.log` |
## Install (one-time)
1. Marvelous Designer > **Plugin** tab > **Plug-in Manager** > **+ ADD**
2. Select `tools/md_bridge/TinqsMDBridge.py`, name it `TinqsMDBridge`, OK.
âš  Unverified: whether the Plug-in Manager references the .py in place or
copies it. `--ping` returns a `source` field — if it isn't the repo path,
MD copied it and **every edit to the plug-in needs remove + re-ADD**. If a
click appears to run stale code (old prints, old behavior), re-register.
## Session workflow
```bash
# 1. In MD: click Plugin > TinqsMDBridge (UI freezes -- session active)
python tools/md_bridge.py --ping # 2. verify
python tools/md_bridge.py --exec "result = dir(pattern_api)"
python tools/md_bridge.py --file draft_tshirt.py
python tools/md_bridge.py --timeout 300 --exec "utility_api.Simulate(200)"
python tools/md_bridge.py --stop # 3. unfreeze MD
```
Wire protocol and `result` convention are identical to the iClone bridge:
newline-delimited JSON, one request per connection; set `result` in submitted
code to get a JSON value back; stdout/stderr captured; exceptions return
`ok:false` + traceback without ending the session. The exec namespace
persists across requests within a session (NOT across sessions) and is
pre-seeded with:
- MD api modules that imported successfully: `import_api`, `export_api`,
`pattern_api`, `fabric_api`, `utility_api`, `ApiTypes` (all 6 confirmed
importable in MD 2026 Personal)
- `BRIDGE` — info dict: `version`, `mode` ("mainloop"), `port`,
`idle_timeout_s`, `source`, `api_modules`, `api_missing`, `python`
## Day-1 checklist (next session)
```bash
python tools/md_bridge.py --ping # mode=mainloop, source path?
python tools/md_bridge.py --exec "result = {m: [f for f in dir(globals()[m]) if not f.startswith('_')] for m in BRIDGE['api_modules']}"
# dump the REAL api surface (docs are incomplete)
python tools/md_bridge.py --file tools/md_bridge/smoke_test.py --timeout 180
# signatures + square-of-fabric + Simulate + snapshot
python tools/md_bridge.py --stop
```
Open question the smoke test answers: do viewport snapshots
(`ExportSnapshot3D`) render while the Qt event loop is blocked? If not,
plan B is pumping events from inside the loop or snapshotting after --stop.
Then the real milestones:
- import `Ariki_Female_QuatSkin` as FBX avatar (Blender-convert the GLB
first), drape a rectangle, snapshot.
- t-shirt + shorts from `tools/tailor/lena_measurements.json` drafts.
- EveryWear rig + GLB export, check bone names == Quaternius.
## Gotchas
- **A session must be started by a human click** in MD — plan work in
batches, and always `--stop` when done so Jeremy gets his UI back.
- Long `Simulate()` calls: raise client `--timeout`; the idle timer only
resets when a request *completes*, so a 4-minute drape is safe but two
6-minute gaps in a row end the session.
- Windows may dim the MD window and offer to kill it — decline; the
process is healthy.
- Port clash: 18900 chosen to coexist with iClone's 18800.