dsh-plugin-blender-3d 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 walkzzz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # dsh-plugin-blender-3d
2
+
3
+ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that lets the agent **drive 3D modeling** through a JSON bridge — create primitives, run boolean CSG, transform, assign materials/modifiers, query the scene, and export `STL`/`OBJ`/`PLY`/`GLTF`/`GLB`.
4
+
5
+ It bundles **two interchangeable runtimes** speaking the same protocol:
6
+
7
+ | Runtime | Needs Blender? | Engine | Use when |
8
+ | --- | --- | --- | --- |
9
+ | `headless_bridge.py` (default) | ❌ no | `trimesh` + `manifold3d` | headless servers, CI, the harness |
10
+ | `blender_ai_bridge/` add-on | ✅ Blender 3.0+ | `bpy` | you want full Blender power |
11
+
12
+ Point the tool at a running Blender (set `AIB_PORT` to the add-on's port) and the same commands upgrade to real Blender — no code change.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ dsh plugin --profile web add dsh-plugin-blender-3d
18
+ ```
19
+
20
+ Restart `dsh web` (or let HMR pick it up). The plugin registers two model-facing tools:
21
+
22
+ - **`blender_model`** — `{ command, args }` → JSON result. Commands: `create_shape`, `boolean`, `transform`, `duplicate`, `delete`, `rename`, `material`, `modifier`, `mesh_edit`, `scene`, `selection`, `scene_clear`, `camera`, `light`, `render`, `export`, `import`, `script`, `command`, `document`.
23
+ - **`blender_scene`** — lightweight scene summary (names/types/counts, no mesh data).
24
+
25
+ It also contributes the **`blender-3d-modeling`** skill (auto-installed into `$DSH_HOME/skills` on first load if absent).
26
+
27
+ ## OOM-safe by design
28
+
29
+ Heavy geometry **never** enters the Node/JSON tool channel. Exports return a `file_path`; the agent reads the model from disk with its own file tools. This is the fix for the Node `Ineffective mark-compacts near heap limit` crash that occurs when full meshes are stuffed into JSON.
30
+
31
+ ## Configuration
32
+
33
+ | Env / config | Default | Meaning |
34
+ | --- | --- | --- |
35
+ | `AIB_PORT` | `13082` | bridge port (set to the add-on port to drive real Blender) |
36
+ | `AIB_HOST` | `127.0.0.1` | bridge host |
37
+ | `AIB_PYTHON` | `python3` | interpreter for the headless bridge |
38
+
39
+ ## Headless runtime deps
40
+
41
+ ```sh
42
+ pip install numpy trimesh manifold3d
43
+ ```
44
+
45
+ ## Publish to the marketplace
46
+
47
+ See [`PUBLISH.md`](./PUBLISH.md). TL;DR: `npm publish` → open a PR to
48
+ [`awesome-dsh-plugin/awesome-dsh-plugin`](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
49
+ adding [`registry-entry.json`](./registry-entry.json) to the list.
50
+
51
+ ## License
52
+
53
+ MIT
54
+
55
+ ## Example
56
+
57
+ ```sh
58
+ node examples/carve_block.mjs # box minus sphere -> carved.stl
59
+ ```
@@ -0,0 +1,4 @@
1
+ # dsh bundle patch: inserts this plugin into a profile's layer stack.
2
+ - insert:
3
+ - id: blender-3d
4
+ name: 'dsh-plugin-blender-3d'
@@ -0,0 +1,109 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Tiny HTTP client + on-demand headless-bridge lifecycle for the
4
+ * dsh-plugin-blender-3d tool. No external deps — Node stdlib only.
5
+ *
6
+ * The bridge is the pure-Python `headless_bridge.py` (trimesh + manifold3d).
7
+ * We keep Node thin: only lightweight command JSON crosses the wire; heavy
8
+ * geometry stays in Python and is written to disk (OOM-safe, see SKILL.md).
9
+ */
10
+
11
+ import { spawn } from "node:child_process";
12
+ import http from "node:http";
13
+ import { fileURLToPath } from "node:url";
14
+ import path from "node:path";
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const BRIDGE_PATH = path.join(__dirname, "runtime", "headless_bridge.py");
18
+
19
+ // Module-level handle so we don't double-spawn within one harness process.
20
+ let _child = null;
21
+
22
+ function healthCheck(host, port, timeoutMs = 2000) {
23
+ return new Promise((resolve) => {
24
+ const req = http.request(
25
+ { method: "GET", hostname: host, port, path: "/health" },
26
+ (res) => {
27
+ const chunks = [];
28
+ res.on("data", (c) => chunks.push(c));
29
+ res.on("end", () => {
30
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString("utf8"))); }
31
+ catch { resolve(null); }
32
+ });
33
+ },
34
+ );
35
+ req.on("error", () => resolve(null));
36
+ req.setTimeout(timeoutMs, () => req.destroy());
37
+ req.end();
38
+ });
39
+ }
40
+
41
+ export async function ensureBridge(opts = {}) {
42
+ const host = opts.host || "127.0.0.1";
43
+ const port = Number(opts.port || process.env.AIB_PORT || 13082);
44
+ const python = opts.python || process.env.AIB_PYTHON || "python3";
45
+
46
+ // Already up?
47
+ const up = await healthCheck(host, port);
48
+ if (up && up.ok) return { host, port, spawned: false, health: up };
49
+
50
+ // Spawn the headless bridge detached.
51
+ if (!_child || _child.exitCode !== null) {
52
+ _child = spawn(python, [BRIDGE_PATH, "--host", host, "--port", String(port)], {
53
+ detached: true,
54
+ stdio: "ignore",
55
+ env: { ...process.env },
56
+ });
57
+ _child.on("error", () => { _child = null; });
58
+ try { _child.unref(); } catch { /* ignore */ }
59
+ }
60
+
61
+ // Poll for readiness.
62
+ const deadline = Date.now() + (opts.startupMs || 20000);
63
+ while (Date.now() < deadline) {
64
+ await new Promise((r) => setTimeout(r, 300));
65
+ const h = await healthCheck(host, port);
66
+ if (h && h.ok) return { host, port, spawned: true, health: h };
67
+ }
68
+ throw new Error(`blender bridge did not come up on http://${host}:${port}`);
69
+ }
70
+
71
+ export function sendCommand({ host, port, command, args = {}, id = 1, timeoutMs = 60000 }) {
72
+ return new Promise((resolve, reject) => {
73
+ const body = Buffer.from(JSON.stringify({ id, command, args }));
74
+ const req = http.request(
75
+ {
76
+ method: "POST", hostname: host, port, path: "/",
77
+ headers: { "Content-Type": "application/json", "Content-Length": body.length },
78
+ },
79
+ (res) => {
80
+ const chunks = [];
81
+ res.on("data", (c) => chunks.push(c));
82
+ res.on("end", () => {
83
+ const text = Buffer.concat(chunks).toString("utf8");
84
+ if (res.statusCode >= 400) return reject(new Error(`HTTP ${res.statusCode}: ${text.slice(0, 300)}`));
85
+ try { resolve(JSON.parse(text)); } catch { resolve({ raw: text }); }
86
+ });
87
+ },
88
+ );
89
+ req.on("error", reject);
90
+ req.setTimeout(timeoutMs, () => req.destroy(new Error("bridge timeout")));
91
+ req.write(body);
92
+ req.end();
93
+ });
94
+ }
95
+
96
+ export async function call(command, args, opts = {}) {
97
+ const { host, port } = await ensureBridge(opts);
98
+ const resp = await sendCommand({ host, port, command, args, timeoutMs: opts.timeoutMs });
99
+ if (resp && resp.error) throw new Error(resp.error);
100
+ if (resp && resp.result && resp.result.ok === false) {
101
+ throw new Error(resp.result.error || "command failed");
102
+ }
103
+ return resp && resp.result;
104
+ }
105
+
106
+ export function stopBridge() {
107
+ if (_child && _child.exitCode === null) { try { _child.kill(); } catch { /* ignore */ } }
108
+ _child = null;
109
+ }
package/lib/index.js ADDED
@@ -0,0 +1,117 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * dsh-plugin-blender-3d — DeepSeek Harness Cordis plugin.
4
+ *
5
+ * Registers a model-facing `blender_model` tool that drives 3D modeling
6
+ * through a JSON bridge (create primitives, boolean CSG, transform, material,
7
+ * modifier, scene, selection, export STL/OBJ/PLY/GLTF, run scripts). The
8
+ * bridge is the bundled pure-Python `headless_bridge.py` (trimesh + manifold3d,
9
+ * no Blender required) and is started on demand and kept alive for the session.
10
+ *
11
+ * The same protocol is spoken by the bundled Blender add-on
12
+ * (`runtime/blender_ai_bridge/`), so pointing the tool at a running Blender
13
+ * (AIB_PORT) upgrades to full Blender power with no code change.
14
+ *
15
+ * OOM-safe: heavy geometry never enters the Node/JSON channel — exports return
16
+ * a `file_path` the agent reads from disk with its own file tools.
17
+ */
18
+
19
+ import { call, ensureBridge, stopBridge } from "./bridge-client.js";
20
+ import { fileURLToPath } from "node:url";
21
+ import path from "node:path";
22
+ import fs from "node:fs";
23
+
24
+ export const name = "dsh-plugin-blender-3d";
25
+ export const inject = ["tools", "effect"];
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+ const BUNDLED_SKILL = path.join(__dirname, "..", "skill", "blender-3d-modeling", "SKILL.md");
29
+
30
+ /** Hand-rolled JSON Schema (zero deps), mirroring the skill-search preset. */
31
+ function schema(spec) {
32
+ const properties = {};
33
+ const required = [];
34
+ for (const [k, m] of Object.entries(spec || {})) {
35
+ properties[k] = { type: m.type, ...(m.description ? { description: m.description } : {}) };
36
+ if (m.required) required.push(k);
37
+ }
38
+ return { type: "object", properties, required, additionalProperties: false };
39
+ }
40
+
41
+ /** Idempotently materialize the bundled skill into the user skill root. */
42
+ function installSkill() {
43
+ try {
44
+ const dshHome = process.env.DSH_HOME;
45
+ if (!dshHome) return;
46
+ const destDir = path.join(dshHome, "skills", "blender-3d-modeling");
47
+ const dest = path.join(destDir, "SKILL.md");
48
+ if (fs.existsSync(dest)) return; // don't clobber an existing/newer skill
49
+ fs.mkdirSync(destDir, { recursive: true });
50
+ fs.copyFileSync(BUNDLED_SKILL, dest);
51
+ } catch { /* best-effort; never break boot */ }
52
+ }
53
+
54
+ export function apply(ctx, config = {}) {
55
+ installSkill();
56
+
57
+ const opts = {
58
+ host: config.host || process.env.AIB_HOST || "127.0.0.1",
59
+ port: config.port || process.env.AIB_PORT || 13082,
60
+ python: config.python || process.env.AIB_PYTHON || "python3",
61
+ startupMs: config.startupMs || 20000,
62
+ timeoutMs: config.timeoutMs || 60000,
63
+ };
64
+
65
+ ctx.tools.register({
66
+ name: "blender_model",
67
+ description:
68
+ "Drive 3D modeling through the Blender AI bridge. Pass a `command` and " +
69
+ "`args`; the bridge runs it and returns a JSON result. Commands: " +
70
+ "create_shape, boolean, transform, duplicate, delete, rename, material, " +
71
+ "modifier, mesh_edit, scene, selection, scene_clear, camera, light, render, " +
72
+ "export, import, script, command, document. Exports return a file_path " +
73
+ "(read the model from disk with your file tools — geometry is never inlined). " +
74
+ "First call auto-starts a headless Python bridge (trimesh+manifold3d); set " +
75
+ "AIB_PORT to drive a real Blender running the bundled add-on instead.",
76
+ parameters: schema({
77
+ command: { type: "string", required: true, description: "bridge command, e.g. create_shape / boolean / export / scene" },
78
+ args: { type: "object", description: "command arguments object" },
79
+ }),
80
+ output: {
81
+ schema: { type: "object", additionalProperties: false, properties: { text: { type: "string" } }, required: ["text"] },
82
+ render: (_a, v) => [{ type: "text", text: v.text }],
83
+ },
84
+ async execute(args, _exec) {
85
+ const command = args?.command;
86
+ const commandArgs = args?.args || {};
87
+ if (!command) throw new Error("blender_model requires a `command` string");
88
+ const result = await call(command, commandArgs, opts);
89
+ return { text: JSON.stringify(result, null, 2) };
90
+ },
91
+ });
92
+
93
+ // Optional: a thin `blender_scene` convenience tool for quick inspection.
94
+ ctx.tools.register({
95
+ name: "blender_scene",
96
+ description:
97
+ "Return a lightweight summary of the current 3D scene (object names, types, " +
98
+ "locations, vertex/face counts). No mesh data — OOM-safe. Equivalent to " +
99
+ "blender_model {command:'scene'}.",
100
+ parameters: schema({}),
101
+ output: {
102
+ schema: { type: "object", additionalProperties: false, properties: { text: { type: "string" } }, required: ["text"] },
103
+ render: (_a, v) => [{ type: "text", text: v.text }],
104
+ },
105
+ async execute(_a, _exec) {
106
+ const result = await call("scene", {}, opts);
107
+ return { text: JSON.stringify(result, null, 2) };
108
+ },
109
+ });
110
+
111
+ // Stop the spawned bridge when the harness tears down this composition.
112
+ try {
113
+ ctx.effect(function* () { yield async () => stopBridge(); }, "blender-3d-bridge");
114
+ } catch { /* effect service unavailable — ignore */ }
115
+ }
116
+
117
+ export { ensureBridge };
@@ -0,0 +1,57 @@
1
+ # Blender AI Bridge — DeepSeek Harness 插件
2
+
3
+ 一个 Blender add-on,把 Blender 的 3D 建模能力通过 **HTTP JSON 命令 API** 暴露出来,
4
+ 让外部 agent(DeepSeek harness)可以驱动建模:创建图元、布尔运算、变换、材质、修改器、
5
+ 场景查询、选择、导出、运行脚本。
6
+
7
+ > 设计与工作区里 `chili3d` 的浏览器桥接同源:插件内开服务、harness 发命令、结果回 JSON。
8
+ > 关键防 OOM:**导出只回文件路径,不把网格二进制塞 JSON**;harness 再从磁盘读模型。
9
+
10
+ ## 安装到 Blender
11
+
12
+ 1. `Edit > Preferences > Add-ons > Install…`
13
+ 2. 选择本目录打包后的 `blender_ai_bridge.zip`(把整个 `blender_ai_bridge/` 目录压缩成 zip)。
14
+ 3. 勾选启用 **AI Bridge (DeepSeek Harness)**。
15
+ 4. 在 3D 视图侧栏(按 `N`)打开 **AI Bridge** 面板,点 **Start**。
16
+ 或设端口:`Preferences > Add-ons > AI Bridge` 里改 Host/Port(默认 `127.0.0.1:13080`)。
17
+
18
+ ## 无头运行(无界面,CI/服务器)
19
+
20
+ ```bash
21
+ AIB_PORT=13080 blender --background --python /workspace/blender_ai_bridge/headless_start.py
22
+ ```
23
+
24
+ 服务起来后 `GET http://127.0.0.1:13080/health` 返回 `{"ok":true,"blender":[4,2,0],...}`。
25
+
26
+ ## 命令协议
27
+
28
+ `POST /` body:`{"id":1,"command":"create_shape","args":{"type":"box","dx":2,"dy":3,"dz":4}}`
29
+ 响应:`{"id":1,"result":{"ok":true,"name":"Cube","type":"box",...}}` 或 `{"id":1,"error":"..."}`
30
+
31
+ | command | args 摘要 |
32
+ |---|---|
33
+ | `create_shape` | `type` box/sphere/ico_sphere/cylinder/cone/torus/plane/grid/circle/monkey + 尺寸 + `location`/`rotation`/`name` |
34
+ | `boolean` | `target`,`tools`/`tool`,`operation` union/difference/intersect,`apply`,`remove_tools`,`solver` |
35
+ | `transform` | `name`,`location`,`rotation`,`scale`,`rotation_degrees` |
36
+ | `duplicate` / `delete` / `rename` | 对象复制/删除/改名 |
37
+ | `material` | `action` create/assign/list + `color`,`metallic`,`roughness` |
38
+ | `modifier` | `action` add/remove/list + `type` subsurf/bevel/array/mirror/solidify + `params`,`apply` |
39
+ | `mesh_edit` | `name`,`action` subdivide/extrude/inset/recalc_normals |
40
+ | `scene` / `selection` / `scene_clear` | 轻量场景/选择查询(不回网格数据) |
41
+ | `camera` / `light` / `render` | 相机/灯光/渲染到文件 |
42
+ | `export` | `format` stl/obj/ply/fbx/gltf/glb/blend + `path`/`names` → 返回 `file_path` |
43
+ | `import` | `format` + `path` |
44
+ | `script` / `command` / `document` | 执行 Python / Blender operator / 新建保存打开 |
45
+
46
+ ## 版本兼容
47
+
48
+ - 导出自动适配 3.x(`export_mesh.stl`/`export_scene.obj`)与 4.1+(`wm.stl_export`/`wm.obj_export`)。
49
+ - 布尔修改器 4.0+ 设 `solver`(默认 `EXACT`)。
50
+ - `bl_info` 声明 `blender: (3,0,0)`,3.0+ 均可加载。
51
+
52
+ ## 文件
53
+
54
+ - `__init__.py` — add-on 入口、注册、N 面板、偏好设置、后台模式自启
55
+ - `server.py` — HTTP 服务 + 主线程 marshal(bpy.ops 线程不安全,经 `bpy.app.timers` 转回主线程)
56
+ - `commands.py` — 命令处理器(bpy/bmesh)
57
+ - `headless_start.py` — `blender --background` 启动器
@@ -0,0 +1,159 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ bl_info = {
3
+ "name": "AI Bridge (DeepSeek Harness)",
4
+ "author": "DeepSeek Harness",
5
+ "version": (1, 0, 0),
6
+ "blender": (3, 0, 0),
7
+ "location": "View3D > Sidebar (N) > AI Bridge",
8
+ "description": "Expose a JSON command API over HTTP so an external agent "
9
+ "(DeepSeek harness) can drive 3D modeling: primitives, "
10
+ "booleans, transforms, materials, modifiers, scene, "
11
+ "selection, export, scripts.",
12
+ "category": "Interface",
13
+ }
14
+
15
+ import bpy
16
+ from bpy.props import StringProperty, IntProperty, BoolProperty
17
+ from bpy.types import Operator, Panel, AddonPreferences
18
+
19
+ from . import server as _server
20
+
21
+ # Single shared server instance for the add-on.
22
+ _SERVER = _server.BridgeHTTPServer()
23
+
24
+
25
+ # --------------------------------------------------------------------------- #
26
+ # operators
27
+ # --------------------------------------------------------------------------- #
28
+ class AIB_OT_start(Operator):
29
+ bl_idname = "aib.start"
30
+ bl_label = "Start AI Bridge"
31
+ bl_description = "Start the HTTP bridge server on the configured port"
32
+
33
+ def execute(self, context):
34
+ prefs = _prefs(context)
35
+ _SERVER.host = prefs.host
36
+ _SERVER.port = prefs.port
37
+ try:
38
+ _SERVER.start()
39
+ except OSError as exc:
40
+ self.report({'ERROR'}, "Failed to start: %s" % exc)
41
+ return {'CANCELLED'}
42
+ _server.ensure_timer()
43
+ prefs.running = True
44
+ self.report({'INFO'}, "AI Bridge on %s" % _SERVER.url())
45
+ return {'FINISHED'}
46
+
47
+
48
+ class AIB_OT_stop(Operator):
49
+ bl_idname = "aib.stop"
50
+ bl_label = "Stop AI Bridge"
51
+ bl_description = "Stop the HTTP bridge server"
52
+
53
+ def execute(self, context):
54
+ _SERVER.stop()
55
+ _prefs(context).running = False
56
+ self.report({'INFO'}, "AI Bridge stopped")
57
+ return {'FINISHED'}
58
+
59
+
60
+ class AIB_OT_status(Operator):
61
+ bl_idname = "aib.status"
62
+ bl_label = "Bridge Status"
63
+ bl_description = "Print bridge status to the info area"
64
+
65
+ def execute(self, context):
66
+ state = "running at %s" % _SERVER.url() if _SERVER.running else "stopped"
67
+ self.report({'INFO'}, "AI Bridge %s" % state)
68
+ return {'FINISHED'}
69
+
70
+
71
+ # --------------------------------------------------------------------------- #
72
+ # panel
73
+ # --------------------------------------------------------------------------- #
74
+ class AIB_PT_panel(Panel):
75
+ bl_label = "AI Bridge"
76
+ bl_idname = "AIB_PT_panel"
77
+ bl_space_type = 'VIEW_3D'
78
+ bl_region_type = 'UI'
79
+ bl_category = "AI Bridge"
80
+
81
+ def draw(self, context):
82
+ prefs = _prefs(context)
83
+ layout = self.layout
84
+ col = layout.column(align=True)
85
+ if _SERVER.running:
86
+ col.label(text="Running: %s" % _SERVER.url(), icon='CHECKMARK')
87
+ col.operator("aib.stop", icon='PAUSE')
88
+ else:
89
+ col.label(text="Stopped", icon='X')
90
+ col.operator("aib.start", icon='PLAY')
91
+ col.separator()
92
+ col.prop(prefs, "host")
93
+ col.prop(prefs, "port")
94
+ col.separator()
95
+ box = col.box()
96
+ box.label(text="Commands:", icon='SCRIPT')
97
+ for line in ("create_shape, boolean, transform",
98
+ "duplicate, delete, material, modifier",
99
+ "scene, selection, export, import",
100
+ "camera, light, render, script"):
101
+ box.label(text=line)
102
+
103
+
104
+ # --------------------------------------------------------------------------- #
105
+ # preferences
106
+ # --------------------------------------------------------------------------- #
107
+ def _prefs(context):
108
+ return context.preferences.addons[__name__].preferences
109
+
110
+
111
+ class AIBPreferences(AddonPreferences):
112
+ bl_idname = __name__
113
+
114
+ host: StringProperty(name="Host", default="127.0.0.1")
115
+ port: IntProperty(name="Port", default=13080, min=1, max=65535)
116
+ running: BoolProperty(name="Running", default=False)
117
+
118
+ def draw(self, context):
119
+ layout = self.layout
120
+ layout.prop(self, "host")
121
+ layout.prop(self, "port")
122
+ if _SERVER.running:
123
+ layout.operator("aib.stop")
124
+ else:
125
+ layout.operator("aib.start")
126
+
127
+
128
+ # --------------------------------------------------------------------------- #
129
+ # registration
130
+ # --------------------------------------------------------------------------- #
131
+ _classes = (AIB_OT_start, AIB_OT_stop, AIB_OT_status, AIB_PT_panel, AIBPreferences)
132
+
133
+
134
+ def register():
135
+ for cls in _classes:
136
+ bpy.utils.register_class(cls)
137
+ # Auto-start in headless mode so `blender --background --python` just works.
138
+ try:
139
+ if bpy.app.background:
140
+ bpy.app.handlers.event_timer # noqa: B018 - probe attribute
141
+ _SERVER.host = "127.0.0.1"
142
+ _SERVER.port = int(__import__("os").environ.get("AIB_PORT", "13080"))
143
+ _SERVER.start()
144
+ _server.ensure_timer()
145
+ except Exception:
146
+ pass
147
+
148
+
149
+ def unregister():
150
+ _SERVER.stop()
151
+ for cls in reversed(_classes):
152
+ try:
153
+ bpy.utils.unregister_class(cls)
154
+ except Exception:
155
+ pass
156
+
157
+
158
+ if __name__ == "__main__":
159
+ register()