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
@@ -0,0 +1,218 @@
# Results: iClone 8 Python API bridge — 2026-07-21
Implements `plans/iclone-api-bridge-plan-2026-07-21.md`. All acceptance criteria in
plan §5 pass. Written after Jeremy restarted iClone with the TinqsBridge plugin
installed at `A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\main.py`.
## Embedded Python version discovered
```
3.8.8 (tags/v3.8.8:024d805, Feb 19 2021, 13:18:16) [MSC v.1928 64 bit (AMD64)]
```
Confirms iClone 8 hosts plugins on **python38.dll**, not python310.dll (Bin64 ships
both). The 3.8-compatible-syntax requirement in the plan was the right call.
## RLPy calls used
Found by grepping `A:\Program Files (x86)\iClone 8\Bin64\RLPy.py` directly (not from
memory or web sources), per the plan's ground-truth rule:
- **Ping** — `RApplication` (stub at line 15328), both `@staticmethod`:
- `RLPy.RApplication.GetProductName()``"iClone"`
- `RLPy.RApplication.GetProductVersion()``[8, 73, 5509]`
- **Scene query** — `RScene` (stub at line 14872) + `RIObject` base class (stub at
line 5142):
- `RLPy.RScene.GetAvatars()``@staticmethod`, returns avatar objects
- `RLPy.RScene.GetProps()``@staticmethod`, returns prop objects
- `RIObject.GetName()` (inherited by both) — display name of each returned object
Both `GetAvatars`/`GetProps` stubs are argument-less in the common call form (the
SWIG stub shows `GetAvatars(*args)` for an overload iClone doesn't require callers to
use); no arguments were needed for a flat name listing.
## Smoke-test transcript (plan §5, all six items)
Log file: `C:\Users\Jeremy\AppData\Local\Temp\tinqs_iclone_bridge.log` (this is the
resolved `%TEMP%` for Jeremy's user session — not `C:\Windows\Temp`). Full contents
after the run:
```
[2026-07-21 13:22:10] ============================================================
[2026-07-21 13:22:10] TinqsBridge initialize_plugin() starting, requested port 18800
[2026-07-21 13:22:10] bound OK on 127.0.0.1:18800
[2026-07-21 13:22:10] accept thread started
[2026-07-21 13:22:10] QTimer executor started (50 ms poll)
[2026-07-21 13:22:10] TinqsBridge ready on 127.0.0.1:18800. Python: 3.8.8 (tags/v3.8.8:024d805, Feb 19 2021, 13:18:16) [MSC v.1928 64 bit (AMD64)]
[2026-07-21 13:22:54] request id=1784665374837 ok=true
[2026-07-21 13:23:39] request id=1784665419368 ok=true
[2026-07-21 13:24:53] request id=1784665493184 ok=true
[2026-07-21 13:24:53] request id=1784665493333 ok=true
[2026-07-21 13:25:44] request id=1784665544442 ok=true
[2026-07-21 13:25:44] request id=1784665544559 ok=true
[2026-07-21 13:26:31] request id=1784665591763 ok=false: ZeroDivisionError: division by zero
[2026-07-21 13:26:31] request id=1784665591935 ok=true
[2026-07-21 13:26:32] request id=1784665592046 ok=true
[2026-07-21 13:26:42] request id=1784665602593 ok=true
[2026-07-21 13:26:42] request id=1784665602745 ok=true
```
### 1. `--ping`
```
$ python tools/iclone_bridge.py --ping
ok: iClone [8, 73, 5509] (embedded python 3.8.8)
$ echo $?
0
```
**PASS** — product name, iClone version, and embedded Python version all present.
### 2. Scene query via RScene
```
$ python tools/iclone_bridge.py --exec "result = [o.GetName() for o in RLPy.RScene.GetAvatars()] + [o.GetName() for o in RLPy.RScene.GetProps()]"
[
"Shadow Catcher"
]
$ echo $?
0
```
**PASS** — valid JSON list, no error. (Scene had no avatars loaded at test time, just
the default "Shadow Catcher" prop — expected for whatever project state iClone was in
after restart; the query mechanism itself is what's under test.)
### 3. Namespace persistence
```
$ python tools/iclone_bridge.py --exec "x = 41"
null
$ echo $?
0
$ python tools/iclone_bridge.py --exec "result = x + 1"
42
$ echo $?
0
```
**PASS**`x` set in one connection was visible from a second, separate connection;
result was exactly `42`.
### 4. Error resilience
```
$ python tools/iclone_bridge.py --exec "1/0"
error: Traceback (most recent call last):
File "A:/Program Files (x86)/iClone 8/Bin64/OpenPlugin/TinqsBridge/main.py", line 120, in _execute
exec(code, _ns, _ns)
File "<string>", line 1, in <module>
ZeroDivisionError: division by zero
$ echo $?
1
$ python tools/iclone_bridge.py --ping
ok: iClone [8, 73, 5509] (embedded python 3.8.8)
$ echo $?
0
```
**PASS**`ok:false` with a full ZeroDivisionError traceback, exit code 1; the
follow-up `--ping` succeeded immediately after, confirming the bridge survives an
exec-time exception.
### 5. stdout capture
```
$ python tools/iclone_bridge.py --exec "print('hi'); result = True"
hi
true
$ echo $?
0
```
**PASS**`stdout` contained `hi` (client prints stdout above the result line); JSON
result `true`.
### 6. ≥2 sequential connections
```
$ python tools/iclone_bridge.py --ping
ok: iClone [8, 73, 5509] (embedded python 3.8.8)
$ python tools/iclone_bridge.py --ping
ok: iClone [8, 73, 5509] (embedded python 3.8.8)
```
**PASS** — two back-to-back separate client invocations (separate TCP connections)
both succeeded. In total the log shows 11 requests served across 9 distinct CLI
invocations plus 2 earlier ad-hoc pings during initial verification, all without the
bridge needing a restart.
**All six acceptance criteria: PASS.**
## Notable event during this session: "incompatible plugin" dialog report
Partway through this session, Jeremy reported that on the iClone restart he saw a
dialog reading the plugin name as "simbridge" (almost certainly TinqsBridge) and
stating it was "not compatible with this iClone." This raised a real concern that the
plugin had failed to load.
**Investigation found no evidence of a load failure.** At the time that report came
in:
- The log file already existed and showed a completely clean startup sequence
(bind OK, accept thread started, QTimer started) timestamped `13:22:10`.
- A `--ping` sent immediately before the report arrived had already returned a
correct, real response (`iClone [8, 73, 5509]`, embedded Python `3.8.8`) — data
that can only come from a running `RApplication` call inside iClone.
- Every subsequent test in this session (all six acceptance criteria) passed cleanly
against the same running instance with no further restart.
Root cause of the dialog was not conclusively identified, but the plugin was **not**
affected by it: `initialize_plugin()` ran, bound the socket, and served requests
successfully throughout. Plausible explanations (unverified): the dialog belonged to
one of the stock plugins (AIStudio/MotionLIVE/VideoMocap) and was misattributed by
name similarity in a quick read, or it's a generic Reallusion update/compatibility
notice unrelated to plugin load success. No code changes were made in response to
this report since the evidence contradicted the failure hypothesis; no second restart
was requested. Recommend Jeremy re-check next time whether the dialog names
"TinqsBridge" specifically, and if so, ask him to read the exact wording verbatim
before further diagnosis.
## Deviations from the plan
- Skipped the optional **Plugins ▸ Tinqs Bridge** menu entry (`RUi.AddMenu` /
`shiboken2`) — plan explicitly allowed skipping if it costs more than ~20 minutes.
The log file (required) fully covers the status surface.
- Added `dispose_plugin()` (not explicitly requested) for clean socket/timer teardown
on unload — logs shutdown + total requests served. Low-risk addition consistent
with the guardrail against silent failures.
- Used `str.format()` instead of f-strings throughout `main.py`, specifically to
avoid relying on Python 3.12-only relaxed f-string grammar that could differ under
3.8 — a conservative choice made before the actual embedded version (3.8.8) was
confirmed by this session's ping.
- A stray `__pycache__/*.pyc` (generated locally by `py_compile` under host Python
3.12) was copied by the installer on its first run; it was harmless (iClone never
reads a 3.12 pyc) but was deleted from both the repo source and the installed copy
before the final install, and the installer was re-run to confirm a clean,
single-file (`main.py`) payload.
## Known limitations
- **UI freeze during exec**: submitted code runs synchronously on iClone's Qt main
thread; iClone's UI (and the bridge's own responsiveness to other requests) blocks
for the duration of any `--exec`/`--file` call. Fine for short operations; avoid
long sleeps/loops.
- **No auth beyond localhost bind**: any process on the machine can connect to
`127.0.0.1:18800` and execute arbitrary Python with full RLPy access. This is the
documented security model (see `docs/iclone-bridge.md`) — do not bind to anything
other than `127.0.0.1`.
- **One request per TCP connection**: the wire protocol is not pipelined; each CLI
invocation opens a fresh socket. This is by design (plan §2) and was exercised by
acceptance criterion 6.
- **JSON-serialization fallback**: RLPy objects generally are not JSON-serializable;
`result` falls back to `repr()` in that case, which is often not round-trippable —
fine for human-readable output, not for structured chaining of RLPy object handles
across separate `--exec` calls (though the persistent namespace means an object can
still be *referenced by variable name* across calls without ever being serialized).
- **Stale port handling**: `SO_REUSEADDR` plus a clear bind-failure log line are in
place, but a truly stuck listener from a crashed prior iClone process would still
require killing that process manually (never automated per guardrails).
- **Scene query test result was trivial**: only one prop ("Shadow Catcher") was in
the scene during smoke testing — no avatars were loaded, so the acceptance test
validates the query *mechanism* (valid JSON list, no error) rather than exercising
avatar-name output specifically. Worth re-running with an avatar loaded before
relying on this for real motion/scene-driving work.