# 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\\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.