3ba86b2ea8
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>
219 lines
9.7 KiB
Markdown
219 lines
9.7 KiB
Markdown
# 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.
|