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>
9.7 KiB
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) +RIObjectbase class (stub at line 5142):RLPy.RScene.GetAvatars()—@staticmethod, returns avatar objectsRLPy.RScene.GetProps()—@staticmethod, returns prop objectsRIObject.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
--pingsent immediately before the report arrived had already returned a correct, real response (iClone [8, 73, 5509], embedded Python3.8.8) — data that can only come from a runningRApplicationcall 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 throughoutmain.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 bypy_compileunder 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/--filecall. 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:18800and execute arbitrary Python with full RLPy access. This is the documented security model (seedocs/iclone-bridge.md) — do not bind to anything other than127.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;
resultfalls back torepr()in that case, which is often not round-trippable — fine for human-readable output, not for structured chaining of RLPy object handles across separate--execcalls (though the persistent namespace means an object can still be referenced by variable name across calls without ever being serialized). - Stale port handling:
SO_REUSEADDRplus 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.