Files

145 lines
5.8 KiB
Markdown
Raw Permalink Normal View History

# iClone bridge
A terminal-to-iClone bridge: drive iClone 8 from the shell (query the scene,
run RLPy code, trigger exports) without clicking through the UI.
iClone 8's Python API (`RLPy`) only runs *inside* iClone -- there is no
external/network API. TinqsBridge is a small auto-load iClone plugin that
opens a localhost TCP socket server inside iClone and executes whatever
Python source is sent to it, on iClone's own Qt main thread.
## Pieces
- `tools/iclone_bridge/TinqsBridge/main.py` -- the plugin source of truth
(lives in this repo, gets copied into iClone).
- `tools/install_iclone_bridge.ps1` -- copies the plugin into iClone's
auto-load folder.
- `tools/iclone_bridge.py` -- the terminal client (Python 3.12, stdlib only).
## Install / update
```powershell
powershell -File tools\install_iclone_bridge.ps1
```
This copies `tools/iclone_bridge/TinqsBridge/` to
`A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\`, overwriting
any existing copy. It is idempotent and only ever touches the `TinqsBridge`
subfolder -- other plugins under `OpenPlugin\` (AIStudio, MotionLIVE,
VideoMocap) are never touched.
**iClone must be restarted** after every install/update to load the new
plugin code -- there is no live-reload of `main.py` on disk. (You *can*
hot-swap runtime *behavior* once the bridge is up, by `--exec`-ing new code
through it -- see "Persistent namespace" below -- but that only affects the
running session, not what gets loaded on the next restart.)
If iClone prompts with a Windows Firewall dialog for a `127.0.0.1` bind,
allow it -- the bind is localhost-only either way.
## CLI usage
```
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` checks the bridge is alive and prints the product name, iClone
version, and the embedded Python version (via `RApplication` +
`sys.version`).
- `--exec CODE` runs a snippet of Python source inside iClone.
- `--file PATH` runs a `.py` file's contents inside iClone.
- `--port` (default `18800`) must match `TINQS_ICLONE_BRIDGE_PORT` if you've
overridden it for the iClone process's environment.
- `--timeout` (default `30` seconds) is the client socket timeout.
On a connection failure the client exits `1` with a hint to check that
iClone is running and was restarted after install. On an exec-level error
(an exception raised by your code inside iClone) it also exits `1`, printing
the traceback captured from iClone.
### Example: list scene objects
```
python tools/iclone_bridge.py --exec "result = [o.GetName() for o in RLPy.RScene.GetAvatars()] + [o.GetName() for o in RLPy.RScene.GetProps()]"
```
## The `result` convention
Whatever code you send is `exec()`'d inside iClone. If it sets a variable
named `result`, that becomes the JSON `result` field of the response
(falling back to `repr()` if it isn't JSON-serializable -- RLPy objects
generally aren't). If your code doesn't set `result`, the response's
`result` is `null`. `stdout`/`stderr` printed during execution are captured
and returned as the response's `stdout` field.
## Persistent namespace
All code you send runs against **one shared namespace dict** that persists
for the life of the iClone session (until iClone is restarted or the plugin
reloaded). This means:
```
python tools/iclone_bridge.py --exec "x = 41"
python tools/iclone_bridge.py --exec "result = x + 1" # -> 42
```
variables and imports from one `--exec`/`--file` call are visible to the
next one. `RLPy` is pre-imported into the namespace. Each new client
connection is a fresh TCP connection, but the *iClone-side* namespace is not
reset per-connection.
## Error resilience
An exception in your code (e.g. `1/0`) produces `ok: false` with the full
traceback in `error` -- the bridge itself keeps running and will serve the
next request normally; you don't need to restart iClone after a bad
`--exec`.
## Log file
`%TEMP%\tinqs_iclone_bridge.log` -- append-only, timestamped lines for:
plugin startup, the bind result (and port), each request's id + ok/error
status, and shutdown. If `--ping` can't connect, or the plugin doesn't seem
to be running after a restart, check this file first. If it's not being
created at all, the plugin likely failed to load -- check iClone's
**Script > Console Log** window too.
## Threading rule (read this before extending the plugin)
`RLPy` is **not thread-safe** and must only be touched on iClone's Qt main
thread. In `main.py`:
- The socket-accept thread and per-connection handler threads
(`_accept_loop`, `_handle_conn`) only move bytes and put/get on a
`queue.Queue`. They never call `RLPy` or touch Qt objects.
- A `QTimer` created in `initialize_plugin()` (which itself runs on the main
thread) polls that queue every ~50 ms and does all the actual `exec()` /
`RLPy` work in `_drain_queue()` / `_execute()`.
If you add new functionality to the plugin, keep any RLPy-touching code
inside that QTimer callback path. Calling RLPy from a socket thread can
crash or corrupt iClone.
## Long-running code freezes iClone's UI
Submitted code runs **synchronously on the Qt main thread**. iClone's UI
(and the bridge itself, since the same timer that answers other requests is
blocked) will freeze for the duration of your `--exec`/`--file` call. This
is fine for anything that takes a few seconds (an FBX export blocks the UI
anyway) but avoid submitting code with long sleeps or unbounded loops.
## Environment variable
- `TINQS_ICLONE_BRIDGE_PORT` -- set in iClone's process environment before
launch to override the default port `18800`. Pass the matching `--port` to
the client.
## Security note
The bridge executes arbitrary Python with full `RLPy` access. It only binds
to `127.0.0.1` -- there is no authentication beyond "only processes on this
machine can connect." Do not change the bind address.