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>
5.3 KiB
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 -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
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:
-
Prefer the data API —
bpy.data,obj.location,mesh.vertices[i].co, orbmeshfor topology. Context-free, and the right tool anyway. -
Override when an operator truly needs a viewport — the seeded helper returns the kwargs:
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:
bpy.datais restricted during startup registration. Modules inscripts/startup/register whilebpy.datais still a_RestrictDatastub — readingbpy.data.filepaththere raisesAttributeError: '_RestrictData' object has no attribute 'filepath', which abortsregister()and the socket never binds, leaving no log file at all. Fix:register()only schedules a one-shotbpy.app.timerscallback; the real start runs after boot, when the full API is live. Diagnose this class of failure withblender --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.)- Unregistering a timer from inside its own callback is an error. The
__STOP__path calledstop()from within_poll. Fix:stop(in_timer=True)just closes the socket and lets the callback'sreturn Noneretire the timer.