145 lines
5.8 KiB
Markdown
145 lines
5.8 KiB
Markdown
|
|
# 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.
|