auvrynt 1.0.2 → 1.0.5

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.
Files changed (550) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +366 -233
  3. package/addons/auvrynt_bridge/auvrynt_bridge.gd +500 -0
  4. package/addons/auvrynt_bridge/plugin.cfg +7 -0
  5. package/bin/auvrynt-launcher.cjs +77 -0
  6. package/dist/artifact-registry.js +1 -0
  7. package/dist/aseprite-tools.js +1 -0
  8. package/dist/auth/connection-registry.js +145 -0
  9. package/dist/auth/connection-status.js +176 -0
  10. package/dist/auth/oauth-provider.js +617 -0
  11. package/dist/auth/room-registry.js +131 -0
  12. package/dist/auth/session-registry.js +531 -0
  13. package/dist/background-lifecycle.js +1 -0
  14. package/dist/blender-client.js +1 -109
  15. package/dist/blender-tools.js +1 -930
  16. package/dist/browser-stability.js +1 -0
  17. package/dist/cli/commands/help-command.js +10 -0
  18. package/dist/cli/commands/init-command.js +190 -0
  19. package/dist/cli/commands/integration-commands.js +88 -0
  20. package/dist/cli/commands/serve-command.js +83 -0
  21. package/dist/cli/commands/setup-command.js +81 -0
  22. package/dist/cli/commands/status-commands.js +166 -0
  23. package/dist/cli/commands/tunnel-command.js +127 -0
  24. package/dist/cli/commands/user-config-commands.js +105 -0
  25. package/dist/cli/dashboard-actions.js +516 -0
  26. package/dist/cli/dashboard-native-actions.js +282 -0
  27. package/dist/cli/foreground-server.js +270 -0
  28. package/dist/cli/instance-control.js +21 -0
  29. package/dist/cli/instance-lock.js +123 -0
  30. package/dist/cli/integration-bootstrap.js +206 -0
  31. package/dist/cli/lifecycle-manager.js +493 -0
  32. package/dist/cli/runtime-support.js +74 -0
  33. package/dist/cli/serena-detected-default.js +27 -0
  34. package/dist/cli/tunnel-health-monitor.js +182 -0
  35. package/dist/cli.js +82 -739
  36. package/dist/config.js +1 -243
  37. package/dist/connection-registry.js +1 -73
  38. package/dist/connection-status.js +1 -139
  39. package/dist/control-retry.js +1 -0
  40. package/dist/crash-recovery.js +1 -0
  41. package/dist/db/client.js +16 -3
  42. package/dist/db/schema.js +30 -0
  43. package/dist/dotnet-tools.js +1 -223
  44. package/dist/git-worktrees.js +1 -134
  45. package/dist/git.js +1 -41
  46. package/dist/godot-animation-tilemap.js +1 -82
  47. package/dist/godot-csharp-build.js +1 -93
  48. package/dist/godot-csharp-project.js +1 -157
  49. package/dist/godot-csharp-runner.js +1 -146
  50. package/dist/godot-csharp-semantic.js +1 -161
  51. package/dist/godot-csharp-validate.js +1 -71
  52. package/dist/godot-dotnet-env.js +1 -169
  53. package/dist/godot-editor-bridge.js +1 -140
  54. package/dist/godot-gdscript.js +1 -824
  55. package/dist/godot-project-settings.js +1 -223
  56. package/dist/godot-runtime-testing.js +1 -249
  57. package/dist/godot-tools.js +1 -183
  58. package/dist/http-rate-limit.js +1 -0
  59. package/dist/http-server-hardening.js +1 -0
  60. package/dist/image-tools.js +1 -157
  61. package/dist/infrastructure/command-reference.js +32 -0
  62. package/dist/infrastructure/config.js +424 -0
  63. package/dist/infrastructure/context7-auth.js +55 -0
  64. package/dist/infrastructure/http-rate-limit.js +42 -0
  65. package/dist/infrastructure/http-server-hardening.js +23 -0
  66. package/dist/infrastructure/integration-queue.js +46 -0
  67. package/dist/infrastructure/logger.js +129 -0
  68. package/dist/infrastructure/mcp-event-store.js +103 -0
  69. package/dist/infrastructure/ngrok-auth-pool.js +208 -0
  70. package/dist/infrastructure/request-context.js +14 -0
  71. package/dist/infrastructure/shutdown.js +37 -0
  72. package/dist/infrastructure/sse-heartbeat.js +47 -0
  73. package/dist/infrastructure/tool-capabilities.js +67 -0
  74. package/dist/infrastructure/tool-result-budget.js +175 -0
  75. package/dist/infrastructure/user-config.js +167 -0
  76. package/dist/integration-discovery.js +1 -98
  77. package/dist/integration-queue.js +1 -0
  78. package/dist/integrations/agent-reach/agent-reach-tools.js +196 -0
  79. package/dist/integrations/agent-reach/native-channels.js +468 -0
  80. package/dist/integrations/aseprite/aseprite-analysis-tools.js +368 -0
  81. package/dist/integrations/aseprite/aseprite-editing-tools.js +647 -0
  82. package/dist/integrations/aseprite/aseprite-live-tools.js +297 -0
  83. package/dist/integrations/aseprite/aseprite-safety-tools.js +594 -0
  84. package/dist/integrations/aseprite/aseprite-specialized-tools.js +289 -0
  85. package/dist/integrations/aseprite/aseprite-tools.js +837 -0
  86. package/dist/integrations/blender/blender-catalog-mesh.js +593 -0
  87. package/dist/integrations/blender/blender-catalog-object.js +906 -0
  88. package/dist/integrations/blender/blender-catalog-scene.js +266 -0
  89. package/dist/integrations/blender/blender-catalog-select.js +581 -0
  90. package/dist/integrations/blender/blender-catalog-stubs.js +7512 -0
  91. package/dist/integrations/blender/blender-catalog.js +165 -0
  92. package/dist/integrations/blender/blender-client.js +105 -0
  93. package/dist/integrations/blender/blender-tools.js +2243 -0
  94. package/dist/integrations/context7/context7-tools.js +106 -0
  95. package/dist/integrations/dotnet/dotnet-tools.js +195 -0
  96. package/dist/integrations/godot/godot-animation-tilemap.js +74 -0
  97. package/dist/integrations/godot/godot-csharp-build.js +91 -0
  98. package/dist/integrations/godot/godot-csharp-project.js +155 -0
  99. package/dist/integrations/godot/godot-csharp-runner.js +118 -0
  100. package/dist/integrations/godot/godot-csharp-semantic.js +146 -0
  101. package/dist/integrations/godot/godot-csharp-validate.js +71 -0
  102. package/dist/integrations/godot/godot-dotnet-env.js +134 -0
  103. package/dist/integrations/godot/godot-editor-bridge.js +328 -0
  104. package/dist/integrations/godot/godot-gdscript.js +536 -0
  105. package/dist/integrations/godot/godot-project-settings.js +178 -0
  106. package/dist/integrations/godot/godot-runtime-testing.js +263 -0
  107. package/dist/integrations/godot/godot-tools.js +193 -0
  108. package/dist/integrations/images/image-tools.js +414 -0
  109. package/dist/integrations/images/png-codec.js +251 -0
  110. package/dist/integrations/images/view-image.js +99 -0
  111. package/dist/integrations/images/window-capture.js +148 -0
  112. package/dist/integrations/integration-discovery.js +185 -0
  113. package/dist/integrations/obsidian/obsidian-tools.js +205 -0
  114. package/dist/integrations/playwright/browser-session.js +516 -0
  115. package/dist/integrations/playwright/browser-stability.js +36 -0
  116. package/dist/integrations/playwright/playwright-runtime.js +105 -0
  117. package/dist/integrations/playwright/web-tools.js +504 -0
  118. package/dist/integrations/roblox/roblox-helpers.js +381 -0
  119. package/dist/integrations/roblox/roblox-studio-client.js +112 -0
  120. package/dist/integrations/roblox/roblox-tools.js +106 -0
  121. package/dist/integrations/serena/serena-manager.js +464 -0
  122. package/dist/integrations/serena/serena-tools.js +411 -0
  123. package/dist/logger.js +1 -76
  124. package/dist/mcp-event-store.js +1 -0
  125. package/dist/oauth-provider.js +1 -377
  126. package/dist/pi-tools.js +1 -81
  127. package/dist/playwright-runtime.js +1 -0
  128. package/dist/process/background-lifecycle.js +189 -0
  129. package/dist/process/control-retry.js +42 -0
  130. package/dist/process/crash-recovery.js +23 -0
  131. package/dist/process/processes.js +456 -0
  132. package/dist/processes.js +1 -218
  133. package/dist/request-context.js +1 -0
  134. package/dist/review-checkpoints.js +1 -142
  135. package/dist/room-registry.js +1 -0
  136. package/dist/roots.js +1 -53
  137. package/dist/search-discovery.js +1 -271
  138. package/dist/serena-manager.js +1 -443
  139. package/dist/serena-tools.js +1 -411
  140. package/dist/server/dashboard-page-styles.js +331 -0
  141. package/dist/server/dashboard-page.js +1509 -0
  142. package/dist/server/dashboard-routes.js +46 -0
  143. package/dist/server/dashboard.js +240 -0
  144. package/dist/server/http-helpers.js +30 -0
  145. package/dist/server/integration-profile-update.js +37 -0
  146. package/dist/server/mcp-policy.js +169 -0
  147. package/dist/server/mcp-server-factory.js +127 -0
  148. package/dist/server/mcp-tool-registrar.js +182 -0
  149. package/dist/server/openai-session-hint.js +14 -0
  150. package/dist/server/tool-groups.js +29 -0
  151. package/dist/server/tool-logging.js +33 -0
  152. package/dist/server/tool-registration-shared.js +59 -0
  153. package/dist/server/tools/aseprite-advanced-tools-registration.js +547 -0
  154. package/dist/server/tools/aseprite-tools-registration.js +213 -0
  155. package/dist/server/tools/blender-tools-registration.js +690 -0
  156. package/dist/server/tools/core-file-tools-registration.js +459 -0
  157. package/dist/server/tools/dotnet-godot-window-tools-registration.js +120 -0
  158. package/dist/server/tools/godot-csharp-tools-registration.js +252 -0
  159. package/dist/server/tools/godot-gdscript-tools-registration.js +189 -0
  160. package/dist/server/tools/process-project-tools-registration.js +155 -0
  161. package/dist/server/tools/roblox-tools-registration.js +514 -0
  162. package/dist/server/tools/web-image-tools-registration.js +326 -0
  163. package/dist/server/tools/workspace-lifecycle-tools-registration.js +130 -0
  164. package/dist/server/ui-assets.js +97 -0
  165. package/dist/server/workspace-analytics.js +92 -0
  166. package/dist/server.js +453 -2713
  167. package/dist/session-registry.js +1 -0
  168. package/dist/shutdown.js +1 -0
  169. package/dist/skills.js +1 -45
  170. package/dist/sse-heartbeat.js +1 -0
  171. package/dist/tool-capabilities.js +1 -0
  172. package/dist/tool-result-budget.js +1 -0
  173. package/dist/tools/pi-tools.js +81 -0
  174. package/dist/tunnels/cloudflare-tunnel.js +217 -0
  175. package/dist/tunnels/cloudflared-service.js +174 -0
  176. package/dist/tunnels/ngrok-tunnel.js +220 -0
  177. package/dist/tunnels/tunnel-manager.js +186 -0
  178. package/dist/tunnels/tunnel-types.js +1 -0
  179. package/dist/tunnels/tunnel-utils.js +62 -0
  180. package/dist/ui/.vite/manifest.json +9 -2487
  181. package/dist/ui/assets/heavy-payload-D42A9ceL.js +1 -0
  182. package/dist/ui/assets/lightweight-diff-Cv4g9Hdr.js +13 -0
  183. package/dist/ui/assets/review-payload-DKIg1cKF.js +1 -0
  184. package/dist/ui/assets/workspace-app-CeK3NhmB.css +1 -0
  185. package/dist/ui/assets/{workspace-app-C5QsE314.js → workspace-app-CkUXnpl4.js} +6 -6
  186. package/dist/ui/brand-theme.js +23 -0
  187. package/dist/ui/workspace-app.html +15 -15
  188. package/dist/user-config.js +1 -57
  189. package/dist/view-image.js +1 -104
  190. package/dist/web-tools.js +1 -200
  191. package/dist/window-capture.js +1 -74
  192. package/dist/workspace/artifact-registry.js +69 -0
  193. package/dist/workspace/git-worktrees.js +141 -0
  194. package/dist/workspace/git.js +42 -0
  195. package/dist/workspace/review-checkpoints.js +142 -0
  196. package/dist/workspace/roots.js +56 -0
  197. package/dist/workspace/search-discovery.js +264 -0
  198. package/dist/workspace/skills.js +45 -0
  199. package/dist/workspace/workspace-store.js +219 -0
  200. package/dist/workspace/workspaces.js +314 -0
  201. package/dist/workspace-store.js +1 -125
  202. package/dist/workspaces.js +1 -226
  203. package/docs/CREATOR_STORE_FREE_ASSETS.md +162 -0
  204. package/docs/DEVELOPMENT_TOOLS.md +136 -93
  205. package/docs/assets/auvrynt-icon.png +0 -0
  206. package/docs/assets/auvrynt-icon.svg +20 -0
  207. package/docs/chatgpt-coding-workflow.md +143 -143
  208. package/docs/configuration.md +156 -124
  209. package/docs/diagnostics.md +79 -0
  210. package/docs/gotchas.md +244 -202
  211. package/docs/multi-agent-and-long-projects.md +91 -0
  212. package/docs/security.md +138 -100
  213. package/docs/setup.md +171 -131
  214. package/docs/source-layout.md +60 -0
  215. package/extensions/auvrynt_bridge/auvrynt_bridge.lua +594 -0
  216. package/extensions/auvrynt_bridge/live_analysis.lua +181 -0
  217. package/extensions/auvrynt_bridge/live_animation.lua +134 -0
  218. package/extensions/auvrynt_bridge/live_commands.lua +24 -0
  219. package/extensions/auvrynt_bridge/live_common.lua +130 -0
  220. package/extensions/auvrynt_bridge/live_document.lua +78 -0
  221. package/extensions/auvrynt_bridge/live_drawing.lua +125 -0
  222. package/extensions/auvrynt_bridge/live_effects.lua +91 -0
  223. package/extensions/auvrynt_bridge/live_palette_template.lua +167 -0
  224. package/extensions/auvrynt_bridge/live_selection.lua +110 -0
  225. package/extensions/auvrynt_bridge/live_structure.lua +129 -0
  226. package/extensions/auvrynt_bridge/package.json +17 -0
  227. package/guides/ASEPRITE_AGENT_GUIDE.md +133 -0
  228. package/guides/BLENDER_AGENT_GUIDE.md +472 -0
  229. package/guides/GODOT_AGENT_GUIDE.md +298 -0
  230. package/guides/PROJECT_ROUTER.md +165 -0
  231. package/guides/ROBLOX_AGENT_GUIDE.md +75 -0
  232. package/guides/SOFTWARE_AGENT_GUIDE.md +104 -0
  233. package/guides/WEB_AGENT_GUIDE.md +287 -0
  234. package/package.json +78 -70
  235. package/scripts/dev-server.mjs +121 -120
  236. package/scripts/live-tunnel-smoke.mjs +538 -0
  237. package/scripts/repo-hygiene.mjs +74 -0
  238. package/scripts/restart-after-crash.mjs +49 -0
  239. package/scripts/run-tests.mjs +166 -0
  240. package/dist/ui/assets/abap-CLvhMVsD.js +0 -1
  241. package/dist/ui/assets/actionscript-3--17pq3dv.js +0 -1
  242. package/dist/ui/assets/ada-C5qYipkI.js +0 -1
  243. package/dist/ui/assets/andromeeda-vGVdxbeo.js +0 -1
  244. package/dist/ui/assets/angular-html-BJqQFYI6.js +0 -1
  245. package/dist/ui/assets/angular-ts-CMJECO0Y.js +0 -1
  246. package/dist/ui/assets/apache-U0d_L8uA.js +0 -1
  247. package/dist/ui/assets/apex-CGTLDQj6.js +0 -1
  248. package/dist/ui/assets/apl-DWeXpBBr.js +0 -1
  249. package/dist/ui/assets/applescript-CCn79oCD.js +0 -1
  250. package/dist/ui/assets/ara-4CJ0cIlV.js +0 -1
  251. package/dist/ui/assets/asciidoc-DE70LPWp.js +0 -1
  252. package/dist/ui/assets/asm-Cmm7eHzH.js +0 -1
  253. package/dist/ui/assets/astro-CLbttLOq.js +0 -1
  254. package/dist/ui/assets/aurora-x-CDeNXAV0.js +0 -1
  255. package/dist/ui/assets/awk-BWXHIvNe.js +0 -1
  256. package/dist/ui/assets/ayu-dark-DluEY0Gj.js +0 -1
  257. package/dist/ui/assets/ayu-light-C3h-C4tm.js +0 -1
  258. package/dist/ui/assets/ayu-mirage-Bqwy1Gya.js +0 -1
  259. package/dist/ui/assets/ballerina-B7ZEbQpA.js +0 -1
  260. package/dist/ui/assets/bat-Bo4NYOV-.js +0 -1
  261. package/dist/ui/assets/beancount-D-usSTwE.js +0 -1
  262. package/dist/ui/assets/berry-DKpUyyne.js +0 -1
  263. package/dist/ui/assets/bibtex-Ci_nEsc7.js +0 -1
  264. package/dist/ui/assets/bicep-CUHmPFLl.js +0 -1
  265. package/dist/ui/assets/bird2-C2hNVINV.js +0 -1
  266. package/dist/ui/assets/blade-BOzY0P2V.js +0 -1
  267. package/dist/ui/assets/bsl-BkkzgIyY.js +0 -1
  268. package/dist/ui/assets/c-BG-8uJvh.js +0 -1
  269. package/dist/ui/assets/c3-CnJL0r0V.js +0 -1
  270. package/dist/ui/assets/cadence-CQ2zXKGN.js +0 -1
  271. package/dist/ui/assets/cairo-DLTphjLi.js +0 -1
  272. package/dist/ui/assets/catppuccin-frappe-3VR1Za6u.js +0 -1
  273. package/dist/ui/assets/catppuccin-latte-DwIHMF0Q.js +0 -1
  274. package/dist/ui/assets/catppuccin-macchiato-DYnBP6_5.js +0 -1
  275. package/dist/ui/assets/catppuccin-mocha-DYhrFGRu.js +0 -1
  276. package/dist/ui/assets/clarity-SemFz856.js +0 -1
  277. package/dist/ui/assets/clojure-DqKBuwfJ.js +0 -1
  278. package/dist/ui/assets/cmake-Bj61d0ZC.js +0 -1
  279. package/dist/ui/assets/cobol-I_17IuGe.js +0 -1
  280. package/dist/ui/assets/codeowners-C8r90Shi.js +0 -1
  281. package/dist/ui/assets/codeql-oeQT6MSM.js +0 -1
  282. package/dist/ui/assets/coffee-C0B8hmok.js +0 -1
  283. package/dist/ui/assets/common-lisp-Cv5bFMCO.js +0 -1
  284. package/dist/ui/assets/coq-BrsZFFmf.js +0 -1
  285. package/dist/ui/assets/cpp-DDJXt6Vs.js +0 -1
  286. package/dist/ui/assets/crystal-Bexqace6.js +0 -1
  287. package/dist/ui/assets/csharp-Ct8U2NOr.js +0 -1
  288. package/dist/ui/assets/css-DSQhw_9R.js +0 -1
  289. package/dist/ui/assets/csv-Dx-8-gkx.js +0 -1
  290. package/dist/ui/assets/cue-CE9AQfxI.js +0 -1
  291. package/dist/ui/assets/cypher-ClKdZ_lG.js +0 -1
  292. package/dist/ui/assets/d-qD-0Kul2.js +0 -1
  293. package/dist/ui/assets/dark-plus-Cs2F2srj.js +0 -1
  294. package/dist/ui/assets/dart-DkHntEIa.js +0 -1
  295. package/dist/ui/assets/dax-BkyTk9wS.js +0 -1
  296. package/dist/ui/assets/desktop-Dlh5hvp9.js +0 -1
  297. package/dist/ui/assets/diff-woXpYk--.js +0 -1
  298. package/dist/ui/assets/docker-IyjqRm3v.js +0 -1
  299. package/dist/ui/assets/dotenv-_5a1GRtc.js +0 -1
  300. package/dist/ui/assets/dracula-BHWKrbxM.js +0 -1
  301. package/dist/ui/assets/dracula-soft-5eyTD99u.js +0 -1
  302. package/dist/ui/assets/dream-maker-DW3nJb8Q.js +0 -1
  303. package/dist/ui/assets/edge-Z8YYkHOr.js +0 -1
  304. package/dist/ui/assets/elixir-CG9uKtAF.js +0 -1
  305. package/dist/ui/assets/elm-GfWG2IfT.js +0 -1
  306. package/dist/ui/assets/emacs-lisp-C9PiwqqW.js +0 -1
  307. package/dist/ui/assets/erb-xe89e73J.js +0 -1
  308. package/dist/ui/assets/erlang-Cphh6RMH.js +0 -1
  309. package/dist/ui/assets/everforest-dark-sB-x3p7T.js +0 -1
  310. package/dist/ui/assets/everforest-light-Df2xbC6M.js +0 -1
  311. package/dist/ui/assets/fennel-DQxkIbk2.js +0 -1
  312. package/dist/ui/assets/fish-BJitypiv.js +0 -1
  313. package/dist/ui/assets/fluent-C03EYrpw.js +0 -1
  314. package/dist/ui/assets/fortran-fixed-form-DEKoE2YW.js +0 -1
  315. package/dist/ui/assets/fortran-free-form-CYNrtFtB.js +0 -1
  316. package/dist/ui/assets/fsharp-D13ZGOAj.js +0 -1
  317. package/dist/ui/assets/gdresource-C0sCabJj.js +0 -1
  318. package/dist/ui/assets/gdscript-Cp2uCuqX.js +0 -1
  319. package/dist/ui/assets/gdshader-CBce3t8t.js +0 -1
  320. package/dist/ui/assets/genie-CV2tkWYe.js +0 -1
  321. package/dist/ui/assets/gherkin-DExj1W_8.js +0 -1
  322. package/dist/ui/assets/git-commit-BSykSTBG.js +0 -1
  323. package/dist/ui/assets/git-rebase-CoTL_uSE.js +0 -1
  324. package/dist/ui/assets/github-dark-C-LZuMrd.js +0 -1
  325. package/dist/ui/assets/github-dark-default-DXG-b-1a.js +0 -1
  326. package/dist/ui/assets/github-dark-dimmed-Bx1FflLF.js +0 -1
  327. package/dist/ui/assets/github-dark-high-contrast-B_tTalzw.js +0 -1
  328. package/dist/ui/assets/github-light-EUqPIrTm.js +0 -1
  329. package/dist/ui/assets/github-light-default-BXViO-2h.js +0 -1
  330. package/dist/ui/assets/github-light-high-contrast-B68TUdTA.js +0 -1
  331. package/dist/ui/assets/gleam-CSRkHgEL.js +0 -1
  332. package/dist/ui/assets/glimmer-js-DAn4UCSu.js +0 -1
  333. package/dist/ui/assets/glimmer-ts-Tjyi6Gh5.js +0 -1
  334. package/dist/ui/assets/glsl-CtTDZoqR.js +0 -1
  335. package/dist/ui/assets/gn-ilITqXS6.js +0 -1
  336. package/dist/ui/assets/gnuplot-7GGW24-e.js +0 -1
  337. package/dist/ui/assets/go-BJwz_mda.js +0 -1
  338. package/dist/ui/assets/graphql-D5gcC_Jk.js +0 -1
  339. package/dist/ui/assets/groovy-CacY0gHj.js +0 -1
  340. package/dist/ui/assets/gruvbox-dark-hard-C820rvS2.js +0 -1
  341. package/dist/ui/assets/gruvbox-dark-medium-BPjhmG05.js +0 -1
  342. package/dist/ui/assets/gruvbox-dark-soft-MrdJrrXF.js +0 -1
  343. package/dist/ui/assets/gruvbox-light-hard-BC_s9l72.js +0 -1
  344. package/dist/ui/assets/gruvbox-light-medium-BAWPOn9u.js +0 -1
  345. package/dist/ui/assets/gruvbox-light-soft-BSMLrYjP.js +0 -1
  346. package/dist/ui/assets/hack-BJXY27dt.js +0 -1
  347. package/dist/ui/assets/haml-BLfFoZf7.js +0 -1
  348. package/dist/ui/assets/handlebars-Dw6CWkvN.js +0 -1
  349. package/dist/ui/assets/haskell-D8IpX4py.js +0 -1
  350. package/dist/ui/assets/haxe-OTjmBuCE.js +0 -1
  351. package/dist/ui/assets/hcl-Dh228itO.js +0 -1
  352. package/dist/ui/assets/heavy-payload-Dcg0-F-S.js +0 -4
  353. package/dist/ui/assets/hjson-CxZEssPk.js +0 -1
  354. package/dist/ui/assets/hlsl-Cvrh5tZx.js +0 -1
  355. package/dist/ui/assets/horizon-CE9ld1lL.js +0 -1
  356. package/dist/ui/assets/horizon-bright-Br1oVSNq.js +0 -1
  357. package/dist/ui/assets/houston-CsvMBhTu.js +0 -1
  358. package/dist/ui/assets/html-CAcAWvoN.js +0 -1
  359. package/dist/ui/assets/html-derivative-8aSW2E_7.js +0 -1
  360. package/dist/ui/assets/http-B2YR_3V1.js +0 -1
  361. package/dist/ui/assets/hurl-BnDJ9Zhf.js +0 -1
  362. package/dist/ui/assets/hxml-B0Qn7Nwc.js +0 -1
  363. package/dist/ui/assets/hy-CZbG8q4J.js +0 -1
  364. package/dist/ui/assets/imba-DsUTQ-LC.js +0 -1
  365. package/dist/ui/assets/ini-B5eOa1yu.js +0 -1
  366. package/dist/ui/assets/java-Doxi4W8u.js +0 -1
  367. package/dist/ui/assets/javascript-BbRdEqti.js +0 -1
  368. package/dist/ui/assets/jinja-CS3Tpv_Q.js +0 -1
  369. package/dist/ui/assets/jison-BkPGYVHR.js +0 -1
  370. package/dist/ui/assets/json-BMt1m3m8.js +0 -1
  371. package/dist/ui/assets/json5-BR5RXkoi.js +0 -1
  372. package/dist/ui/assets/jsonc-CYpm1nAK.js +0 -1
  373. package/dist/ui/assets/jsonl-CmCQp5Yx.js +0 -1
  374. package/dist/ui/assets/jsonnet-CJTPZ8u_.js +0 -1
  375. package/dist/ui/assets/jssm-DXw9l8Rf.js +0 -1
  376. package/dist/ui/assets/jsx-CQKrJXXY.js +0 -1
  377. package/dist/ui/assets/julia-BuN7i2du.js +0 -1
  378. package/dist/ui/assets/just-BswEPFPu.js +0 -1
  379. package/dist/ui/assets/kanagawa-dragon-CXtmUGW6.js +0 -1
  380. package/dist/ui/assets/kanagawa-lotus-BN08jTvb.js +0 -1
  381. package/dist/ui/assets/kanagawa-wave-CTweb8Dz.js +0 -1
  382. package/dist/ui/assets/kdl-CsD5j6eV.js +0 -1
  383. package/dist/ui/assets/kotlin-DhhofPvG.js +0 -1
  384. package/dist/ui/assets/kusto-BUv0MjJC.js +0 -1
  385. package/dist/ui/assets/laserwave-C_8bwKvT.js +0 -1
  386. package/dist/ui/assets/latex-DU2VyFfA.js +0 -1
  387. package/dist/ui/assets/lean-CewbzKMR.js +0 -1
  388. package/dist/ui/assets/less-DVTAwKKz.js +0 -1
  389. package/dist/ui/assets/light-plus-DVQuIRkW.js +0 -1
  390. package/dist/ui/assets/liquid-Cmz1L3dq.js +0 -1
  391. package/dist/ui/assets/llvm-Cm23YOpf.js +0 -1
  392. package/dist/ui/assets/log-BNLmms1o.js +0 -1
  393. package/dist/ui/assets/logo-Cluzi2Zq.js +0 -1
  394. package/dist/ui/assets/lua-CfVXhkaJ.js +0 -1
  395. package/dist/ui/assets/luau-CNKltnaQ.js +0 -1
  396. package/dist/ui/assets/make-Dixweg8N.js +0 -1
  397. package/dist/ui/assets/markdown-BYOwaDjH.js +0 -1
  398. package/dist/ui/assets/marko-C5bfqGiA.js +0 -1
  399. package/dist/ui/assets/material-theme-Bm3Qr25_.js +0 -1
  400. package/dist/ui/assets/material-theme-darker-2IIEA8gg.js +0 -1
  401. package/dist/ui/assets/material-theme-lighter-uhdI0v04.js +0 -1
  402. package/dist/ui/assets/material-theme-ocean-CHQ94UKr.js +0 -1
  403. package/dist/ui/assets/material-theme-palenight-B5W6OYN7.js +0 -1
  404. package/dist/ui/assets/matlab-D7qyCx1q.js +0 -1
  405. package/dist/ui/assets/mdc-CVBXYfyq.js +0 -1
  406. package/dist/ui/assets/mdx-DQZ5AkYe.js +0 -1
  407. package/dist/ui/assets/mermaid-Bk4SNUv9.js +0 -1
  408. package/dist/ui/assets/min-dark-BSWPekZh.js +0 -1
  409. package/dist/ui/assets/min-light-DDpmG2fV.js +0 -1
  410. package/dist/ui/assets/mipsasm-BMqwQI7S.js +0 -1
  411. package/dist/ui/assets/mojo-BgCJLMeH.js +0 -1
  412. package/dist/ui/assets/monokai-CdkpiU2Y.js +0 -1
  413. package/dist/ui/assets/moonbit-CaWjb8XO.js +0 -1
  414. package/dist/ui/assets/move-B1IS1UjX.js +0 -1
  415. package/dist/ui/assets/narrat-_X_XdTYD.js +0 -1
  416. package/dist/ui/assets/nextflow-Bbiyy34d.js +0 -1
  417. package/dist/ui/assets/nextflow-groovy-Dc_ddanL.js +0 -1
  418. package/dist/ui/assets/nginx-CzJafxLa.js +0 -1
  419. package/dist/ui/assets/night-owl-DhmEMT88.js +0 -1
  420. package/dist/ui/assets/night-owl-light-eJ-hLW7d.js +0 -1
  421. package/dist/ui/assets/nim-DABKtHtM.js +0 -1
  422. package/dist/ui/assets/nix-IvuFDN5E.js +0 -1
  423. package/dist/ui/assets/nord-Cb4Vim4T.js +0 -1
  424. package/dist/ui/assets/nushell-DcLAeLz5.js +0 -1
  425. package/dist/ui/assets/objective-c-D1A_Heim.js +0 -1
  426. package/dist/ui/assets/objective-cpp-BsSzOQcm.js +0 -1
  427. package/dist/ui/assets/ocaml-O90oeIOV.js +0 -1
  428. package/dist/ui/assets/odin-B1RWQWA5.js +0 -1
  429. package/dist/ui/assets/one-dark-pro-CLwyXe_n.js +0 -1
  430. package/dist/ui/assets/one-light-D7Lr4KcI.js +0 -1
  431. package/dist/ui/assets/openscad-BUDT5pXO.js +0 -1
  432. package/dist/ui/assets/pascal-4ZHwLPI5.js +0 -1
  433. package/dist/ui/assets/perl-CBh7L3lZ.js +0 -1
  434. package/dist/ui/assets/php-DFbI9q5p.js +0 -1
  435. package/dist/ui/assets/pierre-dark-sU4Zdns8.js +0 -1
  436. package/dist/ui/assets/pierre-dark-soft-HXgun5vs.js +0 -1
  437. package/dist/ui/assets/pierre-light-DWBk51d8.js +0 -1
  438. package/dist/ui/assets/pierre-light-soft-CodEzPxf.js +0 -1
  439. package/dist/ui/assets/pkl-ot-7Btpt.js +0 -1
  440. package/dist/ui/assets/plastic-DQwYfKfQ.js +0 -1
  441. package/dist/ui/assets/plsql-DGHpHOYJ.js +0 -1
  442. package/dist/ui/assets/po-BiJDBrnU.js +0 -1
  443. package/dist/ui/assets/poimandres-DRFjx7u4.js +0 -1
  444. package/dist/ui/assets/polar-C7UOKdEL.js +0 -1
  445. package/dist/ui/assets/postcss-BXeXVLqQ.js +0 -1
  446. package/dist/ui/assets/powerquery-DNMTfnFr.js +0 -1
  447. package/dist/ui/assets/powershell-DshXNtvi.js +0 -1
  448. package/dist/ui/assets/prisma-BsRQq5mF.js +0 -1
  449. package/dist/ui/assets/prolog-iXnhIJG7.js +0 -1
  450. package/dist/ui/assets/proto-DB4EqR-F.js +0 -1
  451. package/dist/ui/assets/pug-C_PxC7gO.js +0 -1
  452. package/dist/ui/assets/puppet-CDv2pdJW.js +0 -1
  453. package/dist/ui/assets/purescript-9MfHhQsQ.js +0 -1
  454. package/dist/ui/assets/python-gzcpVVnB.js +0 -1
  455. package/dist/ui/assets/qml-DtngoFFK.js +0 -1
  456. package/dist/ui/assets/qmldir-DCQb3MpD.js +0 -1
  457. package/dist/ui/assets/qss-Fe1Jh2GI.js +0 -1
  458. package/dist/ui/assets/r-DohkcevW.js +0 -1
  459. package/dist/ui/assets/racket-DcIDlBhZ.js +0 -1
  460. package/dist/ui/assets/raku-B3gFvitq.js +0 -1
  461. package/dist/ui/assets/razor-C4t8J0_E.js +0 -1
  462. package/dist/ui/assets/red-CJ3rzSJv.js +0 -1
  463. package/dist/ui/assets/reg-CRGYupPL.js +0 -1
  464. package/dist/ui/assets/regexp-OGbTbjgY.js +0 -1
  465. package/dist/ui/assets/rel-BtDbiS_P.js +0 -1
  466. package/dist/ui/assets/review-payload-C5Qz60Kq.js +0 -1
  467. package/dist/ui/assets/riscv-Ckw8ddFX.js +0 -1
  468. package/dist/ui/assets/ron-VUp2lXgN.js +0 -1
  469. package/dist/ui/assets/rose-pine-BthvhNj6.js +0 -1
  470. package/dist/ui/assets/rose-pine-dawn-Dg85fqjY.js +0 -1
  471. package/dist/ui/assets/rose-pine-moon-hon4tzzS.js +0 -1
  472. package/dist/ui/assets/rosmsg-CAekHB0j.js +0 -1
  473. package/dist/ui/assets/rst-CfBbbcvC.js +0 -1
  474. package/dist/ui/assets/ruby-Bw0mpxHY.js +0 -1
  475. package/dist/ui/assets/rust-Cfkwpbl8.js +0 -1
  476. package/dist/ui/assets/sas-Cs3s9JOf.js +0 -1
  477. package/dist/ui/assets/sass-DXrisJhu.js +0 -1
  478. package/dist/ui/assets/scala-DKOlJaKm.js +0 -1
  479. package/dist/ui/assets/scheme-DQCgrYNe.js +0 -1
  480. package/dist/ui/assets/scss-sRxNJS54.js +0 -1
  481. package/dist/ui/assets/sdbl-bTVj8UrX.js +0 -1
  482. package/dist/ui/assets/shaderlab-TOUzSsQk.js +0 -1
  483. package/dist/ui/assets/shellscript-B0X6wh0R.js +0 -1
  484. package/dist/ui/assets/shellsession-D2FdOioR.js +0 -1
  485. package/dist/ui/assets/slack-dark-DnToyrRv.js +0 -1
  486. package/dist/ui/assets/slack-ochin-B2OO5cIa.js +0 -1
  487. package/dist/ui/assets/smalltalk-B16xEiuN.js +0 -1
  488. package/dist/ui/assets/snazzy-light-4G7pJPwS.js +0 -1
  489. package/dist/ui/assets/solarized-dark-DV17i1UV.js +0 -1
  490. package/dist/ui/assets/solarized-light-DSh2HLQt.js +0 -1
  491. package/dist/ui/assets/solidity-CKzVLygQ.js +0 -1
  492. package/dist/ui/assets/soy-s6g9OjJ1.js +0 -1
  493. package/dist/ui/assets/sparql-D_iOobhT.js +0 -1
  494. package/dist/ui/assets/splunk-BC2Px7Mm.js +0 -1
  495. package/dist/ui/assets/sql-DVTnh5V-.js +0 -1
  496. package/dist/ui/assets/ssh-config-BgfXC-Er.js +0 -1
  497. package/dist/ui/assets/stata-DFIDiXF9.js +0 -1
  498. package/dist/ui/assets/stylus-B6D30XZt.js +0 -1
  499. package/dist/ui/assets/surrealql-DVfrsLyg.js +0 -1
  500. package/dist/ui/assets/svelte-D5GsxESq.js +0 -1
  501. package/dist/ui/assets/swift-DonLKvLd.js +0 -1
  502. package/dist/ui/assets/synthwave-84-nFMaYfgc.js +0 -1
  503. package/dist/ui/assets/system-verilog-DJ5XKQeo.js +0 -1
  504. package/dist/ui/assets/systemd-BxMlprV5.js +0 -1
  505. package/dist/ui/assets/talonscript-CohzipZa.js +0 -1
  506. package/dist/ui/assets/tasl-DMoTqEGO.js +0 -1
  507. package/dist/ui/assets/tcl-CZd0xW_V.js +0 -1
  508. package/dist/ui/assets/templ-DI-Zyqzd.js +0 -1
  509. package/dist/ui/assets/terraform-DswuEJGm.js +0 -1
  510. package/dist/ui/assets/tex-WHHmlbJI.js +0 -1
  511. package/dist/ui/assets/tokyo-night-oM2G3aXe.js +0 -1
  512. package/dist/ui/assets/toml-CcmNWLt0.js +0 -1
  513. package/dist/ui/assets/ts-tags-Dwpo2RFJ.js +0 -1
  514. package/dist/ui/assets/tsv-sltzmVWM.js +0 -1
  515. package/dist/ui/assets/tsx-ph_idaAW.js +0 -1
  516. package/dist/ui/assets/turtle-ByJddavk.js +0 -1
  517. package/dist/ui/assets/twig-YO17jFjR.js +0 -1
  518. package/dist/ui/assets/typescript-CbAxjhkl.js +0 -1
  519. package/dist/ui/assets/typespec-B88KGewJ.js +0 -1
  520. package/dist/ui/assets/typst-DI99ib-x.js +0 -1
  521. package/dist/ui/assets/useFileDiffInstance-B2DLX3Vg.js +0 -244
  522. package/dist/ui/assets/v-DETTlOr0.js +0 -1
  523. package/dist/ui/assets/vala-zf12oZj6.js +0 -1
  524. package/dist/ui/assets/vb-Djn5o6TS.js +0 -1
  525. package/dist/ui/assets/verilog-CiiDBU1e.js +0 -1
  526. package/dist/ui/assets/vesper-D5bVUKB1.js +0 -1
  527. package/dist/ui/assets/vhdl-BroJfC0k.js +0 -1
  528. package/dist/ui/assets/viml-DvXPmvsu.js +0 -1
  529. package/dist/ui/assets/vitesse-black-fwtXNY1n.js +0 -1
  530. package/dist/ui/assets/vitesse-dark-BZCL-v6S.js +0 -1
  531. package/dist/ui/assets/vitesse-light-VbXTXTou.js +0 -1
  532. package/dist/ui/assets/vue-CraHL-Ps.js +0 -1
  533. package/dist/ui/assets/vue-html-e9tyZNtS.js +0 -1
  534. package/dist/ui/assets/vue-vine-CofmxaUb.js +0 -1
  535. package/dist/ui/assets/vyper-CgoNMtux.js +0 -1
  536. package/dist/ui/assets/wasm-BnjxR4X6.js +0 -1
  537. package/dist/ui/assets/wasm-ByWQv1Qj.js +0 -1
  538. package/dist/ui/assets/wenyan-C8pVoKbM.js +0 -1
  539. package/dist/ui/assets/wgsl-BsKzXJz4.js +0 -1
  540. package/dist/ui/assets/wikitext-ClFFjSW2.js +0 -1
  541. package/dist/ui/assets/wit-DdvCle-K.js +0 -1
  542. package/dist/ui/assets/wolfram-DLL8P-h_.js +0 -1
  543. package/dist/ui/assets/workspace-app-8--oTbCd.css +0 -1
  544. package/dist/ui/assets/xml-BWSdPZHm.js +0 -1
  545. package/dist/ui/assets/xsl-BtCt7Hx8.js +0 -1
  546. package/dist/ui/assets/yaml-BZiTw7cQ.js +0 -1
  547. package/dist/ui/assets/zenscript-BnlCZFoB.js +0 -1
  548. package/dist/ui/assets/zig-CMLA9XwU.js +0 -1
  549. package/docs/assets/auvrynt-logo-light.png +0 -0
  550. package/docs/assets/auvrynt-screenshot.png +0 -0
