Files

109 lines
5.3 KiB
Markdown
Raw Permalink Normal View History

# Blender Bridge (TinqsBlenderBridge)
Live control of a **running, interactive** Blender session from the terminal —
third sibling of the iClone (`docs/iclone-bridge.md`) and MD
(`docs/md-bridge.md`) bridges: a socket server inside the app + a thin
JSON-over-TCP client, same wire protocol and `result` convention.
Distinct from the headless pipeline scripts (`tools/cc_retarget.py`,
`tools/tailor/*`), which spawn `blender --background` and exit. Those never
touch the open GUI. This bridge is for driving the window Jeremy is
*looking at* — inspecting a selection, nudging a mesh, checking a drape.
**The good news vs. MD:** Blender has `bpy.app.timers`, so the server polls a
non-blocking socket from a main-thread timer. **The UI stays fully
interactive** — no frozen-window session like MD. The UI only stalls for the
duration of a single `exec`.
| | iClone bridge | MD bridge | Blender bridge |
|---|---|---|---|
| Server source | `tools/iclone_bridge/TinqsBridge/main.py` | `tools/md_bridge/TinqsMDBridge.py` | `tools/blender_bridge/tinqs_blender_bridge.py` |
| Client | `tools/iclone_bridge.py` | `tools/md_bridge.py` | `tools/blender_bridge.py` |
| Port | 18800 | 18900 | **19000** (`TINQS_BLENDER_BRIDGE_PORT` overrides) |
| Install | ps1 → OpenPlugin, auto-loads | manual register, click = session | ps1 → `scripts/startup/`, auto-starts |
| Executor | QTimer on Qt main thread | main-thread loop, **UI frozen** | `bpy.app.timers`, **UI stays live** |
| Session end | app exit | `--stop` / 4 h idle | `--stop`, or app exit (no idle timeout) |
| Embedded Python | 3.8 | 3.11.8 | 3.13.9 (Blender 5.1.2) |
| Log | `%TEMP%\tinqs_iclone_bridge.log` | `%TEMP%\tinqs_md_bridge.log` | `%TEMP%\tinqs_blender_bridge.log` |
## Install (one-time)
```powershell
powershell -File tools\install_blender_bridge.ps1
```
Copies the server into every `%APPDATA%\Blender Foundation\Blender\<ver>\scripts\startup\`.
Blender imports startup modules at launch and calls their `register()`, so
**the bridge auto-starts on port 19000 from the next launch onward** — zero
clicks, forever.
**To start it in an ALREADY-OPEN Blender** (a session that predates the
install — the common case, and one that matters when the window has unsaved
work you don't want to lose to a restart):
> Scripting workspace → **Open** → `tools\blender_bridge\tinqs_blender_bridge.py` → **Run Script** (▶)
Re-running is safe: a singleton on `builtins` closes the previous socket and
retires its timer before rebinding, so no leak and no "port busy" on reload.
## Workflow
```bash
python tools/blender_bridge.py --ping # blender ver, open .blend
python tools/blender_bridge.py --exec "result = [o.name for o in bpy.data.objects]"
python tools/blender_bridge.py --file edit_mesh.py
python tools/blender_bridge.py --timeout 300 --file long_bake.py
python tools/blender_bridge.py --stop # server off, Blender lives on
```
**Use `--file`, not `--exec`, for anything with quotes.** PowerShell strips
inner double quotes from native-command args, so
`--exec "bpy.data.objects[\"Cube\"]"` arrives as `bpy.data.objects[Cube]`
`NameError: name 'Cube' is not defined`. Write a scratchpad .py and use
`--file`.
The exec namespace persists across requests within a Blender session and is
pre-seeded with `bpy`, `view3d_override()`, and a `BRIDGE` info dict.
## The context trap (read this before using bpy.ops)
Timer callbacks run with **no window/area in `bpy.context`**, so operators
that need one fail with *"context is incorrect"*. Two ways out:
1. **Prefer the data API**`bpy.data`, `obj.location`, `mesh.vertices[i].co`,
or `bmesh` for topology. Context-free, and the right tool anyway.
2. **Override when an operator truly needs a viewport** — the seeded helper
returns the kwargs:
```python
with bpy.context.temp_override(**view3d_override()):
bpy.ops.object.shade_smooth()
```
## Verified (2026-08-04, Blender 5.1.2, Python 3.13.9)
Smoke-tested end to end in a throwaway GUI instance: socket up ~3 s after
launch; `--ping`; direct vertex edit; `bmesh` subdivide (8 → 26 verts);
`shade_smooth` via `temp_override`; `--stop` closed the port cleanly and
**Blender stayed alive and responsive**.
## Two bugs found and fixed during that verification
Both are the kind that silently produce "connection refused", so they're
worth remembering if the bridge is ever ported or rewritten:
1. **`bpy.data` is restricted during startup registration.** Modules in
`scripts/startup/` register while `bpy.data` is still a `_RestrictData`
stub — reading `bpy.data.filepath` there raises
`AttributeError: '_RestrictData' object has no attribute 'filepath'`,
which aborts `register()` and the socket never binds, leaving *no log
file at all*. Fix: `register()` only schedules a one-shot
`bpy.app.timers` callback; the real start runs after boot, when the full
API is live. Diagnose this class of failure with
`blender --background --python-expr "print('x')"` — startup tracebacks
print to stdout. (Do **not** pass `--factory-startup`; it disables user
scripts and hides the very thing you're testing.)
2. **Unregistering a timer from inside its own callback is an error.** The
`__STOP__` path called `stop()` from within `_poll`. Fix: `stop(in_timer=True)`
just closes the socket and lets the callback's `return None` retire the timer.