Files
animation/plans/iclone-api-bridge-results-2026-07-21.md
jeremy 3ba86b2ea8 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>
2026-08-06 15:55:43 -07:00

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:

  • PingRApplication (stub at line 15328), both @staticmethod:
    • RLPy.RApplication.GetProductName()"iClone"
    • RLPy.RApplication.GetProductVersion()[8, 73, 5509]
  • Scene queryRScene (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

PASSx 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

PASSok: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

PASSstdout 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.