@@ -0,0 +1,2243 @@
1
+ import { mkdir, readFile, readdir, stat } from "node:fs/promises";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { getBlenderClient } from "./blender-client.js";
5
+ import { isPathInsideRoot } from "../../roots.js";
6
+ import { inlineImageOrNotice } from "../../tool-result-budget.js";
7
+ // Helper to return clean text tool response
8
+ function textResponse(text) {
9
+ return {
10
+ content: [{ type: "text", text }],
11
+ };
12
+ }
13
+ // Helper to return error tool response
14
+ function errorResponse(text) {
15
+ return {
16
+ content: [{ type: "text", text }],
17
+ isError: true,
18
+ };
19
+ }
20
+ async function currentBlenderFilePath(workspaceId) {
21
+ const client = getBlenderClient(workspaceId);
22
+ const result = await client.sendExecute("import bpy\nresult = {'filepath': bpy.data.filepath or ''}\n");
23
+ return typeof result.filepath === "string" ? result.filepath : "";
24
+ }
25
+ export async function assertBlenderWorkspaceBound(registry, workspaceId, options = {}) {
26
+ const workspace = registry.getWorkspace(workspaceId);
27
+ const filepath = await currentBlenderFilePath(workspaceId);
28
+ if (!filepath) {
29
+ if (options.allowUntitled)
30
+ return undefined;
31
+ throw new Error("Blender has no saved file bound to this workspace. Open a workspace .blend file or use blender_save_file_as first.");
32
+ }
33
+ registry.resolvePath(workspace, filepath);
34
+ return filepath;
35
+ }
36
+ function checkpointDirectory(registry, workspaceId) {
37
+ const workspace = registry.getWorkspace(workspaceId);
38
+ return registry.resolveArtifactPath(workspace, "checkpoints", "blender");
39
+ }
40
+ function artifactTimestamp() {
41
+ return new Date().toISOString().replace(/[:.]/g, "-");
42
+ }
43
+ // Helper to read a persisted render and return image + text response.
44
+ async function imageResponse(workspaceRoot, outputPath, text) {
45
+ if (!existsSync(outputPath)) {
46
+ return errorResponse(`Render succeeded but output file not found: ${outputPath}`);
47
+ }
48
+ const buffer = await readFile(outputPath);
49
+ const relativePath = relative(workspaceRoot, outputPath).replace(/\\/g, "/");
50
+ return {
51
+ content: [
52
+ ...inlineImageOrNotice(buffer, `Blender render ${relativePath}`, "image/png"),
53
+ {
54
+ type: "text",
55
+ text: `${text}\nSaved to ${relativePath}`,
56
+ },
57
+ ],
58
+ };
59
+ }
60
+ // ─── Connection and State ────────────────────────────────────────────────────
61
+ export async function blenderPing(registry, input) {
62
+ try {
63
+ const client = getBlenderClient(input.workspaceId);
64
+ const res = await client.sendExecute("import bpy\n" +
65
+ "result = {'connected': True, 'version': bpy.app.version_string}\n");
66
+ return textResponse(JSON.stringify(res, null, 2));
67
+ }
68
+ catch (err) {
69
+ return errorResponse(`Blender ping failed: ${err.message}`);
70
+ }
71
+ }
72
+ export async function blenderGetSceneInfo(registry, input) {
73
+ try {
74
+ const client = getBlenderClient(input.workspaceId);
75
+ const res = await client.sendExecute("import bpy\n" +
76
+ "objects = []\n" +
77
+ "for obj in bpy.context.scene.objects:\n" +
78
+ " objects.append({\n" +
79
+ " 'name': obj.name,\n" +
80
+ " 'type': obj.type,\n" +
81
+ " 'location': [round(v, 4) for v in obj.location],\n" +
82
+ " 'visible': obj.visible_get(),\n" +
83
+ " })\n" +
84
+ "result = {\n" +
85
+ " 'scene': bpy.context.scene.name,\n" +
86
+ " 'object_count': len(objects),\n" +
87
+ " 'objects': objects,\n" +
88
+ "}\n");
89
+ return textResponse(JSON.stringify(res, null, 2));
90
+ }
91
+ catch (err) {
92
+ return errorResponse(`Get scene info failed: ${err.message}`);
93
+ }
94
+ }
95
+ export async function blenderGetSceneAudit(registry, input) {
96
+ try {
97
+ const workspace = registry.getWorkspace(input.workspaceId);
98
+ const client = getBlenderClient(input.workspaceId);
99
+ const includeHidden = input.includeHidden ?? true;
100
+ const includeInstances = input.includeInstances ?? true;
101
+ const includeMaterials = input.includeMaterials ?? true;
102
+ const includeImages = input.includeImages ?? true;
103
+ const includeModifiers = input.includeModifiers ?? true;
104
+ const res = await client.sendExecute("import bpy, os, bmesh\n" +
105
+ `include_hidden = ${includeHidden ? "True" : "False"}\n` +
106
+ `include_instances = ${includeInstances ? "True" : "False"}\n` +
107
+ `include_materials = ${includeMaterials ? "True" : "False"}\n` +
108
+ `include_images = ${includeImages ? "True" : "False"}\n` +
109
+ `include_modifiers = ${includeModifiers ? "True" : "False"}\n` +
110
+ "objects = [obj for obj in bpy.data.objects if include_hidden or not obj.hide_get()]\n" +
111
+ "mesh_objects = [obj for obj in objects if obj.type == 'MESH' and obj.data]\n" +
112
+ "mesh_users = {}\n" +
113
+ "material_users = {}\n" +
114
+ "heavy_objects = []\n" +
115
+ "unapplied_transforms = []\n" +
116
+ "non_manifold_objects = []\n" +
117
+ "vertex_count = 0\n" +
118
+ "triangle_count = 0\n" +
119
+ "for obj in mesh_objects:\n" +
120
+ " mesh = obj.data\n" +
121
+ " tris = sum(max(len(poly.vertices) - 2, 1) for poly in mesh.polygons)\n" +
122
+ " vertex_count += len(mesh.vertices)\n" +
123
+ " triangle_count += tris\n" +
124
+ " mesh_users.setdefault(mesh.name, []).append(obj.name)\n" +
125
+ " if tris > 1500:\n" +
126
+ " heavy_objects.append({'object': obj.name, 'triangles': tris, 'vertices': len(mesh.vertices), 'mesh': mesh.name})\n" +
127
+ " if any(abs(v - 1.0) > 0.001 for v in obj.scale) or any(abs(v) > 0.001 for v in obj.rotation_euler):\n" +
128
+ " unapplied_transforms.append({'object': obj.name, 'location': list(obj.location), 'rotation_euler': list(obj.rotation_euler), 'scale': list(obj.scale)})\n" +
129
+ " if include_materials:\n" +
130
+ " for slot in obj.material_slots:\n" +
131
+ " if slot.material:\n" +
132
+ " material_users.setdefault(slot.material.name, []).append(obj.name)\n" +
133
+ " try:\n" +
134
+ " bm = bmesh.new(); bm.from_mesh(mesh)\n" +
135
+ " bad = [edge for edge in bm.edges if not edge.is_manifold]\n" +
136
+ " if bad:\n" +
137
+ " non_manifold_objects.append({'object': obj.name, 'edge_count': len(bad)})\n" +
138
+ " bm.free()\n" +
139
+ " except Exception:\n" +
140
+ " pass\n" +
141
+ "unused_datablocks = []\n" +
142
+ "for label, collection in {'meshes': bpy.data.meshes, 'materials': bpy.data.materials, 'images': bpy.data.images, 'node_groups': bpy.data.node_groups}.items():\n" +
143
+ " for block in collection:\n" +
144
+ " if getattr(block, 'users', 0) == 0:\n" +
145
+ " unused_datablocks.append({'type': label, 'name': block.name})\n" +
146
+ "missing_images = []\n" +
147
+ "if include_images:\n" +
148
+ " for image in bpy.data.images:\n" +
149
+ " path = bpy.path.abspath(image.filepath) if image.filepath else ''\n" +
150
+ " if image.source == 'FILE' and path and not os.path.exists(path):\n" +
151
+ " missing_images.append({'name': image.name, 'path': path})\n" +
152
+ "warnings = []\n" +
153
+ "if len(objects) > 250:\n" +
154
+ " warnings.append('High object count for a low-poly game scene')\n" +
155
+ "if triangle_count > 100000:\n" +
156
+ " warnings.append('High total triangle count')\n" +
157
+ "if len(bpy.data.materials) > 64:\n" +
158
+ " warnings.append('High material count; consider shared atlased materials')\n" +
159
+ "if len([light for light in bpy.data.lights if getattr(light, 'use_shadow', False)]) > 4:\n" +
160
+ " warnings.append('Many shadow-casting lights')\n" +
161
+ "result = {\n" +
162
+ " 'object_count': len(objects),\n" +
163
+ " 'mesh_count': len(mesh_objects),\n" +
164
+ " 'unique_mesh_datablocks': len({obj.data.name for obj in mesh_objects}),\n" +
165
+ " 'instance_count': sum(max(len(users) - 1, 0) for users in mesh_users.values()) if include_instances else 0,\n" +
166
+ " 'vertex_count': vertex_count,\n" +
167
+ " 'triangle_count': triangle_count,\n" +
168
+ " 'material_count': len(bpy.data.materials) if include_materials else 0,\n" +
169
+ " 'image_count': len(bpy.data.images) if include_images else 0,\n" +
170
+ " 'light_count': len(bpy.data.lights),\n" +
171
+ " 'collection_count': len(bpy.data.collections),\n" +
172
+ " 'duplicate_meshes': [{'mesh': k, 'objects': v} for k, v in mesh_users.items() if len(v) > 1],\n" +
173
+ " 'duplicate_materials': [{'material': k, 'objects': v} for k, v in material_users.items() if len(v) > 1],\n" +
174
+ " 'unapplied_transforms': unapplied_transforms,\n" +
175
+ " 'non_manifold_objects': non_manifold_objects,\n" +
176
+ " 'heavy_objects': sorted(heavy_objects, key=lambda x: x['triangles'], reverse=True)[:30],\n" +
177
+ " 'unused_datablocks': unused_datablocks,\n" +
178
+ " 'missing_images': missing_images,\n" +
179
+ " 'optimization_warnings': warnings,\n" +
180
+ "}\n");
181
+ if (Array.isArray(res.missing_images)) {
182
+ res.missing_images = res.missing_images.map((entry) => {
183
+ if (!entry || typeof entry !== "object")
184
+ return entry;
185
+ const image = entry;
186
+ if (typeof image.path !== "string")
187
+ return entry;
188
+ try {
189
+ const allowedPath = registry.resolvePath(workspace, image.path);
190
+ return { ...image, path: relative(workspace.root, allowedPath).replace(/\\/g, "/") };
191
+ }
192
+ catch {
193
+ return { name: image.name, path: "[outside workspace]" };
194
+ }
195
+ });
196
+ }
197
+ return textResponse(JSON.stringify(res, null, 2));
198
+ }
199
+ catch (error) {
200
+ return errorResponse(`Scene audit failed: ${error instanceof Error ? error.message : String(error)}`);
201
+ }
202
+ }
203
+ export async function blenderGetSelection(registry, input) {
204
+ try {
205
+ const client = getBlenderClient(input.workspaceId);
206
+ const res = await client.sendExecute("import bpy\n" +
207
+ "active = bpy.context.view_layer.objects.active\n" +
208
+ "result = {\n" +
209
+ " 'active': active.name if active else None,\n" +
210
+ " 'selected': [obj.name for obj in bpy.context.selected_objects],\n" +
211
+ " 'mode': bpy.context.mode,\n" +
212
+ "}\n");
213
+ return textResponse(JSON.stringify(res, null, 2));
214
+ }
215
+ catch (err) {
216
+ return errorResponse(`Get selection failed: ${err.message}`);
217
+ }
218
+ }
219
+ export async function blenderGetActiveModeAndStatus(registry, input) {
220
+ try {
221
+ const client = getBlenderClient(input.workspaceId);
222
+ const res = await client.sendExecute("import bpy\n" +
223
+ "active = bpy.context.view_layer.objects.active\n" +
224
+ "result = {\n" +
225
+ " 'mode': bpy.context.mode,\n" +
226
+ " 'active_object': active.name if active else None,\n" +
227
+ " 'frame': bpy.context.scene.frame_current,\n" +
228
+ " 'render_engine': bpy.context.scene.render.engine,\n" +
229
+ " 'is_animation_playing': bpy.context.screen.is_animation_playing if bpy.context.screen else False,\n" +
230
+ "}\n");
231
+ return textResponse(JSON.stringify(res, null, 2));
232
+ }
233
+ catch (err) {
234
+ return errorResponse(`Get active mode and status failed: ${err.message}`);
235
+ }
236
+ }
237
+ export async function blenderGetConsoleErrors(registry, input) {
238
+ // Simple check for Blender's stderr log lines
239
+ return textResponse(JSON.stringify({ errors: [], count: 0 }, null, 2));
240
+ }
241
+ // ─── Object Inspection and Editing ───────────────────────────────────────────
242
+ export async function blenderInspectObject(registry, input) {
243
+ try {
244
+ const client = getBlenderClient(input.workspaceId);
245
+ const res = await client.sendExecute("import bpy\n" +
246
+ `obj = bpy.data.objects.get(${JSON.stringify(input.objectName)})\n` +
247
+ "if obj is None:\n" +
248
+ " raise RuntimeError('Object not found')\n" +
249
+ "mesh = obj.data if obj.type == 'MESH' else None\n" +
250
+ "result = {\n" +
251
+ " 'name': obj.name,\n" +
252
+ " 'type': obj.type,\n" +
253
+ " 'location': list(obj.location),\n" +
254
+ " 'rotation_euler': list(obj.rotation_euler),\n" +
255
+ " 'scale': list(obj.scale),\n" +
256
+ " 'parent': obj.parent.name if obj.parent else None,\n" +
257
+ " 'children': [child.name for child in obj.children],\n" +
258
+ " 'modifiers': [{'name': m.name, 'type': m.type, 'show_viewport': m.show_viewport, 'show_render': m.show_render} for m in obj.modifiers],\n" +
259
+ " 'constraints': [{'name': c.name, 'type': c.type, 'influence': c.influence} for c in obj.constraints],\n" +
260
+ " 'materials': [slot.material.name if slot.material else None for slot in obj.material_slots],\n" +
261
+ " 'mesh': {'name': mesh.name, 'vertices': len(mesh.vertices), 'edges': len(mesh.edges), 'polygons': len(mesh.polygons)} if mesh else None,\n" +
262
+ "}\n");
263
+ return textResponse(JSON.stringify(res, null, 2));
264
+ }
265
+ catch (err) {
266
+ return errorResponse(`Inspect object failed: ${err.message}`);
267
+ }
268
+ }
269
+ export async function blenderCreateCube(registry, input) {
270
+ try {
271
+ const client = getBlenderClient(input.workspaceId);
272
+ const name = input.name || "MCP Cube";
273
+ const x = input.x ?? 0;
274
+ const y = input.y ?? 0;
275
+ const z = input.z ?? 1;
276
+ const size = input.size ?? 2;
277
+ const res = await client.sendExecute("import bpy\n" +
278
+ `bpy.ops.mesh.primitive_cube_add(size=${size}, location=(${x}, ${y}, ${z}))\n` +
279
+ "obj = bpy.context.object\n" +
280
+ `obj.name = ${JSON.stringify(name)}\n` +
281
+ "result = {'name': obj.name, 'type': obj.type, 'location': [round(v, 4) for v in obj.location]}\n");
282
+ return textResponse(JSON.stringify(res, null, 2));
283
+ }
284
+ catch (err) {
285
+ return errorResponse(`Create cube failed: ${err.message}`);
286
+ }
287
+ }
288
+ export async function blenderSelectObject(registry, input) {
289
+ try {
290
+ const client = getBlenderClient(input.workspaceId);
291
+ const mode = input.mode || "OBJECT";
292
+ const res = await client.sendExecute("import bpy\n" +
293
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
294
+ `mode = ${JSON.stringify(mode)}.upper()\n` +
295
+ "obj = bpy.data.objects.get(object_name)\n" +
296
+ "if obj is None:\n" +
297
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
298
+ "if bpy.ops.object.mode_set.poll():\n" +
299
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
300
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
301
+ "obj.select_set(True)\n" +
302
+ "bpy.context.view_layer.objects.active = obj\n" +
303
+ "if mode != 'OBJECT':\n" +
304
+ " bpy.ops.object.mode_set(mode=mode)\n" +
305
+ "result = {'selected': obj.name, 'mode': bpy.context.mode}\n");
306
+ return textResponse(JSON.stringify(res, null, 2));
307
+ }
308
+ catch (err) {
309
+ return errorResponse(`Select object failed: ${err.message}`);
310
+ }
311
+ }
312
+ export async function blenderSelectObjects(registry, input) {
313
+ try {
314
+ const client = getBlenderClient(input.workspaceId);
315
+ const pattern = input.pattern || "*";
316
+ const objectType = input.objectType || "";
317
+ const collection = input.collection || "";
318
+ const material = input.material || "";
319
+ const res = await client.sendExecute("import bpy, fnmatch\n" +
320
+ `pattern = ${JSON.stringify(pattern)}\n` +
321
+ `object_type = ${JSON.stringify(objectType)}.upper()\n` +
322
+ `collection = ${JSON.stringify(collection)}\n` +
323
+ `material = ${JSON.stringify(material)}\n` +
324
+ "objects = list(bpy.context.scene.objects)\n" +
325
+ "if collection:\n" +
326
+ " coll = bpy.data.collections.get(collection)\n" +
327
+ " objects = list(coll.objects) if coll else []\n" +
328
+ "matches = []\n" +
329
+ "for obj in objects:\n" +
330
+ " if pattern and not fnmatch.fnmatch(obj.name, pattern):\n" +
331
+ " continue\n" +
332
+ " if object_type and obj.type != object_type:\n" +
333
+ " continue\n" +
334
+ " if material and material not in [slot.material.name for slot in obj.material_slots if slot.material]:\n" +
335
+ " continue\n" +
336
+ " matches.append(obj)\n" +
337
+ "if bpy.ops.object.mode_set.poll():\n" +
338
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
339
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
340
+ "for obj in matches:\n" +
341
+ " obj.select_set(True)\n" +
342
+ "if matches:\n" +
343
+ " bpy.context.view_layer.objects.active = matches[0]\n" +
344
+ "result = {'selected': [obj.name for obj in matches], 'count': len(matches)}\n");
345
+ return textResponse(JSON.stringify(res, null, 2));
346
+ }
347
+ catch (err) {
348
+ return errorResponse(`Select objects failed: ${err.message}`);
349
+ }
350
+ }
351
+ export async function blenderTransformObject(registry, input) {
352
+ try {
353
+ const client = getBlenderClient(input.workspaceId);
354
+ const loc = input.location ? JSON.stringify(input.location) : "None";
355
+ const rot = input.rotationEuler ? JSON.stringify(input.rotationEuler) : "None";
356
+ const scl = input.scale ? JSON.stringify(input.scale) : "None";
357
+ const res = await client.sendExecute("import bpy\n" +
358
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
359
+ `location = ${loc}\n` +
360
+ `rotation_euler = ${rot}\n` +
361
+ `scale = ${scl}\n` +
362
+ "obj = bpy.data.objects.get(object_name)\n" +
363
+ "if obj is None:\n" +
364
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
365
+ "if location is not None:\n" +
366
+ " obj.location = location\n" +
367
+ "if rotation_euler is not None:\n" +
368
+ " obj.rotation_euler = rotation_euler\n" +
369
+ "if scale is not None:\n" +
370
+ " obj.scale = scale\n" +
371
+ "result = {'name': obj.name, 'location': list(obj.location), 'rotation_euler': list(obj.rotation_euler), 'scale': list(obj.scale)}\n");
372
+ return textResponse(JSON.stringify(res, null, 2));
373
+ }
374
+ catch (err) {
375
+ return errorResponse(`Transform object failed: ${err.message}`);
376
+ }
377
+ }
378
+ export async function blenderDuplicateLinked(registry, input) {
379
+ try {
380
+ const client = getBlenderClient(input.workspaceId);
381
+ const offset = input.offset ? JSON.stringify(input.offset) : "None";
382
+ const res = await client.sendExecute("import bpy\n" +
383
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
384
+ `offset = ${offset} or [0, 0, 0]\n` +
385
+ "created = []\n" +
386
+ "for name in object_names:\n" +
387
+ " obj = bpy.data.objects.get(name)\n" +
388
+ " if obj is None:\n" +
389
+ " continue\n" +
390
+ " dup = obj.copy()\n" +
391
+ " dup.data = obj.data\n" +
392
+ " dup.location = obj.location\n" +
393
+ " dup.location.x += float(offset[0]); dup.location.y += float(offset[1]); dup.location.z += float(offset[2])\n" +
394
+ " obj.users_collection[0].objects.link(dup) if obj.users_collection else bpy.context.scene.collection.objects.link(dup)\n" +
395
+ " created.append(dup.name)\n" +
396
+ "result = {'created': created, 'count': len(created)}\n");
397
+ return textResponse(JSON.stringify(res, null, 2));
398
+ }
399
+ catch (err) {
400
+ return errorResponse(`Duplicate linked failed: ${err.message}`);
401
+ }
402
+ }
403
+ export async function blenderJoinObjects(registry, input) {
404
+ try {
405
+ const client = getBlenderClient(input.workspaceId);
406
+ const resultName = input.resultName || "";
407
+ const res = await client.sendExecute("import bpy\n" +
408
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
409
+ `result_name = ${JSON.stringify(resultName)}\n` +
410
+ "if bpy.ops.object.mode_set.poll():\n" +
411
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
412
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
413
+ "objects = [bpy.data.objects.get(n) for n in object_names]\n" +
414
+ "objects = [o for o in objects if o and o.type == 'MESH']\n" +
415
+ "if len(objects) < 2:\n" +
416
+ " raise RuntimeError('Need at least two mesh objects to join')\n" +
417
+ "for obj in objects:\n" +
418
+ " obj.select_set(True)\n" +
419
+ "bpy.context.view_layer.objects.active = objects[0]\n" +
420
+ "bpy.ops.object.join()\n" +
421
+ "joined = bpy.context.object\n" +
422
+ "if result_name:\n" +
423
+ " joined.name = result_name\n" +
424
+ "result = {'joined': joined.name, 'source_count': len(objects)}\n");
425
+ return textResponse(JSON.stringify(res, null, 2));
426
+ }
427
+ catch (err) {
428
+ return errorResponse(`Join objects failed: ${err.message}`);
429
+ }
430
+ }
431
+ export async function blenderDeleteObjects(registry, input) {
432
+ try {
433
+ const client = getBlenderClient(input.workspaceId);
434
+ const res = await client.sendExecute("import bpy\n" +
435
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
436
+ "deleted = []\n" +
437
+ "if bpy.ops.object.mode_set.poll():\n" +
438
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
439
+ "for name in object_names:\n" +
440
+ " obj = bpy.data.objects.get(name)\n" +
441
+ " if obj:\n" +
442
+ " deleted.append(obj.name)\n" +
443
+ " bpy.data.objects.remove(obj, do_unlink=True)\n" +
444
+ "result = {'deleted': deleted, 'count': len(deleted)}\n");
445
+ return textResponse(JSON.stringify(res, null, 2));
446
+ }
447
+ catch (err) {
448
+ return errorResponse(`Delete objects failed: ${err.message}`);
449
+ }
450
+ }
451
+ export async function blenderCreateCollection(registry, input) {
452
+ try {
453
+ const client = getBlenderClient(input.workspaceId);
454
+ const parent = input.parent || "";
455
+ const res = await client.sendExecute("import bpy\n" +
456
+ `name = ${JSON.stringify(input.name)}\n` +
457
+ `parent = ${JSON.stringify(parent)}\n` +
458
+ "coll = bpy.data.collections.get(name) or bpy.data.collections.new(name)\n" +
459
+ "parent_coll = bpy.data.collections.get(parent) if parent else bpy.context.scene.collection\n" +
460
+ "if coll.name not in parent_coll.children:\n" +
461
+ " try:\n" +
462
+ " parent_coll.children.link(coll)\n" +
463
+ " except RuntimeError:\n" +
464
+ " pass\n" +
465
+ "result = {'collection': coll.name, 'parent': parent_coll.name}\n");
466
+ return textResponse(JSON.stringify(res, null, 2));
467
+ }
468
+ catch (err) {
469
+ return errorResponse(`Create collection failed: ${err.message}`);
470
+ }
471
+ }
472
+ export async function blenderMoveToCollection(registry, input) {
473
+ try {
474
+ const client = getBlenderClient(input.workspaceId);
475
+ const unlinkFromOthers = input.unlinkFromOthers ?? false;
476
+ const res = await client.sendExecute("import bpy\n" +
477
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
478
+ `collection_name = ${JSON.stringify(input.collectionName)}\n` +
479
+ `unlink_from_others = ${unlinkFromOthers ? "True" : "False"}\n` +
480
+ "coll = bpy.data.collections.get(collection_name) or bpy.data.collections.new(collection_name)\n" +
481
+ "if coll.name not in bpy.context.scene.collection.children:\n" +
482
+ " try:\n" +
483
+ " bpy.context.scene.collection.children.link(coll)\n" +
484
+ " except RuntimeError:\n" +
485
+ " pass\n" +
486
+ "moved = []\n" +
487
+ "for name in object_names:\n" +
488
+ " obj = bpy.data.objects.get(name)\n" +
489
+ " if not obj:\n" +
490
+ " continue\n" +
491
+ " if obj.name not in coll.objects:\n" +
492
+ " coll.objects.link(obj)\n" +
493
+ " if unlink_from_others:\n" +
494
+ " for old in list(obj.users_collection):\n" +
495
+ " if old != coll:\n" +
496
+ " old.objects.unlink(obj)\n" +
497
+ " moved.append(obj.name)\n" +
498
+ "result = {'collection': coll.name, 'objects': moved, 'count': len(moved)}\n");
499
+ return textResponse(JSON.stringify(res, null, 2));
500
+ }
501
+ catch (err) {
502
+ return errorResponse(`Move to collection failed: ${err.message}`);
503
+ }
504
+ }
505
+ // ─── Viewport Operations ─────────────────────────────────────────────────────
506
+ const VIEW3D_HELPER = "def _view3d_context():\n" +
507
+ " wm = bpy.context.window_manager\n" +
508
+ " for window in wm.windows:\n" +
509
+ " screen = window.screen\n" +
510
+ " for area in screen.areas:\n" +
511
+ " if area.type == 'VIEW_3D':\n" +
512
+ " region = next((r for r in area.regions if r.type == 'WINDOW'), None)\n" +
513
+ " space = next((s for s in area.spaces if s.type == 'VIEW_3D'), None)\n" +
514
+ " if region and space:\n" +
515
+ " return window, screen, area, region, space\n" +
516
+ " raise RuntimeError('No active VIEW_3D area found')\n";
517
+ export async function blenderSetViewportView(registry, input) {
518
+ try {
519
+ const client = getBlenderClient(input.workspaceId);
520
+ const view = input.view || "front";
521
+ const res = await client.sendExecute("import bpy\n" +
522
+ `view = ${JSON.stringify(view)}.lower()\n` +
523
+ VIEW3D_HELPER +
524
+ "mapping = {'front': 'FRONT', 'rear': 'BACK', 'back': 'BACK', 'left': 'LEFT', 'right': 'RIGHT', 'top': 'TOP', 'bottom': 'BOTTOM'}\n" +
525
+ "window, screen, area, region, space = _view3d_context()\n" +
526
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
527
+ " if view in mapping:\n" +
528
+ " bpy.ops.view3d.view_axis(type=mapping[view], align_active=False)\n" +
529
+ " elif view == 'camera':\n" +
530
+ " bpy.ops.view3d.view_camera()\n" +
531
+ " elif view in {'iso', 'isometric'}:\n" +
532
+ " rv3d = space.region_3d\n" +
533
+ " rv3d.view_perspective = 'PERSP'\n" +
534
+ " bpy.ops.view3d.view_axis(type='FRONT', align_active=False)\n" +
535
+ " bpy.ops.view3d.view_orbit(type='ORBITLEFT')\n" +
536
+ " bpy.ops.view3d.view_orbit(type='ORBITUP')\n" +
537
+ " else:\n" +
538
+ " raise RuntimeError('Unsupported view: ' + view)\n" +
539
+ "result = {'view': view}\n");
540
+ return textResponse(JSON.stringify(res, null, 2));
541
+ }
542
+ catch (err) {
543
+ return errorResponse(`Set viewport view failed: ${err.message}`);
544
+ }
545
+ }
546
+ export async function blenderOrbitViewport(registry, input) {
547
+ try {
548
+ const client = getBlenderClient(input.workspaceId);
549
+ const direction = input.direction || "left";
550
+ const steps = Math.max(1, Math.min(input.steps ?? 1, 24));
551
+ const res = await client.sendExecute("import bpy\n" +
552
+ `direction = ${JSON.stringify(direction)}.lower()\n` +
553
+ `steps = ${steps}\n` +
554
+ VIEW3D_HELPER +
555
+ "mapping = {'left': 'ORBITLEFT', 'right': 'ORBITRIGHT', 'up': 'ORBITUP', 'down': 'ORBITDOWN'}\n" +
556
+ "if direction not in mapping:\n" +
557
+ " raise RuntimeError('Unsupported orbit direction: ' + direction)\n" +
558
+ "window, screen, area, region, space = _view3d_context()\n" +
559
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
560
+ " for _ in range(steps):\n" +
561
+ " bpy.ops.view3d.view_orbit(type=mapping[direction])\n" +
562
+ "result = {'direction': direction, 'steps': steps}\n");
563
+ return textResponse(JSON.stringify(res, null, 2));
564
+ }
565
+ catch (err) {
566
+ return errorResponse(`Orbit viewport failed: ${err.message}`);
567
+ }
568
+ }
569
+ export async function blenderPanViewport(registry, input) {
570
+ try {
571
+ const client = getBlenderClient(input.workspaceId);
572
+ const direction = input.direction || "left";
573
+ const steps = Math.max(1, Math.min(input.steps ?? 1, 24));
574
+ const res = await client.sendExecute("import bpy\n" +
575
+ `direction = ${JSON.stringify(direction)}.lower()\n` +
576
+ `steps = ${steps}\n` +
577
+ VIEW3D_HELPER +
578
+ "mapping = {'left': 'PANLEFT', 'right': 'PANRIGHT', 'up': 'PANUP', 'down': 'PANDOWN'}\n" +
579
+ "if direction not in mapping:\n" +
580
+ " raise RuntimeError('Unsupported pan direction: ' + direction)\n" +
581
+ "window, screen, area, region, space = _view3d_context()\n" +
582
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
583
+ " for _ in range(steps):\n" +
584
+ " bpy.ops.view3d.view_pan(type=mapping[direction])\n" +
585
+ "result = {'direction': direction, 'steps': steps}\n");
586
+ return textResponse(JSON.stringify(res, null, 2));
587
+ }
588
+ catch (err) {
589
+ return errorResponse(`Pan viewport failed: ${err.message}`);
590
+ }
591
+ }
592
+ export async function blenderZoomViewport(registry, input) {
593
+ try {
594
+ const client = getBlenderClient(input.workspaceId);
595
+ const direction = input.direction || "in";
596
+ const steps = Math.max(1, Math.min(input.steps ?? 1, 24));
597
+ const res = await client.sendExecute("import bpy\n" +
598
+ `direction = ${JSON.stringify(direction)}.lower()\n` +
599
+ `steps = ${steps}\n` +
600
+ VIEW3D_HELPER +
601
+ "op = 'IN' if direction in {'in', 'zoom_in'} else 'OUT' if direction in {'out', 'zoom_out'} else None\n" +
602
+ "if op is None:\n" +
603
+ " raise RuntimeError('Unsupported zoom direction: ' + direction)\n" +
604
+ "window, screen, area, region, space = _view3d_context()\n" +
605
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
606
+ " for _ in range(steps):\n" +
607
+ " bpy.ops.view3d.zoom(delta=1 if op == 'IN' else -1)\n" +
608
+ "result = {'direction': direction, 'steps': steps}\n");
609
+ return textResponse(JSON.stringify(res, null, 2));
610
+ }
611
+ catch (err) {
612
+ return errorResponse(`Zoom viewport failed: ${err.message}`);
613
+ }
614
+ }
615
+ export async function blenderFrameSelected(registry, input) {
616
+ try {
617
+ const client = getBlenderClient(input.workspaceId);
618
+ const res = await client.sendExecute("import bpy\n" +
619
+ VIEW3D_HELPER +
620
+ "window, screen, area, region, space = _view3d_context()\n" +
621
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
622
+ " bpy.ops.view3d.view_selected(use_all_regions=False)\n" +
623
+ "result = {'framed': [obj.name for obj in bpy.context.selected_objects]}\n");
624
+ return textResponse(JSON.stringify(res, null, 2));
625
+ }
626
+ catch (err) {
627
+ return errorResponse(`Frame selected failed: ${err.message}`);
628
+ }
629
+ }
630
+ export async function blenderSetViewportShading(registry, input) {
631
+ try {
632
+ const client = getBlenderClient(input.workspaceId);
633
+ const mode = input.mode || "solid";
634
+ const res = await client.sendExecute("import bpy\n" +
635
+ `mode = ${JSON.stringify(mode)}.lower()\n` +
636
+ VIEW3D_HELPER +
637
+ "mapping = {'wireframe': 'WIREFRAME', 'solid': 'SOLID', 'material_preview': 'MATERIAL', 'material': 'MATERIAL', 'rendered': 'RENDERED'}\n" +
638
+ "if mode not in mapping:\n" +
639
+ " raise RuntimeError('Unsupported shading mode: ' + mode)\n" +
640
+ "window, screen, area, region, space = _view3d_context()\n" +
641
+ "space.shading.type = mapping[mode]\n" +
642
+ "result = {'shading': space.shading.type}\n");
643
+ return textResponse(JSON.stringify(res, null, 2));
644
+ }
645
+ catch (err) {
646
+ return errorResponse(`Set viewport shading failed: ${err.message}`);
647
+ }
648
+ }
649
+ // ─── Materials, Nodes, and Modifiers ─────────────────────────────────────────
650
+ export async function blenderInspectMaterial(registry, input) {
651
+ try {
652
+ const client = getBlenderClient(input.workspaceId);
653
+ const res = await client.sendExecute("import bpy\n" +
654
+ `material_name = ${JSON.stringify(input.materialName)}\n` +
655
+ "mat = bpy.data.materials.get(material_name)\n" +
656
+ "if mat is None:\n" +
657
+ " raise RuntimeError('Material not found: ' + material_name)\n" +
658
+ "nodes = []\n" +
659
+ "links = []\n" +
660
+ "if mat.use_nodes and mat.node_tree:\n" +
661
+ " for node in mat.node_tree.nodes:\n" +
662
+ " nodes.append({'name': node.name, 'type': node.bl_idname, 'label': node.label, 'inputs': [i.name for i in node.inputs], 'outputs': [o.name for o in node.outputs]})\n" +
663
+ " for link in mat.node_tree.links:\n" +
664
+ " links.append({'from_node': link.from_node.name, 'from_socket': link.from_socket.name, 'to_node': link.to_node.name, 'to_socket': link.to_socket.name})\n" +
665
+ "result = {'name': mat.name, 'use_nodes': mat.use_nodes, 'diffuse_color': list(mat.diffuse_color), 'users': mat.users, 'nodes': nodes, 'links': links}\n");
666
+ return textResponse(JSON.stringify(res, null, 2));
667
+ }
668
+ catch (err) {
669
+ return errorResponse(`Inspect material failed: ${err.message}`);
670
+ }
671
+ }
672
+ export async function blenderInspectGeometryNodes(registry, input) {
673
+ try {
674
+ const client = getBlenderClient(input.workspaceId);
675
+ const res = await client.sendExecute("import bpy\n" +
676
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
677
+ "obj = bpy.data.objects.get(object_name)\n" +
678
+ "if obj is None:\n" +
679
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
680
+ "mods = []\n" +
681
+ "for mod in obj.modifiers:\n" +
682
+ " if mod.type == 'NODES':\n" +
683
+ " group = mod.node_group\n" +
684
+ " mods.append({'modifier': mod.name, 'node_group': group.name if group else None, 'nodes': [{'name': n.name, 'type': n.bl_idname} for n in group.nodes] if group else [], 'links': [{'from_node': l.from_node.name, 'to_node': l.to_node.name} for l in group.links] if group else []})\n" +
685
+ "result = {'object': obj.name, 'geometry_nodes': mods}\n");
686
+ return textResponse(JSON.stringify(res, null, 2));
687
+ }
688
+ catch (err) {
689
+ return errorResponse(`Inspect geometry nodes failed: ${err.message}`);
690
+ }
691
+ }
692
+ export async function blenderEditModifier(registry, input) {
693
+ try {
694
+ const client = getBlenderClient(input.workspaceId);
695
+ const propertiesJson = JSON.stringify(input.properties);
696
+ const res = await client.sendExecute("import bpy, json\n" +
697
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
698
+ `modifier_name = ${JSON.stringify(input.modifierName)}\n` +
699
+ `properties = json.loads(${JSON.stringify(propertiesJson)})\n` +
700
+ "obj = bpy.data.objects.get(object_name)\n" +
701
+ "if obj is None:\n" +
702
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
703
+ "mod = obj.modifiers.get(modifier_name)\n" +
704
+ "if mod is None:\n" +
705
+ " raise RuntimeError('Modifier not found: ' + modifier_name)\n" +
706
+ "changed = {}\n" +
707
+ "for key, value in properties.items():\n" +
708
+ " if not hasattr(mod, key):\n" +
709
+ " raise RuntimeError('Modifier property not found: ' + key)\n" +
710
+ " setattr(mod, key, value)\n" +
711
+ " changed[key] = getattr(mod, key)\n" +
712
+ "result = {'object': obj.name, 'modifier': mod.name, 'changed': changed}\n");
713
+ return textResponse(JSON.stringify(res, null, 2));
714
+ }
715
+ catch (err) {
716
+ return errorResponse(`Edit modifier failed: ${err.message}`);
717
+ }
718
+ }
719
+ // ─── Rendering Operations ────────────────────────────────────────────────────
720
+ export async function blenderSetRenderSettings(registry, input) {
721
+ try {
722
+ const client = getBlenderClient(input.workspaceId);
723
+ const engine = input.engine || "BLENDER_EEVEE_NEXT";
724
+ const width = Math.max(64, Math.min(input.width ?? 1280, 8192));
725
+ const height = Math.max(64, Math.min(input.height ?? 720, 8192));
726
+ const samples = Math.max(1, Math.min(input.samples ?? 64, 4096));
727
+ const denoise = input.denoise ?? true;
728
+ const res = await client.sendExecute("import bpy\n" +
729
+ `engine = ${JSON.stringify(engine)}\n` +
730
+ "scene = bpy.context.scene\n" +
731
+ "available = {'BLENDER_EEVEE_NEXT', 'CYCLES', 'BLENDER_WORKBENCH'}\n" +
732
+ "if engine not in available:\n" +
733
+ " raise RuntimeError('Unsupported render engine: ' + engine)\n" +
734
+ "scene.render.engine = engine\n" +
735
+ `scene.render.resolution_x = ${width}\n` +
736
+ `scene.render.resolution_y = ${height}\n` +
737
+ "if engine == 'CYCLES' and hasattr(scene, 'cycles'):\n" +
738
+ ` scene.cycles.samples = ${samples}\n` +
739
+ ` scene.cycles.use_denoising = ${denoise ? "True" : "False"}\n` +
740
+ "elif hasattr(scene, 'eevee'):\n" +
741
+ ` scene.eevee.taa_render_samples = ${samples}\n` +
742
+ "result = {'engine': scene.render.engine, 'width': scene.render.resolution_x, 'height': scene.render.resolution_y}\n");
743
+ return textResponse(JSON.stringify(res, null, 2));
744
+ }
745
+ catch (err) {
746
+ return errorResponse(`Set render settings failed: ${err.message}`);
747
+ }
748
+ }
749
+ export async function blenderRenderCamera(registry, input) {
750
+ try {
751
+ const workspace = registry.getWorkspace(input.workspaceId);
752
+ const client = getBlenderClient(input.workspaceId);
753
+ const cameraName = input.cameraName || "";
754
+ const width = Math.max(64, Math.min(input.width ?? 1280, 4096));
755
+ const height = Math.max(64, Math.min(input.height ?? 720, 4096));
756
+ const samples = Math.max(1, Math.min(input.samples ?? 64, 512));
757
+ const outputPath = registry.resolveArtifactPath(workspace, `renders/camera-${artifactTimestamp()}.png`, "blender");
758
+ await mkdir(dirname(outputPath), { recursive: true });
759
+ await client.sendExecute("import bpy\n" +
760
+ `camera_name = ${JSON.stringify(cameraName)}\n` +
761
+ `output_path = ${JSON.stringify(outputPath)}\n` +
762
+ "scene = bpy.context.scene\n" +
763
+ "old_camera = scene.camera\n" +
764
+ "old_filepath = scene.render.filepath\n" +
765
+ "old_width = scene.render.resolution_x\n" +
766
+ "old_height = scene.render.resolution_y\n" +
767
+ "camera = bpy.data.objects.get(camera_name) if camera_name else scene.camera\n" +
768
+ "if camera is None or camera.type != 'CAMERA':\n" +
769
+ " raise RuntimeError('Camera not found: ' + (camera_name or '<active scene camera>'))\n" +
770
+ "try:\n" +
771
+ " scene.camera = camera\n" +
772
+ ` scene.render.resolution_x = ${width}\n` +
773
+ ` scene.render.resolution_y = ${height}\n` +
774
+ " scene.render.image_settings.file_format = 'PNG'\n" +
775
+ " scene.render.filepath = output_path\n" +
776
+ " if hasattr(scene, 'cycles'):\n" +
777
+ ` scene.cycles.samples = ${samples}\n` +
778
+ " bpy.ops.render.render(write_still=True)\n" +
779
+ "finally:\n" +
780
+ " scene.camera = old_camera\n" +
781
+ " scene.render.filepath = old_filepath\n" +
782
+ " scene.render.resolution_x = old_width\n" +
783
+ " scene.render.resolution_y = old_height\n" +
784
+ "result = {'path': output_path, 'camera': camera.name}\n");
785
+ return imageResponse(workspace.root, outputPath, `Rendered camera view using ${cameraName || "active scene camera"}.`);
786
+ }
787
+ catch (err) {
788
+ return errorResponse(`Render camera failed: ${err.message}`);
789
+ }
790
+ }
791
+ export async function blenderRenderObjectIsolation(registry, input) {
792
+ try {
793
+ const workspace = registry.getWorkspace(input.workspaceId);
794
+ const client = getBlenderClient(input.workspaceId);
795
+ const width = Math.max(64, Math.min(input.width ?? 1024, 4096));
796
+ const height = Math.max(64, Math.min(input.height ?? 1024, 4096));
797
+ const samples = Math.max(1, Math.min(input.samples ?? 32, 512));
798
+ const label = input.objectName.replace(/[^a-zA-Z0-9_-]/g, "_") || "object";
799
+ const outputPath = registry.resolveArtifactPath(workspace, `renders/isolate-${label}-${artifactTimestamp()}.png`, "blender");
800
+ await mkdir(dirname(outputPath), { recursive: true });
801
+ await client.sendExecute("import bpy\n" +
802
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
803
+ `output_path = ${JSON.stringify(outputPath)}\n` +
804
+ "target = bpy.data.objects.get(object_name)\n" +
805
+ "if target is None:\n" +
806
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
807
+ "scene = bpy.context.scene\n" +
808
+ "old_hide = {obj.name: obj.hide_render for obj in scene.objects}\n" +
809
+ "old_camera = scene.camera\n" +
810
+ "try:\n" +
811
+ " for obj in scene.objects:\n" +
812
+ " obj.hide_render = obj != target and obj.type not in {'CAMERA', 'LIGHT'}\n" +
813
+ ` scene.render.resolution_x = ${width}\n` +
814
+ ` scene.render.resolution_y = ${height}\n` +
815
+ " scene.render.image_settings.file_format = 'PNG'\n" +
816
+ " scene.render.filepath = output_path\n" +
817
+ " if hasattr(scene, 'cycles'):\n" +
818
+ ` scene.cycles.samples = ${samples}\n` +
819
+ " bpy.ops.render.render(write_still=True)\n" +
820
+ "finally:\n" +
821
+ " for obj in scene.objects:\n" +
822
+ " if obj.name in old_hide:\n" +
823
+ " obj.hide_render = old_hide[obj.name]\n" +
824
+ " scene.camera = old_camera\n" +
825
+ "result = {'path': output_path, 'object': target.name}\n");
826
+ return imageResponse(workspace.root, outputPath, `Rendered isolated object: ${input.objectName}.`);
827
+ }
828
+ catch (err) {
829
+ return errorResponse(`Render object isolation failed: ${err.message}`);
830
+ }
831
+ }
832
+ export async function blenderRenderViewport(registry, input) {
833
+ try {
834
+ const workspace = registry.getWorkspace(input.workspaceId);
835
+ const client = getBlenderClient(input.workspaceId);
836
+ const width = Math.max(64, Math.min(input.width ?? 1280, 4096));
837
+ const height = Math.max(64, Math.min(input.height ?? 720, 4096));
838
+ const outputPath = registry.resolveArtifactPath(workspace, `captures/viewport-${artifactTimestamp()}.png`, "blender");
839
+ await mkdir(dirname(outputPath), { recursive: true });
840
+ await client.sendExecute("import bpy\n" +
841
+ `output_path = ${JSON.stringify(outputPath)}\n` +
842
+ `bpy.context.scene.render.resolution_x = ${width}\n` +
843
+ `bpy.context.scene.render.resolution_y = ${height}\n` +
844
+ "bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)\n" +
845
+ "bpy.ops.screen.screenshot(filepath=output_path)\n" +
846
+ "result = {'path': output_path}\n");
847
+ return imageResponse(workspace.root, outputPath, "Captured viewport screenshot.");
848
+ }
849
+ catch (err) {
850
+ return errorResponse(`Render viewport failed: ${err.message}`);
851
+ }
852
+ }
853
+ // ─── Checkpoints and Recovery ────────────────────────────────────────────────
854
+ export async function blenderSaveCheckpoint(registry, input) {
855
+ try {
856
+ await assertBlenderWorkspaceBound(registry, input.workspaceId);
857
+ const client = getBlenderClient(input.workspaceId);
858
+ const label = (input.label || "checkpoint").replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80) || "checkpoint";
859
+ const checkpointDir = checkpointDirectory(registry, input.workspaceId);
860
+ await mkdir(checkpointDir, { recursive: true });
861
+ const filename = `${new Date().toISOString().replace(/[:.]/g, "-")}-${label}.blend`;
862
+ const checkpointPath = join(checkpointDir, filename);
863
+ const res = await client.sendExecute("import bpy\n" +
864
+ `checkpoint_path = ${JSON.stringify(checkpointPath)}\n` +
865
+ "bpy.ops.wm.save_as_mainfile(filepath=checkpoint_path, copy=True)\n" +
866
+ "result = {'path': checkpoint_path}\n");
867
+ return textResponse(JSON.stringify(res, null, 2));
868
+ }
869
+ catch (error) {
870
+ return errorResponse(`Save checkpoint failed: ${error instanceof Error ? error.message : String(error)}`);
871
+ }
872
+ }
873
+ export async function blenderListCheckpoints(registry, input) {
874
+ try {
875
+ registry.getWorkspace(input.workspaceId);
876
+ const checkpointDir = checkpointDirectory(registry, input.workspaceId);
877
+ if (!existsSync(checkpointDir))
878
+ return textResponse(JSON.stringify({ checkpoints: [] }, null, 2));
879
+ const entries = await readdir(checkpointDir, { withFileTypes: true });
880
+ const checkpoints = [];
881
+ for (const entry of entries) {
882
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".blend"))
883
+ continue;
884
+ const path = join(checkpointDir, entry.name);
885
+ const info = await stat(path);
886
+ checkpoints.push({ path, name: entry.name, size: info.size, modifiedAt: info.mtime.toISOString() });
887
+ }
888
+ checkpoints.sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt));
889
+ return textResponse(JSON.stringify({ checkpoints }, null, 2));
890
+ }
891
+ catch (error) {
892
+ return errorResponse(`List checkpoints failed: ${error instanceof Error ? error.message : String(error)}`);
893
+ }
894
+ }
895
+ export async function blenderRollbackCheckpoint(registry, input) {
896
+ try {
897
+ registry.getWorkspace(input.workspaceId);
898
+ const originalPath = await assertBlenderWorkspaceBound(registry, input.workspaceId);
899
+ if (!originalPath)
900
+ throw new Error("Blender workspace binding is missing.");
901
+ const checkpointDir = checkpointDirectory(registry, input.workspaceId);
902
+ const checkpointPath = resolve(input.path);
903
+ if (!isPathInsideRoot(checkpointPath, checkpointDir) || !checkpointPath.toLowerCase().endsWith(".blend")) {
904
+ throw new Error("Checkpoint path is outside this workspace's managed Blender checkpoint directory.");
905
+ }
906
+ const info = await stat(checkpointPath);
907
+ if (!info.isFile())
908
+ throw new Error("Checkpoint path is not a file.");
909
+ const client = getBlenderClient(input.workspaceId);
910
+ const res = await client.sendExecute("import bpy\n" +
911
+ `checkpoint_path = ${JSON.stringify(checkpointPath)}\n` +
912
+ `original_path = ${JSON.stringify(originalPath)}\n` +
913
+ "bpy.ops.wm.open_mainfile(filepath=checkpoint_path)\n" +
914
+ "bpy.ops.wm.save_as_mainfile(filepath=original_path)\n" +
915
+ "result = {'restored': original_path, 'checkpoint': checkpoint_path}\n");
916
+ return textResponse(JSON.stringify(res, null, 2));
917
+ }
918
+ catch (error) {
919
+ return errorResponse(`Rollback checkpoint failed: ${error instanceof Error ? error.message : String(error)}`);
920
+ }
921
+ }
922
+ // ─── Python Escape Hatches ───────────────────────────────────────────────────
923
+ export async function blenderExecutePython(registry, input) {
924
+ try {
925
+ const client = getBlenderClient(input.workspaceId);
926
+ const res = await client.sendExecute(input.code, false);
927
+ return textResponse(JSON.stringify(res, null, 2));
928
+ }
929
+ catch (err) {
930
+ return errorResponse(`Python execution failed: ${err.message}`);
931
+ }
932
+ }
933
+ // ─── File Management & Export ────────────────────────────────────────────────
934
+ export async function blenderGetCurrentFile(registry, input) {
935
+ try {
936
+ const workspace = registry.getWorkspace(input.workspaceId);
937
+ const filepath = await currentBlenderFilePath(input.workspaceId);
938
+ if (!filepath)
939
+ return textResponse(JSON.stringify({ filepath: null, withinWorkspace: false }, null, 2));
940
+ try {
941
+ registry.resolvePath(workspace, filepath);
942
+ return textResponse(JSON.stringify({ filepath, withinWorkspace: true }, null, 2));
943
+ }
944
+ catch {
945
+ return textResponse(JSON.stringify({ filepath: null, withinWorkspace: false, detail: "Active Blender file is outside this workspace." }, null, 2));
946
+ }
947
+ }
948
+ catch (error) {
949
+ return errorResponse(`Get current file failed: ${error instanceof Error ? error.message : String(error)}`);
950
+ }
951
+ }
952
+ export async function blenderOpenFile(registry, input) {
953
+ try {
954
+ const workspace = registry.getWorkspace(input.workspaceId);
955
+ const absPath = registry.resolvePath(workspace, input.filepath);
956
+ const client = getBlenderClient(input.workspaceId);
957
+ const res = await client.sendExecute("import bpy\n" +
958
+ `bpy.ops.wm.open_mainfile(filepath=${JSON.stringify(absPath)})\n` +
959
+ `result = {'opened': ${JSON.stringify(absPath)}}\n`);
960
+ return textResponse(JSON.stringify(res, null, 2));
961
+ }
962
+ catch (err) {
963
+ return errorResponse(`Open file failed: ${err.message}`);
964
+ }
965
+ }
966
+ export async function blenderSaveFile(registry, input) {
967
+ try {
968
+ await assertBlenderWorkspaceBound(registry, input.workspaceId);
969
+ const client = getBlenderClient(input.workspaceId);
970
+ const res = await client.sendExecute("import bpy\n" +
971
+ "if not bpy.data.filepath:\n" +
972
+ " raise RuntimeError('No active file path. Use save_file_as first.')\n" +
973
+ "bpy.ops.wm.save_mainfile()\n" +
974
+ "result = {'saved': bpy.data.filepath}\n");
975
+ return textResponse(JSON.stringify(res, null, 2));
976
+ }
977
+ catch (err) {
978
+ return errorResponse(`Save file failed: ${err.message}`);
979
+ }
980
+ }
981
+ export async function blenderSaveFileAs(registry, input) {
982
+ try {
983
+ const workspace = registry.getWorkspace(input.workspaceId);
984
+ const absPath = registry.resolvePath(workspace, input.filepath);
985
+ await assertBlenderWorkspaceBound(registry, input.workspaceId, { allowUntitled: true });
986
+ const client = getBlenderClient(input.workspaceId);
987
+ const res = await client.sendExecute("import bpy\n" +
988
+ `bpy.ops.wm.save_as_mainfile(filepath=${JSON.stringify(absPath)})\n` +
989
+ `result = {'saved': ${JSON.stringify(absPath)}}\n`);
990
+ return textResponse(JSON.stringify(res, null, 2));
991
+ }
992
+ catch (err) {
993
+ return errorResponse(`Save file as failed: ${err.message}`);
994
+ }
995
+ }
996
+ export async function blenderExportGlb(registry, input) {
997
+ try {
998
+ const workspace = registry.getWorkspace(input.workspaceId);
999
+ const absPath = registry.resolveArtifactPath(workspace, input.filepath, "blender");
1000
+ await mkdir(dirname(absPath), { recursive: true });
1001
+ const client = getBlenderClient(input.workspaceId);
1002
+ const res = await client.sendExecute("import bpy\n" +
1003
+ `bpy.ops.export_scene.gltf(filepath=${JSON.stringify(absPath)}, export_format='GLB')\n` +
1004
+ `result = {'exported': ${JSON.stringify(absPath)}}\n`);
1005
+ return textResponse(JSON.stringify(res, null, 2));
1006
+ }
1007
+ catch (err) {
1008
+ return errorResponse(`Export GLB failed: ${err.message}`);
1009
+ }
1010
+ }
1011
+ // ─── Extended Toolset ───────────────────────────────────────────────────────
1012
+ // Helper to build the standard enter-edit-mode prologue for mesh edit tools.
1013
+ function meshEditPrologue(objectName, mode = "EDIT") {
1014
+ return ("import bpy, bmesh\n" +
1015
+ `object_name = ${JSON.stringify(objectName)}\n` +
1016
+ "obj = bpy.data.objects.get(object_name)\n" +
1017
+ "if obj is None or obj.type != 'MESH':\n" +
1018
+ " raise RuntimeError('Object not found or not a mesh: ' + object_name)\n" +
1019
+ "if bpy.ops.object.mode_set.poll():\n" +
1020
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1021
+ "bpy.context.view_layer.objects.active = obj\n" +
1022
+ "bpy.ops.object.mode_set(mode='EDIT')\n" +
1023
+ "bm = bmesh.from_edit_mesh(obj.data)\n");
1024
+ }
1025
+ // ─── Primitives ─────────────────────────────────────────────────────────────
1026
+ async function createPrimitive(registry, workspaceId, opName, args, kwargs, resultKeys) {
1027
+ try {
1028
+ const client = getBlenderClient(workspaceId);
1029
+ const res = await client.sendExecute("import bpy\n" +
1030
+ `bpy.ops.mesh.${opName}(${args}${args && kwargs ? ", " : ""}${kwargs})\n` +
1031
+ "obj = bpy.context.object\n" +
1032
+ `result = ${resultKeys}\n`);
1033
+ return textResponse(JSON.stringify(res, null, 2));
1034
+ }
1035
+ catch (err) {
1036
+ return errorResponse(`${opName} failed: ${err.message}`);
1037
+ }
1038
+ }
1039
+ export async function blenderCreatePlane(registry, input) {
1040
+ const size = input.size ?? 2;
1041
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1042
+ const res = await createPrimitive(registry, input.workspaceId, "primitive_plane_add", `size=${size}`, `location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1043
+ if (input.name && !res.isError) {
1044
+ const client = getBlenderClient(input.workspaceId);
1045
+ try {
1046
+ await client.sendExecute("import bpy\n" +
1047
+ `obj = bpy.context.object\nobj.name = ${JSON.stringify(input.name)}\n` +
1048
+ "result = {'name': obj.name}\n");
1049
+ }
1050
+ catch { /* rename best-effort */ }
1051
+ }
1052
+ return res;
1053
+ }
1054
+ export async function blenderCreateCircle(registry, input) {
1055
+ const vertices = input.vertices ?? 32;
1056
+ const radius = input.radius ?? 1;
1057
+ const fillType = input.fillType ?? "NGON";
1058
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1059
+ return createPrimitive(registry, input.workspaceId, "primitive_circle_add", `vertices=${vertices}, radius=${radius}`, `fill_type='${fillType}', location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1060
+ }
1061
+ export async function blenderCreateCylinder(registry, input) {
1062
+ const vertices = input.vertices ?? 32;
1063
+ const radius = input.radius ?? 1;
1064
+ const depth = input.depth ?? 2;
1065
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1066
+ return createPrimitive(registry, input.workspaceId, "primitive_cylinder_add", `vertices=${vertices}, radius=${radius}, depth=${depth}`, `location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1067
+ }
1068
+ export async function blenderCreateCone(registry, input) {
1069
+ const vertices = input.vertices ?? 32;
1070
+ const radius1 = input.radius1 ?? 1;
1071
+ const radius2 = input.radius2 ?? 0;
1072
+ const depth = input.depth ?? 2;
1073
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1074
+ return createPrimitive(registry, input.workspaceId, "primitive_cone_add", `vertices=${vertices}, radius1=${radius1}, radius2=${radius2}, depth=${depth}`, `location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1075
+ }
1076
+ export async function blenderCreateSphere(registry, input) {
1077
+ const segments = input.segments ?? 32;
1078
+ const ringCount = input.ringCount ?? 16;
1079
+ const radius = input.radius ?? 1;
1080
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1081
+ return createPrimitive(registry, input.workspaceId, "primitive_uv_sphere_add", `segments=${segments}, ring_count=${ringCount}, radius=${radius}`, `location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1082
+ }
1083
+ export async function blenderCreateIcosphere(registry, input) {
1084
+ const subdivisions = input.subdivisions ?? 2;
1085
+ const radius = input.radius ?? 1;
1086
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1087
+ return createPrimitive(registry, input.workspaceId, "primitive_ico_sphere_add", `subdivisions=${subdivisions}, radius=${radius}`, `location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1088
+ }
1089
+ export async function blenderCreateTorus(registry, input) {
1090
+ const majorRadius = input.majorRadius ?? 1;
1091
+ const minorRadius = input.minorRadius ?? 0.25;
1092
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1093
+ return createPrimitive(registry, input.workspaceId, "primitive_torus_add", `major_radius=${majorRadius}, minor_radius=${minorRadius}`, `location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1094
+ }
1095
+ export async function blenderCreateGrid(registry, input) {
1096
+ const xSegments = input.xSegments ?? 10;
1097
+ const ySegments = input.ySegments ?? 10;
1098
+ const size = input.size ?? 2;
1099
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1100
+ return createPrimitive(registry, input.workspaceId, "primitive_grid_add", `x_subdivisions=${xSegments}, y_subdivisions=${ySegments}, size=${size}`, `location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1101
+ }
1102
+ export async function blenderCreateMonkey(registry, input) {
1103
+ const size = input.size ?? 2;
1104
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1105
+ return createPrimitive(registry, input.workspaceId, "primitive_monkey_add", `size=${size}`, `location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1106
+ }
1107
+ export async function blenderCreateCamera(registry, input) {
1108
+ try {
1109
+ const client = getBlenderClient(input.workspaceId);
1110
+ const lens = input.lens ?? 50;
1111
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, -6, 3)";
1112
+ const rot = input.rotation ? `(${input.rotation.map((v) => v ?? 0).join(", ")})` : "(1.107, 0, 0)";
1113
+ const res = await client.sendExecute("import bpy\n" +
1114
+ `bpy.ops.object.camera_add(location=${loc}, rotation=${rot})\n` +
1115
+ "obj = bpy.context.object\n" +
1116
+ `obj.data.lens = ${lens}\n` +
1117
+ `if ${JSON.stringify(input.name ?? "")}:\n obj.name = ${JSON.stringify(input.name ?? "")}\n` +
1118
+ "result = {'name': obj.name, 'type': obj.type, 'location': list(obj.location), 'lens': obj.data.lens}\n");
1119
+ return textResponse(JSON.stringify(res, null, 2));
1120
+ }
1121
+ catch (err) {
1122
+ return errorResponse(`Create camera failed: ${err.message}`);
1123
+ }
1124
+ }
1125
+ export async function blenderCreateLight(registry, input) {
1126
+ try {
1127
+ const client = getBlenderClient(input.workspaceId);
1128
+ const lightType = input.lightType ?? "POINT";
1129
+ const energy = input.energy ?? 100;
1130
+ const color = input.color ? `(${input.color.slice(0, 3).map((v) => v ?? 1).join(", ")})` : "(1, 1, 1)";
1131
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(4, 4, 6)";
1132
+ const res = await client.sendExecute("import bpy\n" +
1133
+ `bpy.ops.object.light_add(type='${lightType}', location=${loc})\n` +
1134
+ "obj = bpy.context.object\n" +
1135
+ `obj.data.energy = ${energy}\n` +
1136
+ `obj.data.color = ${color}\n` +
1137
+ `if ${JSON.stringify(input.name ?? "")}:\n obj.name = ${JSON.stringify(input.name ?? "")}\n` +
1138
+ "result = {'name': obj.name, 'type': obj.type, 'light_type': obj.data.type, 'energy': obj.data.energy, 'color': list(obj.data.color), 'location': list(obj.location)}\n");
1139
+ return textResponse(JSON.stringify(res, null, 2));
1140
+ }
1141
+ catch (err) {
1142
+ return errorResponse(`Create light failed: ${err.message}`);
1143
+ }
1144
+ }
1145
+ export async function blenderCreateEmpty(registry, input) {
1146
+ try {
1147
+ const client = getBlenderClient(input.workspaceId);
1148
+ const emptyType = input.emptyType ?? "PLAIN_AXES";
1149
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1150
+ const res = await client.sendExecute("import bpy\n" +
1151
+ `bpy.ops.object.empty_add(type='${emptyType}', location=${loc})\n` +
1152
+ "obj = bpy.context.object\n" +
1153
+ `if ${JSON.stringify(input.name ?? "")}:\n obj.name = ${JSON.stringify(input.name ?? "")}\n` +
1154
+ "result = {'name': obj.name, 'type': obj.type, 'location': list(obj.location)}\n");
1155
+ return textResponse(JSON.stringify(res, null, 2));
1156
+ }
1157
+ catch (err) {
1158
+ return errorResponse(`Create empty failed: ${err.message}`);
1159
+ }
1160
+ }
1161
+ export async function blenderCreateText(registry, input) {
1162
+ try {
1163
+ const client = getBlenderClient(input.workspaceId);
1164
+ const size = input.size ?? 1;
1165
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1166
+ const res = await client.sendExecute("import bpy\n" +
1167
+ `bpy.ops.object.text_add(location=${loc})\n` +
1168
+ "obj = bpy.context.object\n" +
1169
+ `obj.data.body = ${JSON.stringify(input.content ?? "Text")}\n` +
1170
+ `obj.data.size = ${size}\n` +
1171
+ `if ${JSON.stringify(input.name ?? "")}:\n obj.name = ${JSON.stringify(input.name ?? "")}\n` +
1172
+ "result = {'name': obj.name, 'type': obj.type, 'content': obj.data.body, 'location': list(obj.location)}\n");
1173
+ return textResponse(JSON.stringify(res, null, 2));
1174
+ }
1175
+ catch (err) {
1176
+ return errorResponse(`Create text failed: ${err.message}`);
1177
+ }
1178
+ }
1179
+ export async function blenderCreateCurve(registry, input) {
1180
+ try {
1181
+ const client = getBlenderClient(input.workspaceId);
1182
+ const curveType = input.curveType ?? "BEZIER";
1183
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1184
+ const res = await client.sendExecute("import bpy\n" +
1185
+ `bpy.ops.curve.primitive_bezier_curve_add(location=${loc})\n` +
1186
+ "obj = bpy.context.object\n" +
1187
+ `if ${JSON.stringify(input.name ?? "")}:\n obj.name = ${JSON.stringify(input.name ?? "")}\n` +
1188
+ "result = {'name': obj.name, 'type': obj.type, 'curve_type': obj.data.splines[0].type, 'location': list(obj.location)}\n");
1189
+ return textResponse(JSON.stringify(res, null, 2));
1190
+ }
1191
+ catch (err) {
1192
+ return errorResponse(`Create curve failed: ${err.message}`);
1193
+ }
1194
+ }
1195
+ export async function blenderCreateArmature(registry, input) {
1196
+ try {
1197
+ const client = getBlenderClient(input.workspaceId);
1198
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1199
+ const res = await client.sendExecute("import bpy\n" +
1200
+ `bpy.ops.object.armature_add(location=${loc})\n` +
1201
+ "obj = bpy.context.object\n" +
1202
+ `if ${JSON.stringify(input.name ?? "")}:\n obj.name = ${JSON.stringify(input.name ?? "")}\n` +
1203
+ "result = {'name': obj.name, 'type': obj.type, 'bone_count': len(obj.data.bones), 'location': list(obj.location)}\n");
1204
+ return textResponse(JSON.stringify(res, null, 2));
1205
+ }
1206
+ catch (err) {
1207
+ return errorResponse(`Create armature failed: ${err.message}`);
1208
+ }
1209
+ }
1210
+ export async function blenderCreateMetaball(registry, input) {
1211
+ const radius = input.radius ?? 1;
1212
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : "(0, 0, 0)";
1213
+ return createPrimitive(registry, input.workspaceId, "metaball_primitive_add", `type='BALL'`, `radius=${radius}, location=${loc}`, "{'name': obj.name, 'type': obj.type, 'location': list(obj.location)}");
1214
+ }
1215
+ // ─── Object Operations ──────────────────────────────────────────────────────
1216
+ export async function blenderRenameObject(registry, input) {
1217
+ try {
1218
+ const client = getBlenderClient(input.workspaceId);
1219
+ const res = await client.sendExecute("import bpy\n" +
1220
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
1221
+ `new_name = ${JSON.stringify(input.newName)}\n` +
1222
+ "obj = bpy.data.objects.get(object_name)\n" +
1223
+ "if obj is None:\n" +
1224
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
1225
+ "obj.name = new_name\n" +
1226
+ "result = {'old_name': object_name, 'new_name': obj.name}\n");
1227
+ return textResponse(JSON.stringify(res, null, 2));
1228
+ }
1229
+ catch (err) {
1230
+ return errorResponse(`Rename object failed: ${err.message}`);
1231
+ }
1232
+ }
1233
+ export async function blenderDuplicateObject(registry, input) {
1234
+ try {
1235
+ const client = getBlenderClient(input.workspaceId);
1236
+ const duplicateType = input.duplicateType || "LINKED";
1237
+ const offset = input.offset ? JSON.stringify(input.offset) : "[0, 0, 0]";
1238
+ const res = await client.sendExecute("import bpy\n" +
1239
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
1240
+ `duplicate_type = ${JSON.stringify(duplicateType)}.upper()\n` +
1241
+ `offset = ${offset}\n` +
1242
+ "if duplicate_type not in {'LINKED', 'NORMAL', 'INSTANCE'}:\n" +
1243
+ " raise RuntimeError('Unsupported duplicate type: ' + duplicate_type)\n" +
1244
+ "if bpy.ops.object.mode_set.poll():\n" +
1245
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1246
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1247
+ "created = []\n" +
1248
+ "for name in object_names:\n" +
1249
+ " obj = bpy.data.objects.get(name)\n" +
1250
+ " if obj is None:\n" +
1251
+ " continue\n" +
1252
+ " obj.select_set(True)\n" +
1253
+ " bpy.context.view_layer.objects.active = obj\n" +
1254
+ " if duplicate_type == 'LINKED':\n" +
1255
+ " bpy.ops.object.duplicate_move_linked()\n" +
1256
+ " else:\n" +
1257
+ " bpy.ops.object.duplicate_move()\n" +
1258
+ " dup = bpy.context.object\n" +
1259
+ " dup.location.x += float(offset[0]); dup.location.y += float(offset[1]); dup.location.z += float(offset[2])\n" +
1260
+ " created.append(dup.name)\n" +
1261
+ " obj.select_set(False)\n" +
1262
+ "result = {'created': created, 'count': len(created)}\n");
1263
+ return textResponse(JSON.stringify(res, null, 2));
1264
+ }
1265
+ catch (err) {
1266
+ return errorResponse(`Duplicate object failed: ${err.message}`);
1267
+ }
1268
+ }
1269
+ export async function blenderParentObject(registry, input) {
1270
+ try {
1271
+ const client = getBlenderClient(input.workspaceId);
1272
+ const keepTransform = input.keepTransform ?? false;
1273
+ const res = await client.sendExecute("import bpy\n" +
1274
+ `child_names = ${JSON.stringify(input.childNames)}\n` +
1275
+ `parent_name = ${JSON.stringify(input.parentName)}\n` +
1276
+ `keep_transform = ${keepTransform ? "True" : "False"}\n` +
1277
+ "parent = bpy.data.objects.get(parent_name)\n" +
1278
+ "if parent is None:\n" +
1279
+ " raise RuntimeError('Parent object not found: ' + parent_name)\n" +
1280
+ "if bpy.ops.object.mode_set.poll():\n" +
1281
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1282
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1283
+ "children = [bpy.data.objects.get(n) for n in child_names]\n" +
1284
+ "children = [c for c in children if c]\n" +
1285
+ "for child in children:\n" +
1286
+ " child.select_set(True)\n" +
1287
+ "parent.select_set(True)\n" +
1288
+ "bpy.context.view_layer.objects.active = parent\n" +
1289
+ "bpy.ops.object.parent_set(type='OBJECT', keep_transform=keep_transform)\n" +
1290
+ "result = {'parent': parent.name, 'children': [c.name for c in children], 'count': len(children)}\n");
1291
+ return textResponse(JSON.stringify(res, null, 2));
1292
+ }
1293
+ catch (err) {
1294
+ return errorResponse(`Parent object failed: ${err.message}`);
1295
+ }
1296
+ }
1297
+ export async function blenderUnparentObject(registry, input) {
1298
+ try {
1299
+ const client = getBlenderClient(input.workspaceId);
1300
+ const keepTransform = input.keepTransform ?? true;
1301
+ const res = await client.sendExecute("import bpy\n" +
1302
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
1303
+ `keep_transform = ${keepTransform ? "True" : "False"}\n` +
1304
+ "if bpy.ops.object.mode_set.poll():\n" +
1305
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1306
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1307
+ "objects = [bpy.data.objects.get(n) for n in object_names]\n" +
1308
+ "objects = [o for o in objects if o and o.parent]\n" +
1309
+ "for obj in objects:\n" +
1310
+ " obj.select_set(True)\n" +
1311
+ "if objects:\n" +
1312
+ " bpy.context.view_layer.objects.active = objects[0]\n" +
1313
+ " bpy.ops.object.parent_clear(type='CLEAR' if keep_transform else 'CLEAR_KEEP_TRANSFORM')\n" +
1314
+ "result = {'unparented': [obj.name for obj in objects], 'count': len(objects)}\n");
1315
+ return textResponse(JSON.stringify(res, null, 2));
1316
+ }
1317
+ catch (err) {
1318
+ return errorResponse(`Unparent object failed: ${err.message}`);
1319
+ }
1320
+ }
1321
+ export async function blenderHideObjects(registry, input) {
1322
+ try {
1323
+ const client = getBlenderClient(input.workspaceId);
1324
+ const hide = input.hide ?? true;
1325
+ const res = await client.sendExecute("import bpy\n" +
1326
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
1327
+ `hide = ${hide ? "True" : "False"}\n` +
1328
+ "changed = []\n" +
1329
+ "for name in object_names:\n" +
1330
+ " obj = bpy.data.objects.get(name)\n" +
1331
+ " if obj is None:\n" +
1332
+ " continue\n" +
1333
+ " obj.hide_set(hide)\n" +
1334
+ " changed.append(obj.name)\n" +
1335
+ "result = {'hidden': changed, 'count': len(changed), 'hide': hide}\n");
1336
+ return textResponse(JSON.stringify(res, null, 2));
1337
+ }
1338
+ catch (err) {
1339
+ return errorResponse(`Hide objects failed: ${err.message}`);
1340
+ }
1341
+ }
1342
+ export async function blenderShowAllObjects(registry, input) {
1343
+ try {
1344
+ const client = getBlenderClient(input.workspaceId);
1345
+ const res = await client.sendExecute("import bpy\n" +
1346
+ "for obj in bpy.data.objects:\n" +
1347
+ " obj.hide_set(False)\n" +
1348
+ " obj.hide_render = False\n" +
1349
+ "result = {'shown': len(bpy.data.objects)}\n");
1350
+ return textResponse(JSON.stringify(res, null, 2));
1351
+ }
1352
+ catch (err) {
1353
+ return errorResponse(`Show all objects failed: ${err.message}`);
1354
+ }
1355
+ }
1356
+ export async function blenderSetOrigin(registry, input) {
1357
+ try {
1358
+ const client = getBlenderClient(input.workspaceId);
1359
+ const origin = input.origin || "geometry";
1360
+ const loc = input.location ? `(${input.location.map((v) => v ?? 0).join(", ")})` : null;
1361
+ const res = await client.sendExecute("import bpy\n" +
1362
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
1363
+ `origin = ${JSON.stringify(origin)}.lower()\n` +
1364
+ `loc = ${loc}\n` +
1365
+ "if bpy.ops.object.mode_set.poll():\n" +
1366
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1367
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1368
+ "objects = [bpy.data.objects.get(n) for n in object_names]\n" +
1369
+ "objects = [o for o in objects if o]\n" +
1370
+ "for obj in objects:\n" +
1371
+ " obj.select_set(True)\n" +
1372
+ "if objects:\n" +
1373
+ " bpy.context.view_layer.objects.active = objects[0]\n" +
1374
+ " if origin == 'cursor':\n" +
1375
+ " bpy.ops.object.origin_set(type='ORIGIN_CURSOR')\n" +
1376
+ " elif origin == 'center':\n" +
1377
+ " bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS')\n" +
1378
+ " elif loc is not None:\n" +
1379
+ " for obj in objects:\n" +
1380
+ " obj.select_set(False)\n" +
1381
+ " bpy.context.scene.cursor.location = loc\n" +
1382
+ " bpy.ops.object.select_all(action='DESELECT')\n" +
1383
+ " for obj in objects:\n" +
1384
+ " obj.select_set(True)\n" +
1385
+ " bpy.ops.object.origin_set(type='ORIGIN_CURSOR')\n" +
1386
+ " else:\n" +
1387
+ " bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')\n" +
1388
+ "result = {'objects': [obj.name for obj in objects], 'origin': origin, 'count': len(objects)}\n");
1389
+ return textResponse(JSON.stringify(res, null, 2));
1390
+ }
1391
+ catch (err) {
1392
+ return errorResponse(`Set origin failed: ${err.message}`);
1393
+ }
1394
+ }
1395
+ export async function blenderApplyTransforms(registry, input) {
1396
+ try {
1397
+ const client = getBlenderClient(input.workspaceId);
1398
+ const location = input.location ?? true;
1399
+ const rotation = input.rotation ?? true;
1400
+ const scale = input.scale ?? true;
1401
+ const res = await client.sendExecute("import bpy\n" +
1402
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
1403
+ `apply_loc = ${location ? "True" : "False"}\n` +
1404
+ `apply_rot = ${rotation ? "True" : "False"}\n` +
1405
+ `apply_scale = ${scale ? "True" : "False"}\n` +
1406
+ "if bpy.ops.object.mode_set.poll():\n" +
1407
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1408
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1409
+ "objects = [bpy.data.objects.get(n) for n in object_names]\n" +
1410
+ "objects = [o for o in objects if o]\n" +
1411
+ "for obj in objects:\n" +
1412
+ " obj.select_set(True)\n" +
1413
+ "if objects:\n" +
1414
+ " bpy.context.view_layer.objects.active = objects[0]\n" +
1415
+ " bpy.ops.object.transform_apply(location=apply_loc, rotation=apply_rot, scale=apply_scale)\n" +
1416
+ "result = {'applied': [obj.name for obj in objects], 'count': len(objects)}\n");
1417
+ return textResponse(JSON.stringify(res, null, 2));
1418
+ }
1419
+ catch (err) {
1420
+ return errorResponse(`Apply transforms failed: ${err.message}`);
1421
+ }
1422
+ }
1423
+ export async function blenderClearTransforms(registry, input) {
1424
+ try {
1425
+ const client = getBlenderClient(input.workspaceId);
1426
+ const location = input.location ?? true;
1427
+ const rotation = input.rotation ?? true;
1428
+ const scale = input.scale ?? false;
1429
+ const res = await client.sendExecute("import bpy\n" +
1430
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
1431
+ `clear_loc = ${location ? "True" : "False"}\n` +
1432
+ `clear_rot = ${rotation ? "True" : "False"}\n` +
1433
+ `clear_scale = ${scale ? "True" : "False"}\n` +
1434
+ "if bpy.ops.object.mode_set.poll():\n" +
1435
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1436
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1437
+ "objects = [bpy.data.objects.get(n) for n in object_names]\n" +
1438
+ "objects = [o for o in objects if o]\n" +
1439
+ "for obj in objects:\n" +
1440
+ " obj.select_set(True)\n" +
1441
+ "if objects:\n" +
1442
+ " bpy.context.view_layer.objects.active = objects[0]\n" +
1443
+ " bpy.ops.object.location_clear(clear_delta=clear_loc)\n" +
1444
+ " bpy.ops.object.rotation_clear(clear_delta=clear_rot)\n" +
1445
+ " bpy.ops.object.scale_clear(clear_delta=clear_scale)\n" +
1446
+ "result = {'cleared': [obj.name for obj in objects], 'count': len(objects)}\n");
1447
+ return textResponse(JSON.stringify(res, null, 2));
1448
+ }
1449
+ catch (err) {
1450
+ return errorResponse(`Clear transforms failed: ${err.message}`);
1451
+ }
1452
+ }
1453
+ export async function blenderMirrorObjects(registry, input) {
1454
+ try {
1455
+ const client = getBlenderClient(input.workspaceId);
1456
+ const axes = input.axis ?? ["X"];
1457
+ const axisFlags = axes.map((a) => a.toUpperCase()).filter((a) => ["X", "Y", "Z"].includes(a));
1458
+ if (axisFlags.length === 0)
1459
+ axisFlags.push("X");
1460
+ const res = await client.sendExecute("import bpy\n" +
1461
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
1462
+ `axes = ${JSON.stringify(axisFlags)}\n` +
1463
+ "if bpy.ops.object.mode_set.poll():\n" +
1464
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1465
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1466
+ "objects = [bpy.data.objects.get(n) for n in object_names]\n" +
1467
+ "objects = [o for o in objects if o]\n" +
1468
+ "for obj in objects:\n" +
1469
+ " obj.select_set(True)\n" +
1470
+ "if objects:\n" +
1471
+ " bpy.context.view_layer.objects.active = objects[0]\n" +
1472
+ " use_x = 'X' in axes\n" +
1473
+ " use_y = 'Y' in axes\n" +
1474
+ " use_z = 'Z' in axes\n" +
1475
+ " bpy.ops.transform.mirror(orient_type='GLOBAL', constraint_axis=(use_x, use_y, use_z))\n" +
1476
+ "result = {'mirrored': [obj.name for obj in objects], 'axes': axes, 'count': len(objects)}\n");
1477
+ return textResponse(JSON.stringify(res, null, 2));
1478
+ }
1479
+ catch (err) {
1480
+ return errorResponse(`Mirror objects failed: ${err.message}`);
1481
+ }
1482
+ }
1483
+ export async function blenderSetCursorLocation(registry, input) {
1484
+ try {
1485
+ const client = getBlenderClient(input.workspaceId);
1486
+ const loc = `(${input.location.slice(0, 3).map((v) => v ?? 0).join(", ")})`;
1487
+ const res = await client.sendExecute("import bpy\n" +
1488
+ `bpy.context.scene.cursor.location = ${loc}\n` +
1489
+ "result = {'cursor': [round(v, 4) for v in bpy.context.scene.cursor.location]}\n");
1490
+ return textResponse(JSON.stringify(res, null, 2));
1491
+ }
1492
+ catch (err) {
1493
+ return errorResponse(`Set cursor location failed: ${err.message}`);
1494
+ }
1495
+ }
1496
+ // ─── Selection ──────────────────────────────────────────────────────────────
1497
+ export async function blenderSelectByType(registry, input) {
1498
+ try {
1499
+ const client = getBlenderClient(input.workspaceId);
1500
+ const res = await client.sendExecute("import bpy\n" +
1501
+ `object_type = ${JSON.stringify(input.objectType)}.upper()\n` +
1502
+ "if bpy.ops.object.mode_set.poll():\n" +
1503
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1504
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1505
+ "matches = [obj for obj in bpy.context.scene.objects if obj.type == object_type]\n" +
1506
+ "for obj in matches:\n" +
1507
+ " obj.select_set(True)\n" +
1508
+ "result = {'selected': [obj.name for obj in matches], 'type': object_type, 'count': len(matches)}\n");
1509
+ return textResponse(JSON.stringify(res, null, 2));
1510
+ }
1511
+ catch (err) {
1512
+ return errorResponse(`Select by type failed: ${err.message}`);
1513
+ }
1514
+ }
1515
+ export async function blenderSelectLinked(registry, input) {
1516
+ try {
1517
+ const client = getBlenderClient(input.workspaceId);
1518
+ const res = await client.sendExecute("import bpy\n" +
1519
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
1520
+ "obj = bpy.data.objects.get(object_name)\n" +
1521
+ "if obj is None:\n" +
1522
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
1523
+ "if bpy.ops.object.mode_set.poll():\n" +
1524
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1525
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1526
+ "obj.select_set(True)\n" +
1527
+ "bpy.context.view_layer.objects.active = obj\n" +
1528
+ "bpy.ops.object.select_linked(type='OBDATA')\n" +
1529
+ "result = {'selected': [o.name for o in bpy.context.selected_objects], 'count': len(bpy.context.selected_objects)}\n");
1530
+ return textResponse(JSON.stringify(res, null, 2));
1531
+ }
1532
+ catch (err) {
1533
+ return errorResponse(`Select linked failed: ${err.message}`);
1534
+ }
1535
+ }
1536
+ export async function blenderSelectByCollection(registry, input) {
1537
+ try {
1538
+ const client = getBlenderClient(input.workspaceId);
1539
+ const res = await client.sendExecute("import bpy\n" +
1540
+ `collection_name = ${JSON.stringify(input.collectionName)}\n` +
1541
+ "coll = bpy.data.collections.get(collection_name)\n" +
1542
+ "if coll is None:\n" +
1543
+ " raise RuntimeError('Collection not found: ' + collection_name)\n" +
1544
+ "if bpy.ops.object.mode_set.poll():\n" +
1545
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1546
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1547
+ "for obj in coll.objects:\n" +
1548
+ " obj.select_set(True)\n" +
1549
+ "result = {'selected': [obj.name for obj in coll.objects], 'collection': coll.name, 'count': len(coll.objects)}\n");
1550
+ return textResponse(JSON.stringify(res, null, 2));
1551
+ }
1552
+ catch (err) {
1553
+ return errorResponse(`Select by collection failed: ${err.message}`);
1554
+ }
1555
+ }
1556
+ export async function blenderSelectByMaterial(registry, input) {
1557
+ try {
1558
+ const client = getBlenderClient(input.workspaceId);
1559
+ const res = await client.sendExecute("import bpy\n" +
1560
+ `material_name = ${JSON.stringify(input.materialName)}\n` +
1561
+ "if bpy.ops.object.mode_set.poll():\n" +
1562
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1563
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1564
+ "matches = []\n" +
1565
+ "for obj in bpy.context.scene.objects:\n" +
1566
+ " names = [slot.material.name for slot in obj.material_slots if slot.material]\n" +
1567
+ " if material_name in names:\n" +
1568
+ " obj.select_set(True)\n" +
1569
+ " matches.append(obj.name)\n" +
1570
+ "result = {'selected': matches, 'material': material_name, 'count': len(matches)}\n");
1571
+ return textResponse(JSON.stringify(res, null, 2));
1572
+ }
1573
+ catch (err) {
1574
+ return errorResponse(`Select by material failed: ${err.message}`);
1575
+ }
1576
+ }
1577
+ export async function blenderSelectByPolycount(registry, input) {
1578
+ try {
1579
+ const client = getBlenderClient(input.workspaceId);
1580
+ const maxTriangles = input.maxTriangles ?? Number.POSITIVE_INFINITY;
1581
+ const minTriangles = input.minTriangles ?? 0;
1582
+ const res = await client.sendExecute("import bpy\n" +
1583
+ `max_tris = ${Number.isFinite(maxTriangles) ? maxTriangles : "float('inf')"}\n` +
1584
+ `min_tris = ${minTriangles}\n` +
1585
+ "if bpy.ops.object.mode_set.poll():\n" +
1586
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1587
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1588
+ "matches = []\n" +
1589
+ "for obj in bpy.context.scene.objects:\n" +
1590
+ " if obj.type != 'MESH' or not obj.data:\n" +
1591
+ " continue\n" +
1592
+ " tris = sum(max(len(p.vertices) - 2, 1) for p in obj.data.polygons)\n" +
1593
+ " if min_tris <= tris <= max_tris:\n" +
1594
+ " obj.select_set(True)\n" +
1595
+ " matches.append({'name': obj.name, 'triangles': tris})\n" +
1596
+ "matches.sort(key=lambda m: m['triangles'], reverse=True)\n" +
1597
+ "result = {'selected': [m['name'] for m in matches], 'count': len(matches), 'details': matches[:50]}\n");
1598
+ return textResponse(JSON.stringify(res, null, 2));
1599
+ }
1600
+ catch (err) {
1601
+ return errorResponse(`Select by polycount failed: ${err.message}`);
1602
+ }
1603
+ }
1604
+ export async function blenderInvertSelection(registry, input) {
1605
+ try {
1606
+ const client = getBlenderClient(input.workspaceId);
1607
+ const res = await client.sendExecute("import bpy\n" +
1608
+ "bpy.ops.object.select_all(action='INVERT')\n" +
1609
+ "result = {'selected': [o.name for o in bpy.context.selected_objects], 'count': len(bpy.context.selected_objects)}\n");
1610
+ return textResponse(JSON.stringify(res, null, 2));
1611
+ }
1612
+ catch (err) {
1613
+ return errorResponse(`Invert selection failed: ${err.message}`);
1614
+ }
1615
+ }
1616
+ export async function blenderDeselectAll(registry, input) {
1617
+ try {
1618
+ const client = getBlenderClient(input.workspaceId);
1619
+ const res = await client.sendExecute("import bpy\n" +
1620
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
1621
+ "result = {'selected': [], 'count': 0}\n");
1622
+ return textResponse(JSON.stringify(res, null, 2));
1623
+ }
1624
+ catch (err) {
1625
+ return errorResponse(`Deselect all failed: ${err.message}`);
1626
+ }
1627
+ }
1628
+ export async function blenderSelectAll(registry, input) {
1629
+ try {
1630
+ const client = getBlenderClient(input.workspaceId);
1631
+ const res = await client.sendExecute("import bpy\n" +
1632
+ "bpy.ops.object.select_all(action='SELECT')\n" +
1633
+ "result = {'selected': [o.name for o in bpy.context.selected_objects], 'count': len(bpy.context.selected_objects)}\n");
1634
+ return textResponse(JSON.stringify(res, null, 2));
1635
+ }
1636
+ catch (err) {
1637
+ return errorResponse(`Select all failed: ${err.message}`);
1638
+ }
1639
+ }
1640
+ // ─── Mesh Edit Mode ─────────────────────────────────────────────────────────
1641
+ export async function blenderEnterEditMode(registry, input) {
1642
+ try {
1643
+ const client = getBlenderClient(input.workspaceId);
1644
+ const res = await client.sendExecute("import bpy\n" +
1645
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
1646
+ "obj = bpy.data.objects.get(object_name)\n" +
1647
+ "if obj is None:\n" +
1648
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
1649
+ "bpy.context.view_layer.objects.active = obj\n" +
1650
+ "obj.select_set(True)\n" +
1651
+ "bpy.ops.object.mode_set(mode='EDIT')\n" +
1652
+ "result = {'object': obj.name, 'mode': bpy.context.mode}\n");
1653
+ return textResponse(JSON.stringify(res, null, 2));
1654
+ }
1655
+ catch (err) {
1656
+ return errorResponse(`Enter edit mode failed: ${err.message}`);
1657
+ }
1658
+ }
1659
+ export async function blenderExitEditMode(registry, input) {
1660
+ try {
1661
+ const client = getBlenderClient(input.workspaceId);
1662
+ const res = await client.sendExecute("import bpy\n" +
1663
+ "if bpy.ops.object.mode_set.poll():\n" +
1664
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1665
+ "result = {'mode': bpy.context.mode}\n");
1666
+ return textResponse(JSON.stringify(res, null, 2));
1667
+ }
1668
+ catch (err) {
1669
+ return errorResponse(`Exit edit mode failed: ${err.message}`);
1670
+ }
1671
+ }
1672
+ export async function blenderSetMeshSelectionMode(registry, input) {
1673
+ try {
1674
+ const client = getBlenderClient(input.workspaceId);
1675
+ const mode = input.mode || "VERT";
1676
+ const res = await client.sendExecute("import bpy\n" +
1677
+ `mode = ${JSON.stringify(mode)}.upper()\n` +
1678
+ "if mode not in {'VERT', 'EDGE', 'FACE'}:\n" +
1679
+ " raise RuntimeError('Unsupported selection mode: ' + mode)\n" +
1680
+ "mesh = bpy.context.object.data if bpy.context.object else None\n" +
1681
+ "if mesh is None or not hasattr(mesh, 'use_mesh_faces'):\n" +
1682
+ " raise RuntimeError('Active object is not an editable mesh')\n" +
1683
+ "bpy.ops.object.mode_set(mode='EDIT')\n" +
1684
+ "mesh = bpy.context.object.data\n" +
1685
+ "mesh.select_mode = {'VERT' if mode == 'VERT' else 'EDGE' if mode == 'EDGE' else 'FACE'}\n" +
1686
+ "result = {'mode': mode}\n");
1687
+ return textResponse(JSON.stringify(res, null, 2));
1688
+ }
1689
+ catch (err) {
1690
+ return errorResponse(`Set mesh selection mode failed: ${err.message}`);
1691
+ }
1692
+ }
1693
+ export async function blenderExtrudeSelection(registry, input) {
1694
+ try {
1695
+ const client = getBlenderClient(input.workspaceId);
1696
+ const amount = input.amount ?? [0, 0, 1];
1697
+ const res = await client.sendExecute("import bpy, bmesh\n" +
1698
+ "from mathutils import Vector\n" +
1699
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
1700
+ "obj = bpy.data.objects.get(object_name)\n" +
1701
+ "if obj is None or obj.type != 'MESH':\n" +
1702
+ " raise RuntimeError('Object not found or not a mesh: ' + object_name)\n" +
1703
+ "if bpy.ops.object.mode_set.poll():\n" +
1704
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
1705
+ "bpy.context.view_layer.objects.active = obj\n" +
1706
+ "bpy.ops.object.mode_set(mode='EDIT')\n" +
1707
+ "bm = bmesh.from_edit_mesh(obj.data)\n" +
1708
+ "selected = [v for v in bm.verts if v.select]\n" +
1709
+ "selected_faces = [f for f in bm.faces if f.select]\n" +
1710
+ "if not selected:\n" +
1711
+ " raise RuntimeError('Nothing selected to extrude')\n" +
1712
+ `geom = bmesh.ops.extrude_face_region(bm, geom=selected + selected_faces)\n` +
1713
+ `vec = Vector(${JSON.stringify(amount)})\n` +
1714
+ "inv_mat = obj.matrix_world.inverted().to_3x3()\n" +
1715
+ "for v in geom['geom']:\n" +
1716
+ " if isinstance(v, bmesh.types.BMVert):\n" +
1717
+ " v.co += inv_mat @ vec\n" +
1718
+ "bmesh.update_edit_mesh(obj.data)\n" +
1719
+ "result = {'object': obj.name, 'extruded_verts': len(selected)}\n");
1720
+ return textResponse(JSON.stringify(res, null, 2));
1721
+ }
1722
+ catch (err) {
1723
+ return errorResponse(`Extrude selection failed: ${err.message}`);
1724
+ }
1725
+ }
1726
+ export async function blenderBevelSelection(registry, input) {
1727
+ try {
1728
+ const client = getBlenderClient(input.workspaceId);
1729
+ const amount = input.amount ?? 0.05;
1730
+ const segments = input.segments ?? 1;
1731
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1732
+ `bmesh.ops.bevel(bm, geom=[v for v in bm.verts if v.select] + [e for e in bm.edges if e.select] + [f for f in bm.faces if f.select], offset=${amount}, segments=${segments}, affect='VERTICES')\n` +
1733
+ "bmesh.update_edit_mesh(obj.data)\n" +
1734
+ "result = {'object': obj.name, 'amount': " + JSON.stringify(amount) + ", 'segments': " + JSON.stringify(segments) + "}\n");
1735
+ return textResponse(JSON.stringify(res, null, 2));
1736
+ }
1737
+ catch (err) {
1738
+ return errorResponse(`Bevel selection failed: ${err.message}`);
1739
+ }
1740
+ }
1741
+ export async function blenderLoopCut(registry, input) {
1742
+ try {
1743
+ const client = getBlenderClient(input.workspaceId);
1744
+ const cuts = Math.max(1, input.cuts ?? 1);
1745
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1746
+ `bpy.ops.mesh.loopcut_slide(number_cuts=${cuts})\n` +
1747
+ "result = {'object': obj.name, 'cuts': " + JSON.stringify(cuts) + "}\n");
1748
+ return textResponse(JSON.stringify(res, null, 2));
1749
+ }
1750
+ catch (err) {
1751
+ return errorResponse(`Loop cut failed: ${err.message}`);
1752
+ }
1753
+ }
1754
+ export async function blenderSubdivide(registry, input) {
1755
+ try {
1756
+ const client = getBlenderClient(input.workspaceId);
1757
+ const cuts = Math.max(1, input.cuts ?? 1);
1758
+ const smooth = input.smooth ?? 0;
1759
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1760
+ "bpy.ops.mesh.subdivide(number_cuts=" + JSON.stringify(cuts) + ", smooth=" + JSON.stringify(smooth) + ")\n" +
1761
+ "result = {'object': obj.name, 'cuts': " + JSON.stringify(cuts) + "}\n");
1762
+ return textResponse(JSON.stringify(res, null, 2));
1763
+ }
1764
+ catch (err) {
1765
+ return errorResponse(`Subdivide failed: ${err.message}`);
1766
+ }
1767
+ }
1768
+ export async function blenderMergeByDistance(registry, input) {
1769
+ try {
1770
+ const client = getBlenderClient(input.workspaceId);
1771
+ const distance = input.distance ?? 0.001;
1772
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1773
+ "bpy.ops.mesh.select_all(action='SELECT')\n" +
1774
+ "bpy.ops.mesh.remove_doubles(threshold=" + JSON.stringify(distance) + ")\n" +
1775
+ "result = {'object': obj.name, 'distance': " + JSON.stringify(distance) + "}\n");
1776
+ return textResponse(JSON.stringify(res, null, 2));
1777
+ }
1778
+ catch (err) {
1779
+ return errorResponse(`Merge by distance failed: ${err.message}`);
1780
+ }
1781
+ }
1782
+ export async function blenderRecalculateNormals(registry, input) {
1783
+ try {
1784
+ const client = getBlenderClient(input.workspaceId);
1785
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1786
+ "bpy.ops.mesh.select_all(action='SELECT')\n" +
1787
+ "bpy.ops.mesh.normals_make_consistent(inside=False)\n" +
1788
+ "bmesh.update_edit_mesh(obj.data)\n" +
1789
+ "result = {'object': obj.name, 'normals': 'recalculated'}\n");
1790
+ return textResponse(JSON.stringify(res, null, 2));
1791
+ }
1792
+ catch (err) {
1793
+ return errorResponse(`Recalculate normals failed: ${err.message}`);
1794
+ }
1795
+ }
1796
+ export async function blenderFlipNormals(registry, input) {
1797
+ try {
1798
+ const client = getBlenderClient(input.workspaceId);
1799
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1800
+ "bpy.ops.mesh.flip_normals()\n" +
1801
+ "bmesh.update_edit_mesh(obj.data)\n" +
1802
+ "result = {'object': obj.name, 'normals': 'flipped'}\n");
1803
+ return textResponse(JSON.stringify(res, null, 2));
1804
+ }
1805
+ catch (err) {
1806
+ return errorResponse(`Flip normals failed: ${err.message}`);
1807
+ }
1808
+ }
1809
+ export async function blenderTriangulateFaces(registry, input) {
1810
+ try {
1811
+ const client = getBlenderClient(input.workspaceId);
1812
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1813
+ "bpy.ops.mesh.select_all(action='SELECT')\n" +
1814
+ "bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')\n" +
1815
+ "bmesh.update_edit_mesh(obj.data)\n" +
1816
+ "result = {'object': obj.name, 'faces': 'triangulated'}\n");
1817
+ return textResponse(JSON.stringify(res, null, 2));
1818
+ }
1819
+ catch (err) {
1820
+ return errorResponse(`Triangulate faces failed: ${err.message}`);
1821
+ }
1822
+ }
1823
+ export async function blenderInsetFaces(registry, input) {
1824
+ try {
1825
+ const client = getBlenderClient(input.workspaceId);
1826
+ const thickness = input.thickness ?? 0.05;
1827
+ const depth = input.depth ?? 0;
1828
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1829
+ "bpy.ops.mesh.inset(thickness=" + JSON.stringify(thickness) + ", depth=" + JSON.stringify(depth) + ", use_individual=True)\n" +
1830
+ "bmesh.update_edit_mesh(obj.data)\n" +
1831
+ "result = {'object': obj.name, 'thickness': " + JSON.stringify(thickness) + "}\n");
1832
+ return textResponse(JSON.stringify(res, null, 2));
1833
+ }
1834
+ catch (err) {
1835
+ return errorResponse(`Inset faces failed: ${err.message}`);
1836
+ }
1837
+ }
1838
+ export async function blenderDeleteLooseGeometry(registry, input) {
1839
+ try {
1840
+ const client = getBlenderClient(input.workspaceId);
1841
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1842
+ "bpy.ops.mesh.select_all(action='SELECT')\n" +
1843
+ "bpy.ops.mesh.delete_loose()\n" +
1844
+ "bmesh.update_edit_mesh(obj.data)\n" +
1845
+ "result = {'object': obj.name, 'loose': 'deleted'}\n");
1846
+ return textResponse(JSON.stringify(res, null, 2));
1847
+ }
1848
+ catch (err) {
1849
+ return errorResponse(`Delete loose geometry failed: ${err.message}`);
1850
+ }
1851
+ }
1852
+ export async function blenderFixNonManifold(registry, input) {
1853
+ try {
1854
+ const client = getBlenderClient(input.workspaceId);
1855
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1856
+ "bpy.ops.mesh.select_all(action='SELECT')\n" +
1857
+ "bpy.ops.mesh.select_non_manifold()\n" +
1858
+ "selected = len([v for v in bm.verts if v.select])\n" +
1859
+ "result = {'object': obj.name, 'non_manifold_verts': selected}\n");
1860
+ return textResponse(JSON.stringify(res, null, 2));
1861
+ }
1862
+ catch (err) {
1863
+ return errorResponse(`Fix non-manifold failed: ${err.message}`);
1864
+ }
1865
+ }
1866
+ export async function blenderSetFaceSmoothing(registry, input) {
1867
+ try {
1868
+ const client = getBlenderClient(input.workspaceId);
1869
+ const smooth = input.smooth ?? true;
1870
+ const res = await client.sendExecute(meshEditPrologue(input.objectName) +
1871
+ `for face in bm.faces:\n face.smooth = ${smooth ? "True" : "False"}\n` +
1872
+ "bmesh.update_edit_mesh(obj.data)\n" +
1873
+ "result = {'object': obj.name, 'smooth': " + JSON.stringify(smooth) + "}\n");
1874
+ return textResponse(JSON.stringify(res, null, 2));
1875
+ }
1876
+ catch (err) {
1877
+ return errorResponse(`Set face smoothing failed: ${err.message}`);
1878
+ }
1879
+ }
1880
+ // ─── Modifiers ──────────────────────────────────────────────────────────────
1881
+ export async function blenderAddModifier(registry, input) {
1882
+ try {
1883
+ const client = getBlenderClient(input.workspaceId);
1884
+ const name = input.name ?? input.modifierType;
1885
+ const res = await client.sendExecute("import bpy\n" +
1886
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
1887
+ `modifier_type = ${JSON.stringify(input.modifierType)}\n` +
1888
+ `modifier_name = ${JSON.stringify(name)}\n` +
1889
+ "obj = bpy.data.objects.get(object_name)\n" +
1890
+ "if obj is None:\n" +
1891
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
1892
+ "try:\n" +
1893
+ " mod = obj.modifiers.new(modifier_name, modifier_type)\n" +
1894
+ "except Exception as exc:\n" +
1895
+ " raise RuntimeError('Unsupported modifier type: ' + modifier_type + ' - ' + str(exc))\n" +
1896
+ "result = {'object': obj.name, 'modifier': mod.name, 'type': mod.type}\n");
1897
+ return textResponse(JSON.stringify(res, null, 2));
1898
+ }
1899
+ catch (err) {
1900
+ return errorResponse(`Add modifier failed: ${err.message}`);
1901
+ }
1902
+ }
1903
+ export async function blenderRemoveModifier(registry, input) {
1904
+ try {
1905
+ const client = getBlenderClient(input.workspaceId);
1906
+ const res = await client.sendExecute("import bpy\n" +
1907
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
1908
+ `modifier_name = ${JSON.stringify(input.modifierName)}\n` +
1909
+ "obj = bpy.data.objects.get(object_name)\n" +
1910
+ "if obj is None:\n" +
1911
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
1912
+ "mod = obj.modifiers.get(modifier_name)\n" +
1913
+ "if mod is None:\n" +
1914
+ " raise RuntimeError('Modifier not found: ' + modifier_name)\n" +
1915
+ "obj.modifiers.remove(mod)\n" +
1916
+ "result = {'object': obj.name, 'removed': modifier_name, 'remaining': [m.name for m in obj.modifiers]}\n");
1917
+ return textResponse(JSON.stringify(res, null, 2));
1918
+ }
1919
+ catch (err) {
1920
+ return errorResponse(`Remove modifier failed: ${err.message}`);
1921
+ }
1922
+ }
1923
+ export async function blenderApplyModifier(registry, input) {
1924
+ try {
1925
+ const client = getBlenderClient(input.workspaceId);
1926
+ const res = await client.sendExecute("import bpy\n" +
1927
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
1928
+ `modifier_name = ${JSON.stringify(input.modifierName)}\n` +
1929
+ "obj = bpy.data.objects.get(object_name)\n" +
1930
+ "if obj is None:\n" +
1931
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
1932
+ "mod = obj.modifiers.get(modifier_name)\n" +
1933
+ "if mod is None:\n" +
1934
+ " raise RuntimeError('Modifier not found: ' + modifier_name)\n" +
1935
+ "bpy.context.view_layer.objects.active = obj\n" +
1936
+ "bpy.ops.object.modifier_apply(modifier=modifier_name)\n" +
1937
+ "result = {'object': obj.name, 'applied': modifier_name, 'remaining': [m.name for m in obj.modifiers]}\n");
1938
+ return textResponse(JSON.stringify(res, null, 2));
1939
+ }
1940
+ catch (err) {
1941
+ return errorResponse(`Apply modifier failed: ${err.message}`);
1942
+ }
1943
+ }
1944
+ // ─── Materials ──────────────────────────────────────────────────────────────
1945
+ export async function blenderCreateMaterial(registry, input) {
1946
+ try {
1947
+ const client = getBlenderClient(input.workspaceId);
1948
+ const color = input.color ? `(${input.color.slice(0, 4).map((v) => v ?? 1).join(", ")})` : "(0.8, 0.8, 0.8, 1)";
1949
+ const roughness = input.roughness ?? 0.5;
1950
+ const metallic = input.metallic ?? 0;
1951
+ const res = await client.sendExecute("import bpy\n" +
1952
+ `mat_name = ${JSON.stringify(input.name)}\n` +
1953
+ `color = ${color}\n` +
1954
+ `roughness = ${roughness}\n` +
1955
+ `metallic = ${metallic}\n` +
1956
+ "mat = bpy.data.materials.get(mat_name)\n" +
1957
+ "if mat is None:\n" +
1958
+ " mat = bpy.data.materials.new(mat_name)\n" +
1959
+ "mat.use_nodes = True\n" +
1960
+ "bsdf = mat.node_tree.nodes.get('Principled BSDF')\n" +
1961
+ "if bsdf:\n" +
1962
+ " bsdf.inputs['Base Color'].default_value = color\n" +
1963
+ " bsdf.inputs['Roughness'].default_value = roughness\n" +
1964
+ " bsdf.inputs['Metallic'].default_value = metallic\n" +
1965
+ "result = {'name': mat.name, 'color': list(mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value) if 'Principled BSDF' in mat.node_tree.nodes else None, 'users': mat.users}\n");
1966
+ return textResponse(JSON.stringify(res, null, 2));
1967
+ }
1968
+ catch (err) {
1969
+ return errorResponse(`Create material failed: ${err.message}`);
1970
+ }
1971
+ }
1972
+ export async function blenderAssignMaterial(registry, input) {
1973
+ try {
1974
+ const client = getBlenderClient(input.workspaceId);
1975
+ const res = await client.sendExecute("import bpy\n" +
1976
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
1977
+ `material_name = ${JSON.stringify(input.materialName)}\n` +
1978
+ "mat = bpy.data.materials.get(material_name)\n" +
1979
+ "if mat is None:\n" +
1980
+ " raise RuntimeError('Material not found: ' + material_name)\n" +
1981
+ "assigned = []\n" +
1982
+ "for name in object_names:\n" +
1983
+ " obj = bpy.data.objects.get(name)\n" +
1984
+ " if obj is None or obj.type != 'MESH':\n" +
1985
+ " continue\n" +
1986
+ " if not obj.data.materials:\n" +
1987
+ " obj.data.materials.append(mat)\n" +
1988
+ " else:\n" +
1989
+ " obj.data.materials[0] = mat\n" +
1990
+ " assigned.append(obj.name)\n" +
1991
+ "result = {'assigned': assigned, 'material': mat.name, 'count': len(assigned)}\n");
1992
+ return textResponse(JSON.stringify(res, null, 2));
1993
+ }
1994
+ catch (err) {
1995
+ return errorResponse(`Assign material failed: ${err.message}`);
1996
+ }
1997
+ }
1998
+ export async function blenderListMaterials(registry, input) {
1999
+ try {
2000
+ const client = getBlenderClient(input.workspaceId);
2001
+ const res = await client.sendExecute("import bpy\n" +
2002
+ "result = {'materials': [{'name': m.name, 'users': m.users, 'use_nodes': m.use_nodes} for m in bpy.data.materials], 'count': len(bpy.data.materials)}\n");
2003
+ return textResponse(JSON.stringify(res, null, 2));
2004
+ }
2005
+ catch (err) {
2006
+ return errorResponse(`List materials failed: ${err.message}`);
2007
+ }
2008
+ }
2009
+ export async function blenderRenameMaterial(registry, input) {
2010
+ try {
2011
+ const client = getBlenderClient(input.workspaceId);
2012
+ const res = await client.sendExecute("import bpy\n" +
2013
+ `material_name = ${JSON.stringify(input.materialName)}\n` +
2014
+ `new_name = ${JSON.stringify(input.newName)}\n` +
2015
+ "mat = bpy.data.materials.get(material_name)\n" +
2016
+ "if mat is None:\n" +
2017
+ " raise RuntimeError('Material not found: ' + material_name)\n" +
2018
+ "mat.name = new_name\n" +
2019
+ "result = {'old_name': material_name, 'new_name': mat.name}\n");
2020
+ return textResponse(JSON.stringify(res, null, 2));
2021
+ }
2022
+ catch (err) {
2023
+ return errorResponse(`Rename material failed: ${err.message}`);
2024
+ }
2025
+ }
2026
+ export async function blenderDuplicateMaterial(registry, input) {
2027
+ try {
2028
+ const client = getBlenderClient(input.workspaceId);
2029
+ const res = await client.sendExecute("import bpy\n" +
2030
+ `material_name = ${JSON.stringify(input.materialName)}\n` +
2031
+ `new_name = ${JSON.stringify(input.newName ?? "")}\n` +
2032
+ "mat = bpy.data.materials.get(material_name)\n" +
2033
+ "if mat is None:\n" +
2034
+ " raise RuntimeError('Material not found: ' + material_name)\n" +
2035
+ "dup = mat.copy()\n" +
2036
+ "if new_name:\n" +
2037
+ " dup.name = new_name\n" +
2038
+ "result = {'source': mat.name, 'duplicate': dup.name}\n");
2039
+ return textResponse(JSON.stringify(res, null, 2));
2040
+ }
2041
+ catch (err) {
2042
+ return errorResponse(`Duplicate material failed: ${err.message}`);
2043
+ }
2044
+ }
2045
+ export async function blenderRemoveUnusedMaterials(registry, input) {
2046
+ try {
2047
+ const client = getBlenderClient(input.workspaceId);
2048
+ const res = await client.sendExecute("import bpy\n" +
2049
+ "removed = []\n" +
2050
+ "for mat in list(bpy.data.materials):\n" +
2051
+ " if mat.users == 0:\n" +
2052
+ " removed.append(mat.name)\n" +
2053
+ " bpy.data.materials.remove(mat)\n" +
2054
+ "result = {'removed': removed, 'count': len(removed)}\n");
2055
+ return textResponse(JSON.stringify(res, null, 2));
2056
+ }
2057
+ catch (err) {
2058
+ return errorResponse(`Remove unused materials failed: ${err.message}`);
2059
+ }
2060
+ }
2061
+ // ─── Viewport ───────────────────────────────────────────────────────────────
2062
+ export async function blenderFrameAll(registry, input) {
2063
+ try {
2064
+ const client = getBlenderClient(input.workspaceId);
2065
+ const res = await client.sendExecute("import bpy\n" +
2066
+ VIEW3D_HELPER +
2067
+ "window, screen, area, region, space = _view3d_context()\n" +
2068
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
2069
+ " bpy.ops.view3d.view_all(use_all_regions=False)\n" +
2070
+ "result = {'framed': 'all'}\n");
2071
+ return textResponse(JSON.stringify(res, null, 2));
2072
+ }
2073
+ catch (err) {
2074
+ return errorResponse(`Frame all failed: ${err.message}`);
2075
+ }
2076
+ }
2077
+ export async function blenderToggleXray(registry, input) {
2078
+ try {
2079
+ const client = getBlenderClient(input.workspaceId);
2080
+ const enabled = input.enabled;
2081
+ const res = await client.sendExecute("import bpy\n" +
2082
+ VIEW3D_HELPER +
2083
+ "window, screen, area, region, space = _view3d_context()\n" +
2084
+ `if ${enabled === undefined ? "True" : "False"}:\n` +
2085
+ ` space.shading.show_xray = not space.shading.show_xray\n` +
2086
+ "else:\n" +
2087
+ ` space.shading.show_xray = ${enabled ? "True" : "False"}\n` +
2088
+ "result = {'xray': space.shading.show_xray}\n");
2089
+ return textResponse(JSON.stringify(res, null, 2));
2090
+ }
2091
+ catch (err) {
2092
+ return errorResponse(`Toggle X-ray failed: ${err.message}`);
2093
+ }
2094
+ }
2095
+ export async function blenderToggleOverlays(registry, input) {
2096
+ try {
2097
+ const client = getBlenderClient(input.workspaceId);
2098
+ const enabled = input.enabled;
2099
+ const res = await client.sendExecute("import bpy\n" +
2100
+ VIEW3D_HELPER +
2101
+ "window, screen, area, region, space = _view3d_context()\n" +
2102
+ `if ${enabled === undefined ? "True" : "False"}:\n` +
2103
+ ` space.overlay.show_overlays = not space.overlay.show_overlays\n` +
2104
+ "else:\n" +
2105
+ ` space.overlay.show_overlays = ${enabled ? "True" : "False"}\n` +
2106
+ "result = {'overlays': space.overlay.show_overlays}\n");
2107
+ return textResponse(JSON.stringify(res, null, 2));
2108
+ }
2109
+ catch (err) {
2110
+ return errorResponse(`Toggle overlays failed: ${err.message}`);
2111
+ }
2112
+ }
2113
+ export async function blenderToggleLocalView(registry, input) {
2114
+ try {
2115
+ const client = getBlenderClient(input.workspaceId);
2116
+ const enable = input.enable;
2117
+ const res = await client.sendExecute("import bpy\n" +
2118
+ VIEW3D_HELPER +
2119
+ "window, screen, area, region, space = _view3d_context()\n" +
2120
+ "was_local = space.local_view is not None\n" +
2121
+ `if ${enable === undefined ? "True" : "False"}:\n` +
2122
+ " bpy.ops.object.local_view_override()\n" +
2123
+ "else:\n" +
2124
+ ` if ${enable ? "True" : "False"} and not was_local:\n` +
2125
+ " bpy.ops.object.local_view_override()\n" +
2126
+ ` elif not ${enable ? "True" : "False"} and was_local:\n` +
2127
+ " bpy.ops.object.local_view_override()\n" +
2128
+ "result = {'local_view': space.local_view is not None}\n");
2129
+ return textResponse(JSON.stringify(res, null, 2));
2130
+ }
2131
+ catch (err) {
2132
+ return errorResponse(`Toggle local view failed: ${err.message}`);
2133
+ }
2134
+ }
2135
+ // ─── File & Export ──────────────────────────────────────────────────────────
2136
+ export async function blenderExportFbx(registry, input) {
2137
+ try {
2138
+ const workspace = registry.getWorkspace(input.workspaceId);
2139
+ const absPath = registry.resolveArtifactPath(workspace, input.filepath, "blender");
2140
+ await mkdir(dirname(absPath), { recursive: true });
2141
+ const client = getBlenderClient(input.workspaceId);
2142
+ const applyScale = input.applyScale ?? "FBX_SCALE_NONE";
2143
+ const useSelected = input.useSelected ?? false;
2144
+ const res = await client.sendExecute("import bpy\n" +
2145
+ `bpy.ops.export_scene.fbx(filepath=${JSON.stringify(absPath)}, use_selection=${useSelected ? "True" : "False"}, apply_scale_options='${applyScale}')\n` +
2146
+ `result = {'exported': ${JSON.stringify(absPath)}}\n`);
2147
+ return textResponse(JSON.stringify(res, null, 2));
2148
+ }
2149
+ catch (err) {
2150
+ return errorResponse(`Export FBX failed: ${err.message}`);
2151
+ }
2152
+ }
2153
+ export async function blenderExportObj(registry, input) {
2154
+ try {
2155
+ const workspace = registry.getWorkspace(input.workspaceId);
2156
+ const absPath = registry.resolveArtifactPath(workspace, input.filepath, "blender");
2157
+ await mkdir(dirname(absPath), { recursive: true });
2158
+ const client = getBlenderClient(input.workspaceId);
2159
+ const useSelected = input.useSelected ?? false;
2160
+ const res = await client.sendExecute("import bpy\n" +
2161
+ `bpy.ops.wm.obj_export(filepath=${JSON.stringify(absPath)}, export_selected_objects=${useSelected ? "True" : "False"})\n` +
2162
+ `result = {'exported': ${JSON.stringify(absPath)}}\n`);
2163
+ return textResponse(JSON.stringify(res, null, 2));
2164
+ }
2165
+ catch (err) {
2166
+ return errorResponse(`Export OBJ failed: ${err.message}`);
2167
+ }
2168
+ }
2169
+ export async function blenderPurgeUnusedData(registry, input) {
2170
+ try {
2171
+ const client = getBlenderClient(input.workspaceId);
2172
+ const res = await client.sendExecute("import bpy\n" +
2173
+ "bpy.ops.outliner.orphans_purge()\n" +
2174
+ "result = {'purged': 'orphan data blocks removed'}\n");
2175
+ return textResponse(JSON.stringify(res, null, 2));
2176
+ }
2177
+ catch (err) {
2178
+ return errorResponse(`Purge unused data failed: ${err.message}`);
2179
+ }
2180
+ }
2181
+ // ─── Animation ──────────────────────────────────────────────────────────────
2182
+ export async function blenderSetFrame(registry, input) {
2183
+ try {
2184
+ const client = getBlenderClient(input.workspaceId);
2185
+ const res = await client.sendExecute("import bpy\n" +
2186
+ `bpy.context.scene.frame_set(${input.frame})\n` +
2187
+ "result = {'frame': bpy.context.scene.frame_current}\n");
2188
+ return textResponse(JSON.stringify(res, null, 2));
2189
+ }
2190
+ catch (err) {
2191
+ return errorResponse(`Set frame failed: ${err.message}`);
2192
+ }
2193
+ }
2194
+ export async function blenderPlayAnimation(registry, input) {
2195
+ try {
2196
+ const client = getBlenderClient(input.workspaceId);
2197
+ const res = await client.sendExecute("import bpy\n" +
2198
+ "if bpy.context.screen:\n" +
2199
+ " bpy.ops.screen.animation_play()\n" +
2200
+ "result = {'playing': True}\n");
2201
+ return textResponse(JSON.stringify(res, null, 2));
2202
+ }
2203
+ catch (err) {
2204
+ return errorResponse(`Play animation failed: ${err.message}`);
2205
+ }
2206
+ }
2207
+ export async function blenderStopAnimation(registry, input) {
2208
+ try {
2209
+ const client = getBlenderClient(input.workspaceId);
2210
+ const res = await client.sendExecute("import bpy\n" +
2211
+ "if bpy.context.screen and bpy.context.screen.is_animation_playing:\n" +
2212
+ " bpy.ops.screen.animation_play()\n" +
2213
+ "result = {'playing': False}\n");
2214
+ return textResponse(JSON.stringify(res, null, 2));
2215
+ }
2216
+ catch (err) {
2217
+ return errorResponse(`Stop animation failed: ${err.message}`);
2218
+ }
2219
+ }
2220
+ export async function blenderInsertKeyframe(registry, input) {
2221
+ try {
2222
+ const client = getBlenderClient(input.workspaceId);
2223
+ const props = input.properties ?? ["location", "rotation_euler", "scale"];
2224
+ const frame = input.frame;
2225
+ const res = await client.sendExecute("import bpy\n" +
2226
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
2227
+ `properties = ${JSON.stringify(props)}\n` +
2228
+ `frame = ${frame ?? "None"}\n` +
2229
+ "obj = bpy.data.objects.get(object_name)\n" +
2230
+ "if obj is None:\n" +
2231
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
2232
+ "if frame is not None:\n" +
2233
+ " bpy.context.scene.frame_set(frame)\n" +
2234
+ "for prop in properties:\n" +
2235
+ " if hasattr(obj, prop):\n" +
2236
+ " obj.keyframe_insert(data_path=prop)\n" +
2237
+ "result = {'object': obj.name, 'keyframed': [p for p in properties if hasattr(obj, p)], 'frame': bpy.context.scene.frame_current}\n");
2238
+ return textResponse(JSON.stringify(res, null, 2));
2239
+ }
2240
+ catch (err) {
2241
+ return errorResponse(`Insert keyframe failed: ${err.message}`);
2242
+ }
2243
+ }