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:
@@ -0,0 +1,191 @@
|
||||
# Plan: iClone 8 Python API bridge — control iClone from the terminal
|
||||
|
||||
**Status:** ready for implementation · **Author:** Fable 5 session 2026-07-21 · **Implementer:** GLM session
|
||||
|
||||
## 0. Context (you have no other context — read this fully)
|
||||
|
||||
You are on Jeremy's **Windows 11 PC**. This repo (`C:\Users\Jeremy\tinqs\animation`) is the
|
||||
animation bridge for the ariki-game project: animations are authored in iClone 8, exported
|
||||
as FBX into `exchange/`, and retargeted with `tools/cc_retarget.py`. Today that pipeline is
|
||||
entirely manual on the iClone side (Jeremy clicks Export in the UI).
|
||||
|
||||
**Goal:** establish a programmatic connection to iClone 8 so terminal agents (Claude, GLM,
|
||||
scripts) can drive it — load motions, query the scene, trigger FBX exports — without Jeremy
|
||||
clicking through the UI each time.
|
||||
|
||||
**Why a bridge is needed:** iClone 8 has an embedded Python API (`RLPy` module) but **no
|
||||
external/network API of any kind**. Scripts only run *inside* iClone, loaded either from the
|
||||
Script menu or as auto-loading plugins. The standard pattern (used by MotionLIVE etc.) is a
|
||||
plugin that runs a localhost socket server inside iClone and executes commands sent to it.
|
||||
That is what you will build.
|
||||
|
||||
### Verified environment facts (checked 2026-07-21 — trust these)
|
||||
|
||||
- iClone 8 install: `A:\Program Files (x86)\iClone 8\` (note: **A: drive**, not C:).
|
||||
- Plugin auto-load folder: `A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\` —
|
||||
**verified writable without elevation**. Currently contains only stock plugins
|
||||
(AIStudio, MotionLIVE, VideoMocap). Do not touch those.
|
||||
- API stub for reference: `A:\Program Files (x86)\iClone 8\Bin64\RLPy.py` — this file
|
||||
lists every RLPy class/method with docstrings. **It is your API ground truth**; grep it
|
||||
before using any RLPy call. Do not trust API names from memory or old forum posts.
|
||||
- Embedded Python: `Bin64` contains BOTH `python38.dll` and `python310.dll` — do not
|
||||
assume which one hosts plugins. Write the plugin in **3.8-compatible syntax** (no
|
||||
`match`, no `X | Y` unions, no 3.9+ stdlib) and report the real version via the bridge
|
||||
itself (`sys.version`) in your results.
|
||||
- Qt: iClone embeds Qt 5.15 / PySide2 (`shiboken2` available in-process).
|
||||
- Client-side Python: 3.12.10 on PATH (`python`). Client must be **stdlib-only**.
|
||||
- iClone was RUNNING when this plan was written. A restart is required to load the new
|
||||
plugin — **Jeremy must do the restart himself** (he may have unsaved work). Never kill
|
||||
the iClone process.
|
||||
|
||||
### Reference docs (fetch only if stuck; the RLPy.py stub usually suffices)
|
||||
|
||||
- Plugin structure: https://wiki.reallusion.com/IC_Python_API:Your_First_iClone_Python_Plugin
|
||||
- IC8 API wiki: https://wiki.reallusion.com/IC8_Python_API
|
||||
- Official samples (QTimer/menu/PySide2 patterns): https://github.com/reallusion/iClone
|
||||
|
||||
## 1. Architecture
|
||||
|
||||
```
|
||||
┌─ this repo ──────────────────┐ ┌─ inside iClone 8 (Qt main thread) ─────────┐
|
||||
│ tools/iclone_bridge.py │ TCP │ OpenPlugin\TinqsBridge\main.py │
|
||||
│ (client, py3.12, stdlib) │◄──────►│ socket thread: accept + read/write ONLY │
|
||||
│ │ 127.0. │ queue → QTimer(50ms, main thread) drains, │
|
||||
│ tools/iclone_bridge/ │ 0.1: │ exec()s code with RLPy in namespace, │
|
||||
│ TinqsBridge/main.py (source)│ 18800 │ posts JSON reply back to socket thread │
|
||||
│ tools/install_iclone_bridge │ │ │
|
||||
│ .ps1 (copies to OpenPlugin) │ └────────────────────────────────────────────┘
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
**Non-negotiable threading rule:** RLPy is not thread-safe and must only be called on
|
||||
iClone's Qt main thread. The socket thread NEVER touches RLPy. It pushes requests onto a
|
||||
`queue.Queue`; a PySide2 `QTimer` created in `initialize_plugin()` (which runs on the main
|
||||
thread) fires every ~50 ms, drains the queue, executes, and hands the response back (a
|
||||
second queue or per-request `threading.Event` — your choice).
|
||||
|
||||
## 2. Wire protocol (keep it this simple)
|
||||
|
||||
Newline-delimited JSON over TCP, localhost only, one request/response pair at a time.
|
||||
|
||||
- Request: `{"id": 1, "code": "<python source>"}`
|
||||
- Success: `{"id": 1, "ok": true, "result": <json>, "stdout": "<captured prints>"}`
|
||||
- Failure: `{"id": 1, "ok": false, "error": "<full traceback>", "stdout": "..."}`
|
||||
|
||||
Execution semantics:
|
||||
|
||||
- `exec(code, ns, ns)` — one **persistent** namespace dict `ns` shared across all requests
|
||||
for the life of the iClone session, pre-seeded with `RLPy` imported. Same dict as globals
|
||||
AND locals (avoids the exec-scoping trap where nested functions can't see top-level names).
|
||||
- If the code sets a variable named `result`, that is the response `result`
|
||||
(JSON-serialize; on `TypeError` fall back to `repr()`). Otherwise `result` is null.
|
||||
Clear `ns["result"]` before each exec so stale values don't leak.
|
||||
- Capture stdout/stderr during exec (`contextlib.redirect_stdout/stderr`) into `stdout`.
|
||||
- Any exception → `ok:false` with `traceback.format_exc()`; the bridge itself must survive
|
||||
and serve the next request.
|
||||
|
||||
## 3. Deliverables
|
||||
|
||||
1. **`tools/iclone_bridge/TinqsBridge/main.py`** — the plugin (source of truth lives in the
|
||||
repo; the installer copies it out). Requirements:
|
||||
- `initialize_plugin()` (mandatory — iClone refuses to load the plugin without it).
|
||||
Wrap its entire body in try/except that writes `traceback.format_exc()` to the log
|
||||
file — plugin load failures are otherwise near-silent.
|
||||
- TCP server on `127.0.0.1:18800`, `SO_REUSEADDR`, port overridable via env var
|
||||
`TINQS_ICLONE_BRIDGE_PORT` read inside iClone. If the bind fails, log it clearly and
|
||||
return without crashing the plugin loader.
|
||||
- Socket-accept loop on a `daemon=True` thread; QTimer executor per §1. Keep
|
||||
module-level references to the timer and thread (a GC'd QTimer silently stops).
|
||||
- Log to `%TEMP%\tinqs_iclone_bridge.log` (append, timestamped lines: startup, bind
|
||||
result, each request id + ok/error, shutdown).
|
||||
- Optional nice-to-have, skip if it costs you more than ~20 min: a **Plugins ▸ Tinqs
|
||||
Bridge** menu entry showing status (port, requests served) via `RLPy.RUi.AddMenu` +
|
||||
`shiboken2.wrapInstance` (see the GitHub samples). The log file is the required
|
||||
status surface; the menu is garnish.
|
||||
2. **`tools/iclone_bridge.py`** — client, Python 3.12, stdlib only. API:
|
||||
`run(code, port=18800, timeout=30.0) -> dict` and `ping(port, timeout) -> dict`.
|
||||
CLI (match the repo's argparse style):
|
||||
```
|
||||
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` sends a snippet returning `{"product": ..., "version": ..., "python": sys.version}`
|
||||
— find the real product/version getters by grepping `RLPy.py` (look at `RApplication`
|
||||
and `RGlobal`; do not guess). Exit 0 on ok:true, 1 on ok:false or connection failure,
|
||||
with a readable message either way (mention "is iClone running? was it restarted after
|
||||
install?" on connection-refused).
|
||||
3. **`tools/install_iclone_bridge.ps1`** — copies `tools/iclone_bridge/TinqsBridge/` →
|
||||
`A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\` (overwrite), prints
|
||||
what it did and reminds that iClone must be restarted. Idempotent.
|
||||
4. **`docs/iclone-bridge.md`** — short usage doc: what it is, install/update procedure,
|
||||
CLI examples, the `result` convention, the persistent-namespace behavior, the log file
|
||||
location, the threading rule for anyone extending the plugin, and the "long-running
|
||||
code freezes the iClone UI" warning.
|
||||
5. **Results report `plans/iclone-api-bridge-results-2026-07-21.md`**: actual embedded
|
||||
Python version discovered, RLPy calls used for ping, smoke-test transcript (§5), any
|
||||
deviations from this plan with reasons, and known limitations.
|
||||
|
||||
## 4. Implementation order
|
||||
|
||||
1. Grep `A:\Program Files (x86)\iClone 8\Bin64\RLPy.py` for the calls you need
|
||||
(`RApplication`, `RGlobal`, `RScene`, `RFileIO`) and note exact signatures.
|
||||
2. Write plugin + client + installer.
|
||||
3. Run the installer. Verify the files landed in OpenPlugin.
|
||||
4. **Stop and ask Jeremy to save his iClone work and restart iClone.** Do not proceed to
|
||||
smoke tests until he confirms. Never kill or restart the process yourself.
|
||||
5. Run smoke tests (§5). If the plugin didn't load: check the log file first, then
|
||||
iClone's **Script ▸ Console Log** window (ask Jeremy to read it out if needed).
|
||||
6. Write docs + results report.
|
||||
|
||||
## 5. Acceptance criteria (all must pass, transcript goes in the results report)
|
||||
|
||||
1. `python tools/iclone_bridge.py --ping` → ok, prints product name, iClone version, and
|
||||
embedded Python version.
|
||||
2. Scene query: `--exec` a snippet listing scene object names via `RScene` (e.g. avatars +
|
||||
props; exact API from the stub) returns a JSON list without error.
|
||||
3. Namespace persistence: `--exec "x = 41"` then `--exec "result = x + 1"` → `42`.
|
||||
4. Error resilience: `--exec "1/0"` → `ok:false` with a ZeroDivisionError traceback, AND
|
||||
a follow-up `--ping` still succeeds.
|
||||
5. stdout capture: `--exec "print('hi'); result = True"` → `stdout` contains `hi`.
|
||||
6. Bridge survives ≥ 2 sequential client connections (each CLI call is a new connection).
|
||||
|
||||
## 6. Guardrails — do NOT
|
||||
|
||||
- Do not kill, restart, or send input to the running iClone process. Restarts are
|
||||
Jeremy's job (step 4.4).
|
||||
- Do not modify anything in `OpenPlugin\` other than creating/updating `TinqsBridge\`.
|
||||
- Do not modify existing repo tools (`cc_retarget.py`, `loop_qc.py`, `loop_fix.py`,
|
||||
`dance_profile.py`, etc.), anything in `.claude/`, `docs/` (except the new
|
||||
`docs/iclone-bridge.md`), or `exchange/`.
|
||||
- Do not bind to anything other than `127.0.0.1`. This is an arbitrary-code-execution
|
||||
endpoint by design; localhost-only is the security model.
|
||||
- Do not install any packages — plugin uses iClone's embedded stdlib + PySide2; client
|
||||
uses Python 3.12 stdlib.
|
||||
- Do not commit or push. Leave everything in the working tree for Jeremy's review.
|
||||
|
||||
## 7. Known traps
|
||||
|
||||
- **RLPy off the main thread** crashes or corrupts iClone. The QTimer pattern in §1 is
|
||||
mandatory; the socket thread only moves bytes.
|
||||
- **QTimer lifetime:** keep a module-level reference; also create it *in*
|
||||
`initialize_plugin()` (main thread), never in the socket thread.
|
||||
- `initialize_plugin()` missing, misnamed, or raising → plugin silently fails to load.
|
||||
Hence the try/except-to-logfile around everything.
|
||||
- **exec scoping:** pass the same dict as globals and locals, or nested
|
||||
functions/comprehensions in user code will throw NameError on top-level names.
|
||||
- Commands run synchronously on the Qt main thread → **iClone's UI freezes for the
|
||||
duration**. Fine for seconds-long operations (an FBX export blocks anyway); document it.
|
||||
- JSON can't serialize RLPy objects — always try/except the dumps and fall back to repr.
|
||||
- Stale port from a previous iClone instance: `SO_REUSEADDR` + a clear log line beats a
|
||||
cryptic bind exception.
|
||||
- Windows Firewall shouldn't prompt for a 127.0.0.1 bind, but if a prompt appears Jeremy
|
||||
should allow it (note in docs).
|
||||
- Socket recv: requests may arrive fragmented — read until `\n`, decode UTF-8.
|
||||
- Both python38.dll and python310.dll exist in Bin64 — write for 3.8, verify via ping.
|
||||
- Old wiki pages mix iClone 7 and 8 APIs; several IC7 calls were renamed/removed. The
|
||||
local `RLPy.py` stub outranks every web source.
|
||||
- After the bridge is up once, you can iterate on *executor* behavior by exec-ing new
|
||||
code through the bridge itself (hot-swap) — but changes to `main.py` on disk still need
|
||||
an iClone restart to load. Batch your plugin edits to minimize restart requests.
|
||||
Reference in New Issue
Block a user