blenderwright 2.0.1__tar.gz → 2.2.0__tar.gz
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.
- {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/workflows/ci.yml +9 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/PKG-INFO +42 -9
- {blenderwright-2.0.1 → blenderwright-2.2.0}/README.md +41 -8
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/__init__.py +1 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/blender_manifest.toml +1 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/__init__.py +2 -0
- blenderwright-2.2.0/addon/handlers/code_exec.py +176 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/objects.py +35 -0
- blenderwright-2.2.0/addon/handlers/sequencer.py +212 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/ui_panel.py +34 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/index.html +171 -40
- blenderwright-2.2.0/docs/shuttle-launch.mp4 +0 -0
- blenderwright-2.2.0/glama.json +6 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/launch_ollama.sh +3 -2
- {blenderwright-2.0.1 → blenderwright-2.2.0}/pyproject.toml +1 -1
- blenderwright-2.2.0/shuttle-launch.gif +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/ollama_chat.py +427 -52
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/server.py +26 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/armature.py +2 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/booltool.py +13 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/camera.py +4 -3
- blenderwright-2.2.0/src/blenderwright/tools/code_exec.py +39 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/collections.py +2 -2
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/curves.py +3 -2
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/geometry_nodes.py +2 -2
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/gpencil.py +2 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/lighting.py +5 -3
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/materials.py +23 -7
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/mesh_editing.py +4 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/modeling.py +2 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/objects.py +83 -9
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/rendering.py +16 -8
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/scene.py +2 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/selection.py +15 -8
- blenderwright-2.2.0/src/blenderwright/tools/sequencer.py +221 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/sweep.py +2 -2
- blenderwright-2.2.0/src/blenderwright/toolsets.py +299 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/validators.py +81 -4
- blenderwright-2.2.0/tests/test_addon/conftest.py +28 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_code_exec_handler.py +99 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_objects_handler.py +115 -0
- blenderwright-2.2.0/tests/test_addon/test_handlers/test_sequencer_handler.py +261 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_ui_panel.py +20 -0
- blenderwright-2.2.0/tests/test_long_names.py +131 -0
- blenderwright-2.2.0/tests/test_ollama_budget.py +232 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_ollama_chat.py +5 -2
- blenderwright-2.2.0/tests/test_ollama_toolsets.py +152 -0
- blenderwright-2.2.0/tests/test_ollama_vision.py +195 -0
- blenderwright-2.2.0/tests/test_round_wasters.py +152 -0
- blenderwright-2.2.0/tests/test_tool_disambiguation.py +90 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_camera.py +3 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_code_exec.py +16 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_collections.py +4 -3
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_issue_31_validation.py +3 -1
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_materials.py +4 -3
- blenderwright-2.2.0/tests/test_tools/test_negative_scale_primitives.py +103 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_rendering.py +18 -13
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_scene.py +4 -3
- blenderwright-2.2.0/tests/test_tools/test_sequencer.py +235 -0
- blenderwright-2.2.0/tests/test_toolsets.py +251 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_validators.py +22 -4
- {blenderwright-2.0.1 → blenderwright-2.2.0}/uv.lock +220 -209
- blenderwright-2.0.1/addon/handlers/code_exec.py +0 -101
- blenderwright-2.0.1/src/blenderwright/tools/code_exec.py +0 -31
- blenderwright-2.0.1/tests/test_addon/conftest.py +0 -11
- {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/FUNDING.yml +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/workflows/pylint.yml +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/workflows/release.yml +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/workflows/test-bpy51.yml +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/.gitignore +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/LICENSE.md +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/dispatcher.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/animation.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/armature.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/booltool.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/camera.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/collections.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/curves.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/file_ops.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/geometry_nodes.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/gpencil.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/lighting.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/materials.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/mesh_editing.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/mesh_quality.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/modeling.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/physics.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/print3d.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/procedural.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/raster.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/rendering.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/scene.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/sculpting.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/selection.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/sweep.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/transforms.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/uv.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/viewport.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/render_guard.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/server.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/thread_safety.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/blend-ai-v1.0.0.zip +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/build.ps1 +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/build.sh +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/.nojekyll +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/CNAME +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/googled2f6c71abc2d6a7b.html +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/robots.txt +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/sitemap.xml +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/install-addon.sh +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/install_addon.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/saloon.png +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/screenshot.png +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/scripts/generate_tool_docs.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/shader-preview.png +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/shuttle-launch.png +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/__init__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/__main__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/aliases.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/connection.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/enum_hints.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/prompts/__init__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/prompts/workflows.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/resources/__init__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/resources/scene_info.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/strict.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tool_registry.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/__init__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/animation.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/file_ops.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/mesh_quality.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/physics.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/print3d.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/screenshot.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/sculpting.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/transforms.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/uv.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/viewport.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/__init__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/conftest.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/__init__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_dispatcher.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handler_registration.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/__init__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_camera_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_geometry_nodes_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_gpencil_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_materials_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_mesh_quality_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_modeling_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_print3d_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_procedural_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_raster_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_rendering_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_scene_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sculpting_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_selection_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sweep_handler.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_layer_consistency.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_module_cache_purge.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_render_guard.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_server.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_thread_safety.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_command_registry.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_connection.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_enum_hints.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_installer.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_object_param_naming.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_project_metadata.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_strict_parameters.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/__init__.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/conftest.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_animation.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_armature.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_auto_critique_prompt.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_booltool.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_curves.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_eevee_light_path.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_extensions.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_file_ops.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_geometry_nodes.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_gpencil.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_lighting.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_mesh_editing.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_mesh_quality.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_mesh_repair.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_modeling.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_new_nodes.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_numeric_text_values.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_objects.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_physics.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_point_camera_at.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_print3d.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_procedural.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_prompts.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_raster.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_screenshot.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_sculpting.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_selection.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_selection_respected.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_shader_nodes.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_sweep.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_transforms.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_uv.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_uv_slim.py +0 -0
- {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_viewport.py +0 -0
|
@@ -37,11 +37,19 @@ jobs:
|
|
|
37
37
|
run: bandit -r src/ -ll # low severity threshold; skip install_addon.py (subprocess by design)
|
|
38
38
|
- name: Install project dependencies
|
|
39
39
|
run: pip install -e ".[dev]"
|
|
40
|
-
- name: pip-audit
|
|
40
|
+
- name: pip-audit (fresh resolution)
|
|
41
41
|
# Audits the environment, which now contains this project's declared
|
|
42
42
|
# dependencies. The upgrade step above keeps the runner's own build
|
|
43
43
|
# tooling current so drift in the image does not fail the build.
|
|
44
44
|
run: pip-audit
|
|
45
|
+
- name: pip-audit (uv.lock)
|
|
46
|
+
# The step above resolves today's versions and passed for weeks while
|
|
47
|
+
# uv.lock, which is what `uv run` and every developer actually
|
|
48
|
+
# install, carried 64 known vulnerabilities. Audit the pins too.
|
|
49
|
+
run: |
|
|
50
|
+
pip install uv
|
|
51
|
+
uv export --all-extras --format requirements-txt --no-hashes --no-emit-project -o /tmp/uv-lock.txt
|
|
52
|
+
pip-audit --no-deps --disable-pip --strict -r /tmp/uv-lock.txt
|
|
45
53
|
|
|
46
54
|
tests:
|
|
47
55
|
name: Tests (${{ matrix.os }} / Python ${{ matrix.python-version }})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blenderwright
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: The most intuitive and efficient MCP Server for Blender
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -24,26 +24,32 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
|
|
|
24
24
|
|
|
25
25
|
**blenderwright goes beyond tool exposure: it guides the LLM to produce professional 3D results** through expert prompts, proven workflows, visual feedback, and mesh quality analysis.
|
|
26
26
|
|
|
27
|
-
<small>A
|
|
27
|
+
<small>A shuttle launch from two questions: "do you want to have a go at the space shuttle launch?" and then "could you animate the launch?" Claude Code (Fable 5.1) modelled the orbiter, tank, boosters and service tower, lit the plumes as emissive volumes and grew the exhaust cloud from metaballs. Keyframes lift the stack and tilt the camera after it, drivers stream the plumes and boil the smoke, and all 60 Cycles frames come back from a single `render_animation` call, all through blenderwright with no manual modelling:</small>
|
|
28
28
|
|
|
29
|
-

|
|
30
|
+
|
|
31
|
+
<small>[Watch it with sound](https://blenderwright.holdmybeer.gg/shuttle-launch.mp4). Launch audio courtesy of NASA: STS-131, "Sound of Launch."</small>
|
|
30
32
|
|
|
31
|
-
<small>
|
|
33
|
+
<small>The still it grew from, modelled and rendered in under twenty minutes:</small>
|
|
32
34
|
|
|
33
35
|

|
|
34
36
|
|
|
37
|
+
<small>A two-storey Western saloon from a single sentence: "create a Western style 2-story saloon, the outside is more important than any inside detail." Claude Code (Fable 5.1) built the false front, plank siding, balcony, batwing doors and street dressing, then relit it for dusk, all through blenderwright, in about twenty minutes with no manual modelling:</small>
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
35
41
|
<small>Fifteen procedural materials, each built by a single `create_procedural_material` call. The selected ball's node graph below was generated entirely by that one call: coordinates, mapping, noise, height mask, colour ramp, and Principled BSDF, laid out and wired:</small>
|
|
36
42
|
|
|
37
43
|

|
|
38
44
|
|
|
39
45
|
## Key Features
|
|
40
46
|
|
|
41
|
-
- **[
|
|
47
|
+
- **[191 tools](https://blenderwright.holdmybeer.gg/)** across 28 modules covering every major Blender domain: modeling, mesh editing, materials, shader nodes, lighting, camera, animation, rendering, sculpting, UV mapping, physics, geometry nodes, rigging, curves, sweeps along a path, 3D-print checking, annotations, collections, file I/O, Bool Tool, viewport control, mesh quality analysis, and extension suggestions
|
|
42
48
|
- **12 expert prompts**: topology best practices, real-world scale references, lighting principles, studio setup, character basemesh workflow, PBR material guide, auto-critique feedback loop, and more
|
|
43
49
|
- **Visual feedback loop**: fast viewport screenshots via OpenGL render (~ms, not seconds) with auto-critique prompts that guide the LLM to check its own work
|
|
44
50
|
- **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
|
|
45
51
|
- **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
|
|
46
|
-
- **
|
|
52
|
+
- **Raw Python is opt-in**: `execute_blender_code` is off until you tick "Allow raw Python" in the N-panel, per session, never saved into the .blend. When on, every snippet is echoed to the console and listed in the panel. A restricted namespace blocks obvious mistakes (`os`, `subprocess`, `exec`, `open`, ...); it is a guard against accidents, not a security boundary
|
|
47
53
|
- **Render-aware**: automatically detects when Blender is rendering and queues commands. Recovers from stuck render guards via `load_post` handler and reset command
|
|
48
54
|
- **Blender 4.2+ compatible**: ships as a Blender Extension; tested against Blender 5.1 with EEVEE identifier, Annotation API, sculpt stroke_method, SLIM UV unwrap, Raycast shader node, and EEVEE light path intensity controls
|
|
49
55
|
- **Custom port**: configure the server port from the N-panel UI (default: 9876, range: 1024–65535)
|
|
@@ -250,7 +256,7 @@ blenderwright includes 12 MCP prompts that guide the LLM toward professional-qua
|
|
|
250
256
|
## Tool Domains
|
|
251
257
|
|
|
252
258
|
<details>
|
|
253
|
-
<summary><strong>All
|
|
259
|
+
<summary><strong>All 191 tools across 28 modules</strong></summary>
|
|
254
260
|
|
|
255
261
|
Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://blenderwright.holdmybeer.gg/)**
|
|
256
262
|
|
|
@@ -272,6 +278,7 @@ Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://ble
|
|
|
272
278
|
| Curves | 10 | Bezier/NURBS/path, 3D text, convert, reverse, handle types, cyclic, subdivide |
|
|
273
279
|
| Sweep | 2 | Sweep a profile along a 3D path for tubes, hoses, cables and rails, with flip-free framing; check a path for bends too tight for the profile |
|
|
274
280
|
| 3D Printing | 1 | Reversed normals, self-intersection, thin walls, shells and overhangs via Blender's 3D Print Toolbox |
|
|
281
|
+
| Sequencer | 5 | Image-sequence strips, sound strips, list and remove strips, render to MP4, MKV or WebM with audio |
|
|
275
282
|
| Sculpting | 8 | Brushes, remesh, multires, symmetry, dynamic topology, stroke_method |
|
|
276
283
|
| UV Mapping | 4 | Smart project, unwrap (ANGLE_BASED, CONFORMAL, SLIM), projection, pack islands |
|
|
277
284
|
| Physics | 9 | Rigid body, cloth, fluid, particles (velocity, rendering, delete), bake |
|
|
@@ -286,6 +293,31 @@ Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://ble
|
|
|
286
293
|
|
|
287
294
|
</details>
|
|
288
295
|
|
|
296
|
+
### Toolsets
|
|
297
|
+
|
|
298
|
+
Every tool is loaded by default, and that is the setting the featured renders were made with. The full set of schemas costs roughly 48k tokens per request, which is fine for Claude-class clients and too much for a local model on a 32k window. `BLENDERWRIGHT_TOOLSETS` (or `--toolsets`) trims it:
|
|
299
|
+
|
|
300
|
+
| Value | Loads |
|
|
301
|
+
|-------|-------|
|
|
302
|
+
| unset or `all` | everything (default) |
|
|
303
|
+
| `auto` | the core toolsets plus `list_toolsets` and `enable_toolset`; the model loads other groups as a task needs them |
|
|
304
|
+
| `core` | scene, objects, transforms, modeling, mesh quality, lighting, camera, rendering, screenshot, viewport, collections |
|
|
305
|
+
| `core,physics,animation` | any comma-separated list of toolset names; a toolset is one row of the table above |
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"mcpServers": {
|
|
310
|
+
"blenderwright": {
|
|
311
|
+
"command": "uvx",
|
|
312
|
+
"args": ["blenderwright"],
|
|
313
|
+
"env": { "BLENDERWRIGHT_TOOLSETS": "auto" }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`auto` relies on the client honouring the MCP `tools/list_changed` notification. The bundled Ollama chat client picks `auto` on its own when the full set would take more than half of `--num-ctx`, and says so at startup.
|
|
320
|
+
|
|
289
321
|
## Architecture
|
|
290
322
|
|
|
291
323
|
```
|
|
@@ -317,7 +349,8 @@ AI Assistant <--stdio/MCP--> blenderwright server <--TCP socket--> Blender addon
|
|
|
317
349
|
<summary><strong>Security</strong></summary>
|
|
318
350
|
|
|
319
351
|
- **Localhost only**: The TCP socket binds to `127.0.0.1`, never exposed to the network.
|
|
320
|
-
- **
|
|
352
|
+
- **Raw Python is off by default**: `execute_blender_code` refuses to run until you tick "Allow raw Python" in the blenderwright N-panel. The switch is a `WindowManager` property, so it is per Blender session and is never written into a `.blend` file. Every run is echoed to the system console and the panel shows a count and the last snippet. The 191 structured tools do not need it.
|
|
353
|
+
- **The restricted namespace is not a sandbox**: when raw Python is on, the import hook refuses 25 imports (`os`, `subprocess`, `socket`, `shutil`, `sys`, `ctypes`, `importlib`, `pathlib`, `signal`, `multiprocessing`, `pickle`, `shelve`, `tempfile`, `http`, `urllib`, `ftplib`, `smtplib`, `xmlrpc`, `code`, `codeop`, `compileall`, `webbrowser`, `antigravity`, `turtle`, `tkinter`) and the builtins `__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help` and `memoryview` are removed. That catches a model making a mistake. It does not contain an adversary: the code runs in-process with full `bpy`, and `bpy` alone can save files anywhere and run any operator. Treat "Allow raw Python" the way you would treat running a script someone else wrote: read what the model is about to run, and turn it off when you are done.
|
|
321
354
|
- **Input validation**: All inputs pass through validators before reaching Blender: name sanitization, path traversal prevention, numeric range checks, enum allowlists.
|
|
322
355
|
- **File safety**: Import operations disable `use_scripts_auto_execute` to prevent script injection from imported files. File extensions are checked against allowlists.
|
|
323
356
|
- **Command allowlist**: The addon dispatcher only processes explicitly registered commands. Unknown commands are rejected.
|
|
@@ -369,7 +402,7 @@ blenderwright/
|
|
|
369
402
|
│ ├── server.py # FastMCP entry point
|
|
370
403
|
│ ├── connection.py # TCP client to Blender (with busy-retry)
|
|
371
404
|
│ ├── validators.py # Input validation
|
|
372
|
-
│ ├── tools/ #
|
|
405
|
+
│ ├── tools/ # 28 tool modules (191 tools)
|
|
373
406
|
│ ├── resources/ # MCP resources (scene, objects, materials)
|
|
374
407
|
│ └── prompts/ # 12 expert prompt templates
|
|
375
408
|
├── addon/ # Blender addon (zero external deps)
|
|
@@ -6,26 +6,32 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
|
|
|
6
6
|
|
|
7
7
|
**blenderwright goes beyond tool exposure: it guides the LLM to produce professional 3D results** through expert prompts, proven workflows, visual feedback, and mesh quality analysis.
|
|
8
8
|
|
|
9
|
-
<small>A
|
|
9
|
+
<small>A shuttle launch from two questions: "do you want to have a go at the space shuttle launch?" and then "could you animate the launch?" Claude Code (Fable 5.1) modelled the orbiter, tank, boosters and service tower, lit the plumes as emissive volumes and grew the exhaust cloud from metaballs. Keyframes lift the stack and tilt the camera after it, drivers stream the plumes and boil the smoke, and all 60 Cycles frames come back from a single `render_animation` call, all through blenderwright with no manual modelling:</small>
|
|
10
10
|
|
|
11
|
-

|
|
12
|
+
|
|
13
|
+
<small>[Watch it with sound](https://blenderwright.holdmybeer.gg/shuttle-launch.mp4). Launch audio courtesy of NASA: STS-131, "Sound of Launch."</small>
|
|
12
14
|
|
|
13
|
-
<small>
|
|
15
|
+
<small>The still it grew from, modelled and rendered in under twenty minutes:</small>
|
|
14
16
|
|
|
15
17
|

|
|
16
18
|
|
|
19
|
+
<small>A two-storey Western saloon from a single sentence: "create a Western style 2-story saloon, the outside is more important than any inside detail." Claude Code (Fable 5.1) built the false front, plank siding, balcony, batwing doors and street dressing, then relit it for dusk, all through blenderwright, in about twenty minutes with no manual modelling:</small>
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
17
23
|
<small>Fifteen procedural materials, each built by a single `create_procedural_material` call. The selected ball's node graph below was generated entirely by that one call: coordinates, mapping, noise, height mask, colour ramp, and Principled BSDF, laid out and wired:</small>
|
|
18
24
|
|
|
19
25
|

|
|
20
26
|
|
|
21
27
|
## Key Features
|
|
22
28
|
|
|
23
|
-
- **[
|
|
29
|
+
- **[191 tools](https://blenderwright.holdmybeer.gg/)** across 28 modules covering every major Blender domain: modeling, mesh editing, materials, shader nodes, lighting, camera, animation, rendering, sculpting, UV mapping, physics, geometry nodes, rigging, curves, sweeps along a path, 3D-print checking, annotations, collections, file I/O, Bool Tool, viewport control, mesh quality analysis, and extension suggestions
|
|
24
30
|
- **12 expert prompts**: topology best practices, real-world scale references, lighting principles, studio setup, character basemesh workflow, PBR material guide, auto-critique feedback loop, and more
|
|
25
31
|
- **Visual feedback loop**: fast viewport screenshots via OpenGL render (~ms, not seconds) with auto-critique prompts that guide the LLM to check its own work
|
|
26
32
|
- **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
|
|
27
33
|
- **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
|
|
28
|
-
- **
|
|
34
|
+
- **Raw Python is opt-in**: `execute_blender_code` is off until you tick "Allow raw Python" in the N-panel, per session, never saved into the .blend. When on, every snippet is echoed to the console and listed in the panel. A restricted namespace blocks obvious mistakes (`os`, `subprocess`, `exec`, `open`, ...); it is a guard against accidents, not a security boundary
|
|
29
35
|
- **Render-aware**: automatically detects when Blender is rendering and queues commands. Recovers from stuck render guards via `load_post` handler and reset command
|
|
30
36
|
- **Blender 4.2+ compatible**: ships as a Blender Extension; tested against Blender 5.1 with EEVEE identifier, Annotation API, sculpt stroke_method, SLIM UV unwrap, Raycast shader node, and EEVEE light path intensity controls
|
|
31
37
|
- **Custom port**: configure the server port from the N-panel UI (default: 9876, range: 1024–65535)
|
|
@@ -232,7 +238,7 @@ blenderwright includes 12 MCP prompts that guide the LLM toward professional-qua
|
|
|
232
238
|
## Tool Domains
|
|
233
239
|
|
|
234
240
|
<details>
|
|
235
|
-
<summary><strong>All
|
|
241
|
+
<summary><strong>All 191 tools across 28 modules</strong></summary>
|
|
236
242
|
|
|
237
243
|
Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://blenderwright.holdmybeer.gg/)**
|
|
238
244
|
|
|
@@ -254,6 +260,7 @@ Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://ble
|
|
|
254
260
|
| Curves | 10 | Bezier/NURBS/path, 3D text, convert, reverse, handle types, cyclic, subdivide |
|
|
255
261
|
| Sweep | 2 | Sweep a profile along a 3D path for tubes, hoses, cables and rails, with flip-free framing; check a path for bends too tight for the profile |
|
|
256
262
|
| 3D Printing | 1 | Reversed normals, self-intersection, thin walls, shells and overhangs via Blender's 3D Print Toolbox |
|
|
263
|
+
| Sequencer | 5 | Image-sequence strips, sound strips, list and remove strips, render to MP4, MKV or WebM with audio |
|
|
257
264
|
| Sculpting | 8 | Brushes, remesh, multires, symmetry, dynamic topology, stroke_method |
|
|
258
265
|
| UV Mapping | 4 | Smart project, unwrap (ANGLE_BASED, CONFORMAL, SLIM), projection, pack islands |
|
|
259
266
|
| Physics | 9 | Rigid body, cloth, fluid, particles (velocity, rendering, delete), bake |
|
|
@@ -268,6 +275,31 @@ Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://ble
|
|
|
268
275
|
|
|
269
276
|
</details>
|
|
270
277
|
|
|
278
|
+
### Toolsets
|
|
279
|
+
|
|
280
|
+
Every tool is loaded by default, and that is the setting the featured renders were made with. The full set of schemas costs roughly 48k tokens per request, which is fine for Claude-class clients and too much for a local model on a 32k window. `BLENDERWRIGHT_TOOLSETS` (or `--toolsets`) trims it:
|
|
281
|
+
|
|
282
|
+
| Value | Loads |
|
|
283
|
+
|-------|-------|
|
|
284
|
+
| unset or `all` | everything (default) |
|
|
285
|
+
| `auto` | the core toolsets plus `list_toolsets` and `enable_toolset`; the model loads other groups as a task needs them |
|
|
286
|
+
| `core` | scene, objects, transforms, modeling, mesh quality, lighting, camera, rendering, screenshot, viewport, collections |
|
|
287
|
+
| `core,physics,animation` | any comma-separated list of toolset names; a toolset is one row of the table above |
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"mcpServers": {
|
|
292
|
+
"blenderwright": {
|
|
293
|
+
"command": "uvx",
|
|
294
|
+
"args": ["blenderwright"],
|
|
295
|
+
"env": { "BLENDERWRIGHT_TOOLSETS": "auto" }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`auto` relies on the client honouring the MCP `tools/list_changed` notification. The bundled Ollama chat client picks `auto` on its own when the full set would take more than half of `--num-ctx`, and says so at startup.
|
|
302
|
+
|
|
271
303
|
## Architecture
|
|
272
304
|
|
|
273
305
|
```
|
|
@@ -299,7 +331,8 @@ AI Assistant <--stdio/MCP--> blenderwright server <--TCP socket--> Blender addon
|
|
|
299
331
|
<summary><strong>Security</strong></summary>
|
|
300
332
|
|
|
301
333
|
- **Localhost only**: The TCP socket binds to `127.0.0.1`, never exposed to the network.
|
|
302
|
-
- **
|
|
334
|
+
- **Raw Python is off by default**: `execute_blender_code` refuses to run until you tick "Allow raw Python" in the blenderwright N-panel. The switch is a `WindowManager` property, so it is per Blender session and is never written into a `.blend` file. Every run is echoed to the system console and the panel shows a count and the last snippet. The 191 structured tools do not need it.
|
|
335
|
+
- **The restricted namespace is not a sandbox**: when raw Python is on, the import hook refuses 25 imports (`os`, `subprocess`, `socket`, `shutil`, `sys`, `ctypes`, `importlib`, `pathlib`, `signal`, `multiprocessing`, `pickle`, `shelve`, `tempfile`, `http`, `urllib`, `ftplib`, `smtplib`, `xmlrpc`, `code`, `codeop`, `compileall`, `webbrowser`, `antigravity`, `turtle`, `tkinter`) and the builtins `__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help` and `memoryview` are removed. That catches a model making a mistake. It does not contain an adversary: the code runs in-process with full `bpy`, and `bpy` alone can save files anywhere and run any operator. Treat "Allow raw Python" the way you would treat running a script someone else wrote: read what the model is about to run, and turn it off when you are done.
|
|
303
336
|
- **Input validation**: All inputs pass through validators before reaching Blender: name sanitization, path traversal prevention, numeric range checks, enum allowlists.
|
|
304
337
|
- **File safety**: Import operations disable `use_scripts_auto_execute` to prevent script injection from imported files. File extensions are checked against allowlists.
|
|
305
338
|
- **Command allowlist**: The addon dispatcher only processes explicitly registered commands. Unknown commands are rejected.
|
|
@@ -351,7 +384,7 @@ blenderwright/
|
|
|
351
384
|
│ ├── server.py # FastMCP entry point
|
|
352
385
|
│ ├── connection.py # TCP client to Blender (with busy-retry)
|
|
353
386
|
│ ├── validators.py # Input validation
|
|
354
|
-
│ ├── tools/ #
|
|
387
|
+
│ ├── tools/ # 28 tool modules (191 tools)
|
|
355
388
|
│ ├── resources/ # MCP resources (scene, objects, materials)
|
|
356
389
|
│ └── prompts/ # 12 expert prompt templates
|
|
357
390
|
├── addon/ # Blender addon (zero external deps)
|
|
@@ -8,7 +8,7 @@ Blender's Python API.
|
|
|
8
8
|
bl_info = {
|
|
9
9
|
"name": "blenderwright",
|
|
10
10
|
"author": "blenderwright",
|
|
11
|
-
"version": (2,
|
|
11
|
+
"version": (2, 2, 0),
|
|
12
12
|
"blender": (4, 2, 0),
|
|
13
13
|
"location": "View3D > Sidebar > blenderwright",
|
|
14
14
|
"description": "MCP Server integration for AI-assisted 3D workflows",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Blender addon handler for raw Python execution (off by default).
|
|
2
|
+
|
|
3
|
+
What this is: a restricted namespace that blocks the obvious footguns
|
|
4
|
+
(import os/subprocess/socket..., exec/eval/open/compile) so a model
|
|
5
|
+
that makes a mistake fails loudly instead of touching the filesystem
|
|
6
|
+
or the network by accident.
|
|
7
|
+
|
|
8
|
+
What this is NOT: a security boundary. The code runs in-process inside
|
|
9
|
+
Blender with full `bpy`, and `bpy` alone can save files anywhere and
|
|
10
|
+
run any operator. A denylist cannot contain a determined adversary in
|
|
11
|
+
the same interpreter, and this module does not claim to.
|
|
12
|
+
|
|
13
|
+
So the real control is the switch, not the list:
|
|
14
|
+
|
|
15
|
+
- `execute_code` is refused unless the user has ticked "Allow raw
|
|
16
|
+
Python" in the blenderwright N-panel. The flag lives on
|
|
17
|
+
`bpy.types.WindowManager`, so it is per session and is never saved
|
|
18
|
+
into a .blend file that could be handed to someone else.
|
|
19
|
+
- Every snippet that runs is echoed to the system console and kept in
|
|
20
|
+
a short log that the panel displays, so the user can see what the
|
|
21
|
+
model actually did.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
import io
|
|
26
|
+
import time
|
|
27
|
+
from collections import deque
|
|
28
|
+
from .. import dispatcher
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Name of the per-session switch registered on bpy.types.WindowManager
|
|
32
|
+
# by ui_panel.register(). Not a Scene property on purpose: Scene props
|
|
33
|
+
# are written into the .blend.
|
|
34
|
+
SWITCH_PROP = "blenderwright_allow_code_exec"
|
|
35
|
+
|
|
36
|
+
# How many recent runs the panel can show.
|
|
37
|
+
EXEC_LOG_SIZE = 20
|
|
38
|
+
_exec_log: deque = deque(maxlen=EXEC_LOG_SIZE)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_enabled() -> bool:
|
|
42
|
+
"""True only if the user turned raw Python on for this Blender session.
|
|
43
|
+
|
|
44
|
+
Anything other than a real `True` (missing property, wrong type, no
|
|
45
|
+
window manager yet, bpy not importable) counts as off.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
import bpy
|
|
49
|
+
flag = getattr(bpy.context.window_manager, SWITCH_PROP, False)
|
|
50
|
+
except Exception:
|
|
51
|
+
return False
|
|
52
|
+
return isinstance(flag, bool) and flag
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_exec_log() -> list:
|
|
56
|
+
"""Copy of the recent-run log, oldest first."""
|
|
57
|
+
return list(_exec_log)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _record(code: str, ok: bool) -> None:
|
|
61
|
+
lines = code.strip().splitlines() or [""]
|
|
62
|
+
_exec_log.append({
|
|
63
|
+
"time": time.strftime("%H:%M:%S"),
|
|
64
|
+
"first_line": lines[0].strip(),
|
|
65
|
+
"lines": len(lines),
|
|
66
|
+
"ok": ok,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Modules refused by the import hook. A guard against accidental misuse by
|
|
71
|
+
# the model, not a security control: see the module docstring.
|
|
72
|
+
BLOCKED_MODULES = frozenset({
|
|
73
|
+
"os", "subprocess", "socket", "shutil", "sys", "ctypes",
|
|
74
|
+
"importlib", "pathlib", "signal", "multiprocessing",
|
|
75
|
+
"pickle", "shelve", "tempfile", "http", "urllib", "ftplib",
|
|
76
|
+
"smtplib", "xmlrpc", "code", "codeop", "compileall",
|
|
77
|
+
"webbrowser", "antigravity", "turtle", "tkinter",
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
# Builtins removed from the restricted namespace. Same caveat as above.
|
|
81
|
+
_REMOVED_BUILTINS = frozenset({
|
|
82
|
+
"__import__", "exec", "eval", "compile", "open",
|
|
83
|
+
"globals", "locals", "vars", "input", "breakpoint",
|
|
84
|
+
"exit", "quit", "help", "memoryview",
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
# Reference to the real __import__ for the safe import hook
|
|
88
|
+
_real_import = builtins_import = __builtins__.__import__ if hasattr(
|
|
89
|
+
__builtins__, "__import__"
|
|
90
|
+
) else __builtins__["__import__"] if isinstance(__builtins__, dict) else __import__
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _safe_import(name, *args, **kwargs):
|
|
94
|
+
"""Import hook that refuses the modules in BLOCKED_MODULES."""
|
|
95
|
+
base_module = name.split(".")[0]
|
|
96
|
+
if base_module in BLOCKED_MODULES:
|
|
97
|
+
raise ImportError(
|
|
98
|
+
f"Module '{name}' is blocked inside execute_blender_code. "
|
|
99
|
+
f"Use the structured MCP tools instead of direct Python imports."
|
|
100
|
+
)
|
|
101
|
+
return _real_import(name, *args, **kwargs)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _build_safe_builtins():
|
|
105
|
+
"""Build the restricted builtins dict."""
|
|
106
|
+
# Get all builtins as a dict
|
|
107
|
+
if isinstance(__builtins__, dict):
|
|
108
|
+
raw = dict(__builtins__)
|
|
109
|
+
else:
|
|
110
|
+
raw = {k: getattr(__builtins__, k) for k in dir(__builtins__)}
|
|
111
|
+
|
|
112
|
+
# Remove dangerous builtins
|
|
113
|
+
safe = {k: v for k, v in raw.items() if k not in _REMOVED_BUILTINS}
|
|
114
|
+
|
|
115
|
+
# Add our safe import hook
|
|
116
|
+
safe["__import__"] = _safe_import
|
|
117
|
+
|
|
118
|
+
return safe
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Build once at module load
|
|
122
|
+
SAFE_BUILTINS = _build_safe_builtins()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def handle_execute_code(params: dict) -> dict:
|
|
126
|
+
"""Run Python inside Blender, if the user has allowed it this session.
|
|
127
|
+
|
|
128
|
+
- Refuses with PermissionError while "Allow raw Python" is unticked.
|
|
129
|
+
- Echoes the snippet to the system console (stderr) and records it
|
|
130
|
+
in the exec log before running it.
|
|
131
|
+
- Runs in the restricted namespace (see module docstring for what
|
|
132
|
+
that does and does not mean) and captures stdout.
|
|
133
|
+
"""
|
|
134
|
+
code = params.get("code", "")
|
|
135
|
+
if not code:
|
|
136
|
+
raise ValueError("No code provided")
|
|
137
|
+
|
|
138
|
+
if not is_enabled():
|
|
139
|
+
raise PermissionError(
|
|
140
|
+
"execute_blender_code is disabled. Raw Python is off by default; "
|
|
141
|
+
"tick 'Allow raw Python' in the blenderwright panel "
|
|
142
|
+
"(View3D > Sidebar > blenderwright) to enable it for this Blender "
|
|
143
|
+
"session, or use the structured tools instead."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Echo to the real console so the user can see what ran, regardless
|
|
147
|
+
# of whether the MCP client shows tool inputs. Goes to stderr so it
|
|
148
|
+
# is never mixed into the captured stdout returned to the model.
|
|
149
|
+
print(
|
|
150
|
+
f"[blenderwright] execute_code ({time.strftime('%H:%M:%S')}):\n{code}",
|
|
151
|
+
file=sys.stderr,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Capture stdout
|
|
155
|
+
old_stdout = sys.stdout
|
|
156
|
+
sys.stdout = buffer = io.StringIO()
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
exec(code, {"__builtins__": SAFE_BUILTINS})
|
|
160
|
+
output = buffer.getvalue()
|
|
161
|
+
_record(code, ok=True)
|
|
162
|
+
return {
|
|
163
|
+
"output": output.strip(),
|
|
164
|
+
"success": True,
|
|
165
|
+
}
|
|
166
|
+
except Exception as e:
|
|
167
|
+
output = buffer.getvalue()
|
|
168
|
+
_record(code, ok=False)
|
|
169
|
+
raise RuntimeError(f"{type(e).__name__}: {e}\nOutput before error: {output}")
|
|
170
|
+
finally:
|
|
171
|
+
sys.stdout = old_stdout
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def register():
|
|
175
|
+
"""Register code execution handler with the dispatcher."""
|
|
176
|
+
dispatcher.register_handler("execute_code", handle_execute_code)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import math
|
|
4
4
|
|
|
5
5
|
import bpy
|
|
6
|
+
import mathutils
|
|
6
7
|
from .. import dispatcher
|
|
7
8
|
|
|
8
9
|
# Map of primitive type to bpy.ops function
|
|
@@ -38,6 +39,29 @@ def _get_primitive_op(ptype: str):
|
|
|
38
39
|
return ops_map.get(ptype)
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
def _extent(obj) -> dict:
|
|
43
|
+
"""The object's size and where it ends, in world space.
|
|
44
|
+
|
|
45
|
+
A caller placing the next part needs to know where this one stops. A
|
|
46
|
+
model assumed a primitive was one unit tall, scaled it by 18.5, and put
|
|
47
|
+
the nose cone at z=9.25, halfway up inside a body that reached 18.5.
|
|
48
|
+
Blender's cylinder is two units tall; the result says so now.
|
|
49
|
+
"""
|
|
50
|
+
bpy.context.view_layer.update()
|
|
51
|
+
out = {"dimensions": [float(v) for v in obj.dimensions]}
|
|
52
|
+
try:
|
|
53
|
+
corners = [obj.matrix_world @ mathutils.Vector(c) for c in obj.bound_box]
|
|
54
|
+
xs, ys, zs = ([float(getattr(c, axis)) for c in corners] for axis in "xyz")
|
|
55
|
+
out["bounds"] = {
|
|
56
|
+
"min": [min(xs), min(ys), min(zs)],
|
|
57
|
+
"max": [max(xs), max(ys), max(zs)],
|
|
58
|
+
}
|
|
59
|
+
except Exception:
|
|
60
|
+
# No bound box (an empty), or something odd: the dimensions still help.
|
|
61
|
+
pass
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
41
65
|
def handle_create_object(params: dict) -> dict:
|
|
42
66
|
"""Create a primitive object in the scene."""
|
|
43
67
|
obj_type = params.get("type", "CUBE")
|
|
@@ -65,6 +89,7 @@ def handle_create_object(params: dict) -> dict:
|
|
|
65
89
|
"name": obj.name,
|
|
66
90
|
"type": obj.type,
|
|
67
91
|
"location": list(obj.location),
|
|
92
|
+
**_extent(obj),
|
|
68
93
|
}
|
|
69
94
|
except Exception as e:
|
|
70
95
|
raise RuntimeError(f"Failed to create object: {e}")
|
|
@@ -100,6 +125,7 @@ def handle_create_polygon_prism(params: dict) -> dict:
|
|
|
100
125
|
"type": obj.type,
|
|
101
126
|
"location": list(obj.location),
|
|
102
127
|
"sides": sides,
|
|
128
|
+
**_extent(obj),
|
|
103
129
|
}
|
|
104
130
|
except Exception as e:
|
|
105
131
|
raise RuntimeError(f"Failed to create polygon prism: {e}")
|
|
@@ -260,6 +286,7 @@ def handle_create_threaded_shaft(params: dict) -> dict:
|
|
|
260
286
|
"pitch": pitch,
|
|
261
287
|
"thread_depth": thread_depth,
|
|
262
288
|
"iterations": iterations,
|
|
289
|
+
**_extent(obj),
|
|
263
290
|
}
|
|
264
291
|
except Exception as e:
|
|
265
292
|
raise RuntimeError(f"Failed to create threaded shaft: {e}")
|
|
@@ -510,9 +537,17 @@ def handle_join_objects(params: dict) -> dict:
|
|
|
510
537
|
bpy.ops.object.join()
|
|
511
538
|
|
|
512
539
|
result_obj = bpy.context.active_object
|
|
540
|
+
# The others are gone. A caller that uses one of their names on the
|
|
541
|
+
# next call gets "not found" and loses a round; say so here instead.
|
|
542
|
+
removed = [n for n in names if n != result_obj.name]
|
|
513
543
|
return {
|
|
514
544
|
"name": result_obj.name,
|
|
515
545
|
"joined_count": len(names),
|
|
546
|
+
"removed": removed,
|
|
547
|
+
"note": (
|
|
548
|
+
f"{', '.join(removed)} no longer exist; their geometry is part of "
|
|
549
|
+
f"{result_obj.name}. Refer to {result_obj.name} from now on."
|
|
550
|
+
),
|
|
516
551
|
"success": True,
|
|
517
552
|
}
|
|
518
553
|
except ValueError:
|