blenderwright 2.2.0__tar.gz → 2.3.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.2.0 → blenderwright-2.3.0}/PKG-INFO +5 -4
- {blenderwright-2.2.0 → blenderwright-2.3.0}/README.md +4 -3
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/__init__.py +1 -1
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/blender_manifest.toml +1 -1
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/materials.py +12 -2
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/mesh_quality.py +6 -4
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/objects.py +21 -1
- {blenderwright-2.2.0 → blenderwright-2.3.0}/pyproject.toml +1 -1
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/prompts/workflows.py +51 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_materials_handler.py +64 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_mesh_quality_handler.py +46 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_objects_handler.py +62 -0
- blenderwright-2.3.0/tests/test_tools/test_imported_mesh_cleanup_prompt.py +71 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/uv.lock +4 -4
- {blenderwright-2.2.0 → blenderwright-2.3.0}/.github/FUNDING.yml +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/.github/workflows/ci.yml +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/.github/workflows/pylint.yml +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/.github/workflows/release.yml +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/.github/workflows/test-bpy51.yml +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/.gitignore +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/LICENSE.md +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/dispatcher.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/animation.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/armature.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/booltool.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/camera.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/code_exec.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/collections.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/curves.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/file_ops.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/geometry_nodes.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/gpencil.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/lighting.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/mesh_editing.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/modeling.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/physics.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/print3d.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/procedural.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/raster.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/rendering.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/scene.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/sculpting.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/selection.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/sequencer.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/sweep.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/transforms.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/uv.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/handlers/viewport.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/render_guard.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/server.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/thread_safety.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/addon/ui_panel.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/blend-ai-v1.0.0.zip +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/build.ps1 +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/build.sh +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/docs/.nojekyll +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/docs/CNAME +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/docs/googled2f6c71abc2d6a7b.html +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/docs/index.html +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/docs/robots.txt +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/docs/shuttle-launch.mp4 +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/docs/sitemap.xml +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/glama.json +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/install-addon.sh +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/install_addon.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/launch_ollama.sh +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/saloon.png +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/screenshot.png +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/scripts/generate_tool_docs.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/shader-preview.png +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/shuttle-launch.gif +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/shuttle-launch.png +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/__main__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/aliases.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/connection.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/enum_hints.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/ollama_chat.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/prompts/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/resources/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/resources/scene_info.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/server.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/strict.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tool_registry.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/animation.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/armature.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/booltool.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/camera.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/code_exec.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/collections.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/curves.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/file_ops.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/geometry_nodes.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/gpencil.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/lighting.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/materials.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/mesh_editing.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/mesh_quality.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/modeling.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/objects.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/physics.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/print3d.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/rendering.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/scene.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/screenshot.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/sculpting.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/selection.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/sequencer.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/sweep.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/transforms.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/uv.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/tools/viewport.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/toolsets.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/src/blenderwright/validators.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/conftest.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/conftest.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_dispatcher.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handler_registration.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_camera_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_code_exec_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_geometry_nodes_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_gpencil_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_modeling_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_print3d_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_procedural_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_raster_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_rendering_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_scene_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_sculpting_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_selection_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_sequencer_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_sweep_handler.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_layer_consistency.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_module_cache_purge.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_render_guard.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_server.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_thread_safety.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_ui_panel.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_command_registry.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_connection.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_enum_hints.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_installer.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_long_names.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_object_param_naming.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_ollama_budget.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_ollama_chat.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_ollama_toolsets.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_ollama_vision.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_project_metadata.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_round_wasters.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_strict_parameters.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tool_disambiguation.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/__init__.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/conftest.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_animation.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_armature.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_auto_critique_prompt.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_booltool.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_camera.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_code_exec.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_collections.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_curves.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_eevee_light_path.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_extensions.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_file_ops.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_geometry_nodes.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_gpencil.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_issue_31_validation.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_lighting.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_materials.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_mesh_editing.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_mesh_quality.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_mesh_repair.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_modeling.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_negative_scale_primitives.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_new_nodes.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_numeric_text_values.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_objects.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_physics.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_point_camera_at.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_print3d.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_procedural.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_prompts.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_raster.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_rendering.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_scene.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_screenshot.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_sculpting.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_selection.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_selection_respected.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_sequencer.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_shader_nodes.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_sweep.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_transforms.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_uv.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_uv_slim.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_tools/test_viewport.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_toolsets.py +0 -0
- {blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blenderwright
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: The most intuitive and efficient MCP Server for Blender
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -45,7 +45,7 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
|
|
|
45
45
|
## Key Features
|
|
46
46
|
|
|
47
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
|
|
48
|
-
- **
|
|
48
|
+
- **13 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
|
|
49
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
|
|
50
50
|
- **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
|
|
51
51
|
- **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
|
|
@@ -236,7 +236,7 @@ you have; it reports symlinks distinctly.
|
|
|
236
236
|
|
|
237
237
|
## Expert Guidance
|
|
238
238
|
|
|
239
|
-
blenderwright includes
|
|
239
|
+
blenderwright includes 13 MCP prompts that guide the LLM toward professional-quality results:
|
|
240
240
|
|
|
241
241
|
| Prompt | What It Teaches |
|
|
242
242
|
|--------|----------------|
|
|
@@ -252,6 +252,7 @@ blenderwright includes 12 MCP prompts that guide the LLM toward professional-qua
|
|
|
252
252
|
| `character_base_mesh` | Character modeling guide |
|
|
253
253
|
| `scene_cleanup` | Scene organization workflow |
|
|
254
254
|
| `animation_turntable` | Turntable animation setup |
|
|
255
|
+
| `imported_mesh_cleanup` | Import, analyze, repair, decimate, normals, scale, UVs for meshes from scans, downloads, or generators |
|
|
255
256
|
|
|
256
257
|
## Tool Domains
|
|
257
258
|
|
|
@@ -404,7 +405,7 @@ blenderwright/
|
|
|
404
405
|
│ ├── validators.py # Input validation
|
|
405
406
|
│ ├── tools/ # 28 tool modules (191 tools)
|
|
406
407
|
│ ├── resources/ # MCP resources (scene, objects, materials)
|
|
407
|
-
│ └── prompts/ #
|
|
408
|
+
│ └── prompts/ # 13 expert prompt templates
|
|
408
409
|
├── addon/ # Blender addon (zero external deps)
|
|
409
410
|
│ ├── blender_manifest.toml # Blender 4.2+ Extension manifest
|
|
410
411
|
│ ├── __init__.py # bl_info (legacy fallback) + register/unregister
|
|
@@ -27,7 +27,7 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
|
|
|
27
27
|
## Key Features
|
|
28
28
|
|
|
29
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
|
|
30
|
-
- **
|
|
30
|
+
- **13 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
|
|
31
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
|
|
32
32
|
- **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
|
|
33
33
|
- **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
|
|
@@ -218,7 +218,7 @@ you have; it reports symlinks distinctly.
|
|
|
218
218
|
|
|
219
219
|
## Expert Guidance
|
|
220
220
|
|
|
221
|
-
blenderwright includes
|
|
221
|
+
blenderwright includes 13 MCP prompts that guide the LLM toward professional-quality results:
|
|
222
222
|
|
|
223
223
|
| Prompt | What It Teaches |
|
|
224
224
|
|--------|----------------|
|
|
@@ -234,6 +234,7 @@ blenderwright includes 12 MCP prompts that guide the LLM toward professional-qua
|
|
|
234
234
|
| `character_base_mesh` | Character modeling guide |
|
|
235
235
|
| `scene_cleanup` | Scene organization workflow |
|
|
236
236
|
| `animation_turntable` | Turntable animation setup |
|
|
237
|
+
| `imported_mesh_cleanup` | Import, analyze, repair, decimate, normals, scale, UVs for meshes from scans, downloads, or generators |
|
|
237
238
|
|
|
238
239
|
## Tool Domains
|
|
239
240
|
|
|
@@ -386,7 +387,7 @@ blenderwright/
|
|
|
386
387
|
│ ├── validators.py # Input validation
|
|
387
388
|
│ ├── tools/ # 28 tool modules (191 tools)
|
|
388
389
|
│ ├── resources/ # MCP resources (scene, objects, materials)
|
|
389
|
-
│ └── prompts/ #
|
|
390
|
+
│ └── prompts/ # 13 expert prompt templates
|
|
390
391
|
├── addon/ # Blender addon (zero external deps)
|
|
391
392
|
│ ├── blender_manifest.toml # Blender 4.2+ Extension manifest
|
|
392
393
|
│ ├── __init__.py # bl_info (legacy fallback) + register/unregister
|
|
@@ -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, 3, 0),
|
|
12
12
|
"blender": (4, 2, 0),
|
|
13
13
|
"location": "View3D > Sidebar > blenderwright",
|
|
14
14
|
"description": "MCP Server integration for AI-assisted 3D workflows",
|
|
@@ -55,9 +55,19 @@ def handle_assign_material(params: dict) -> dict:
|
|
|
55
55
|
obj.active_material_index = i
|
|
56
56
|
return {"object": obj.name, "material": mat.name, "action": "already_assigned", "slot": i}
|
|
57
57
|
|
|
58
|
+
# Fill the first empty slot before adding one. A boolean modifier
|
|
59
|
+
# leaves an empty slot 0 that every face points at; appending puts
|
|
60
|
+
# the material in slot 1 and the object still renders grey.
|
|
61
|
+
for i, slot_mat in enumerate(obj.data.materials):
|
|
62
|
+
if slot_mat is None:
|
|
63
|
+
obj.data.materials[i] = mat
|
|
64
|
+
obj.active_material_index = i
|
|
65
|
+
return {"object": obj.name, "material": mat.name, "action": "assigned", "slot": i}
|
|
66
|
+
|
|
58
67
|
obj.data.materials.append(mat)
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
slot = len(obj.data.materials) - 1
|
|
69
|
+
obj.active_material_index = slot
|
|
70
|
+
return {"object": obj.name, "material": mat.name, "action": "assigned", "slot": slot}
|
|
61
71
|
except Exception as e:
|
|
62
72
|
raise RuntimeError(f"Failed to assign material: {e}")
|
|
63
73
|
|
|
@@ -97,10 +97,8 @@ def handle_repair_mesh(params: dict) -> dict:
|
|
|
97
97
|
|
|
98
98
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
99
99
|
try:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
bpy.ops.mesh.delete_loose()
|
|
103
|
-
applied.append("remove_loose")
|
|
100
|
+
# Dissolving degenerate faces and filling holes can both leave
|
|
101
|
+
# vertices with no faces behind, so loose removal runs last.
|
|
104
102
|
if params.get("dissolve_degenerate", True):
|
|
105
103
|
bpy.ops.mesh.select_all(action="SELECT")
|
|
106
104
|
bpy.ops.mesh.dissolve_degenerate()
|
|
@@ -109,6 +107,10 @@ def handle_repair_mesh(params: dict) -> dict:
|
|
|
109
107
|
bpy.ops.mesh.select_all(action="SELECT")
|
|
110
108
|
bpy.ops.mesh.fill_holes(sides=0)
|
|
111
109
|
applied.append("fill_holes")
|
|
110
|
+
if params.get("remove_loose", True):
|
|
111
|
+
bpy.ops.mesh.select_all(action="SELECT")
|
|
112
|
+
bpy.ops.mesh.delete_loose()
|
|
113
|
+
applied.append("remove_loose")
|
|
112
114
|
finally:
|
|
113
115
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
114
116
|
|
|
@@ -39,6 +39,21 @@ def _get_primitive_op(ptype: str):
|
|
|
39
39
|
return ops_map.get(ptype)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def _bake_scale(obj, scale) -> None:
|
|
43
|
+
"""Apply a creation scale the way primitive_cube_add does: into the mesh.
|
|
44
|
+
|
|
45
|
+
Mesh data is transformed so the object keeps unit scale and modifiers
|
|
46
|
+
see the real size. An empty has no mesh, so it gets object scale.
|
|
47
|
+
"""
|
|
48
|
+
sx, sy, sz = (float(v) for v in scale)
|
|
49
|
+
if (sx, sy, sz) == (1.0, 1.0, 1.0):
|
|
50
|
+
return
|
|
51
|
+
if obj.data is not None and hasattr(obj.data, "transform"):
|
|
52
|
+
obj.data.transform(mathutils.Matrix.Diagonal((sx, sy, sz, 1)))
|
|
53
|
+
else:
|
|
54
|
+
obj.scale = (sx, sy, sz)
|
|
55
|
+
|
|
56
|
+
|
|
42
57
|
def _extent(obj) -> dict:
|
|
43
58
|
"""The object's size and where it ends, in world space.
|
|
44
59
|
|
|
@@ -77,9 +92,14 @@ def handle_create_object(params: dict) -> dict:
|
|
|
77
92
|
op_func = _get_primitive_op(obj_type)
|
|
78
93
|
if op_func is None:
|
|
79
94
|
raise ValueError(f"Unknown object type: {obj_type}")
|
|
80
|
-
|
|
95
|
+
# Created at unit scale on purpose: the scale is baked below.
|
|
96
|
+
# Blender 5.1's primitive_plane_add and primitive_circle_add
|
|
97
|
+
# silently ignore their scale argument while primitive_cube_add
|
|
98
|
+
# bakes it into the mesh, so the op cannot be trusted with it.
|
|
99
|
+
op_func(location=location, rotation=rotation)
|
|
81
100
|
|
|
82
101
|
obj = bpy.context.active_object
|
|
102
|
+
_bake_scale(obj, scale)
|
|
83
103
|
if name:
|
|
84
104
|
obj.name = name
|
|
85
105
|
if obj.data:
|
|
@@ -328,3 +328,54 @@ def auto_critique_workflow() -> str:
|
|
|
328
328
|
"end of the sequence, not after every individual step.\n"
|
|
329
329
|
"- If the user asks you to skip screenshots, respect that preference.\n"
|
|
330
330
|
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@mcp.prompt()
|
|
334
|
+
def imported_mesh_cleanup() -> str:
|
|
335
|
+
"""Take a mesh that did not originate in Blender from import to clean, usable geometry."""
|
|
336
|
+
return (
|
|
337
|
+
"## Imported Mesh Cleanup Workflow\n\n"
|
|
338
|
+
"Meshes that come from outside Blender (photogrammetry scans, marketplace "
|
|
339
|
+
"downloads, sculpt exports, CAD conversions, AI-generated models) usually "
|
|
340
|
+
"carry defects: split vertices along every seam, loose vertices, zero-area "
|
|
341
|
+
"faces, open boundaries, inverted normals, far too many triangles, and no "
|
|
342
|
+
"usable UVs. Measure before you fix, and fix only what the report calls for.\n\n"
|
|
343
|
+
"1. Import with `import_file(filepath)`. Use the names in `imported_objects` "
|
|
344
|
+
"for every step below.\n"
|
|
345
|
+
"2. Run `analyze_mesh_quality(object_name)` and read the report before touching "
|
|
346
|
+
"anything. `issues_found` says whether steps 3 to 5 are needed at all.\n"
|
|
347
|
+
"3. If `duplicate_vertex_count` is non-zero, run `merge_vertices(object_name)` "
|
|
348
|
+
"with the default threshold FIRST. GLB, FBX and OBJ exporters split vertices "
|
|
349
|
+
"wherever UVs or normals change, so a clean mesh can arrive with thousands of "
|
|
350
|
+
"duplicates and a `non_manifold_edge_count` that is mostly false. Merging "
|
|
351
|
+
"collapses them and makes the non-manifold count truthful.\n"
|
|
352
|
+
"4. Run `repair_mesh(object_name)` with the defaults (remove_loose and "
|
|
353
|
+
"dissolve_degenerate). These cannot damage a well-formed mesh. Leave "
|
|
354
|
+
"`fill_holes` off unless `non_manifold_edge_count` is still high after merging "
|
|
355
|
+
"AND the model is meant to be watertight (3D print, boolean operand, fluid "
|
|
356
|
+
"domain). Filling holes invents geometry, so check the result visually.\n"
|
|
357
|
+
"5. Run `analyze_mesh_quality` again and compare the counts. If "
|
|
358
|
+
"`non_manifold_edge_count` is still non-zero on a model that only needs to "
|
|
359
|
+
"render, stop here; open boundaries are fine for rendering.\n"
|
|
360
|
+
"6. Check density from `face_count`. Scans and generated meshes are often "
|
|
361
|
+
"500k+ triangles. If the face count is far beyond what the use needs, run "
|
|
362
|
+
"`decimate_mesh(object_name, ratio)` (0.1 to 0.3 for a scan, 0.5 for a mild "
|
|
363
|
+
"reduction). For a clean quad surface instead of collapsed triangles, use "
|
|
364
|
+
"`remesh` with a voxel size appropriate to the model's scale.\n"
|
|
365
|
+
"7. Run `recalculate_normals(object_name)`. Imported meshes frequently have "
|
|
366
|
+
"inconsistent winding, which shows as black patches in the viewport.\n"
|
|
367
|
+
"8. Check scale and transforms with `get_object_info`. Many exporters land a "
|
|
368
|
+
"mesh with a 100x or 0.01x object scale; compare `dimensions` against "
|
|
369
|
+
"`scale_reference_guide`, fix with `set_scale`, then `apply_transforms` so the "
|
|
370
|
+
"mesh data carries the real size. Use `set_origin` if the pivot is off.\n"
|
|
371
|
+
"9. Run `shade_auto_smooth(object_name)` so hard edges stay crisp and curved "
|
|
372
|
+
"surfaces look smooth.\n"
|
|
373
|
+
"10. If the model has no UVs or they are unusable, run "
|
|
374
|
+
"`smart_uv_project(object_name)` before building materials.\n"
|
|
375
|
+
"11. Run `focus_on_object(object_name)` so the viewport actually frames it "
|
|
376
|
+
"(the view may be left far away after a scale fix), then capture "
|
|
377
|
+
"`get_viewport_screenshot(mode='fast')` and critique the result (see "
|
|
378
|
+
"auto_critique_workflow). Look for holes, shading artifacts, and proportions.\n\n"
|
|
379
|
+
"Only after this pass should you retopologize, add modifiers, or build "
|
|
380
|
+
"materials on the mesh. Working on dirty geometry compounds every later problem."
|
|
381
|
+
)
|
{blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_materials_handler.py
RENAMED
|
@@ -481,3 +481,67 @@ class TestRegistration:
|
|
|
481
481
|
"set_color_ramp_interpolation",
|
|
482
482
|
"get_color_ramp",
|
|
483
483
|
} <= registered
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
# assign_material fills an empty slot before adding one
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class TestAssignMaterialSlots:
|
|
492
|
+
"""An object that has been through a boolean modifier carries an empty
|
|
493
|
+
material slot 0. Appending put the material in slot 1, every face still
|
|
494
|
+
pointed at slot 0, and the wall rendered grey. The first empty slot is
|
|
495
|
+
filled instead; a new slot is only added when none is empty."""
|
|
496
|
+
|
|
497
|
+
def _obj(self, slots):
|
|
498
|
+
obj = MagicMock()
|
|
499
|
+
obj.name = "Wall"
|
|
500
|
+
obj.data.materials = list(slots)
|
|
501
|
+
return obj
|
|
502
|
+
|
|
503
|
+
def _mat(self, name):
|
|
504
|
+
mat = MagicMock()
|
|
505
|
+
mat.name = name
|
|
506
|
+
return mat
|
|
507
|
+
|
|
508
|
+
def _patch(self, monkeypatch, obj, mat):
|
|
509
|
+
import bpy
|
|
510
|
+
monkeypatch.setattr(bpy.data.objects, "get", lambda n: obj)
|
|
511
|
+
monkeypatch.setattr(bpy.data.materials, "get", lambda n: mat)
|
|
512
|
+
|
|
513
|
+
def test_fills_the_empty_slot(self, mh, monkeypatch):
|
|
514
|
+
sage = self._mat("Wall_Sage")
|
|
515
|
+
obj = self._obj([None])
|
|
516
|
+
self._patch(monkeypatch, obj, sage)
|
|
517
|
+
result = mh.handle_assign_material({"object_name": "Wall", "material_name": "Wall_Sage"})
|
|
518
|
+
assert obj.data.materials == [sage]
|
|
519
|
+
assert result["slot"] == 0
|
|
520
|
+
assert result["action"] == "assigned"
|
|
521
|
+
|
|
522
|
+
def test_fills_the_first_empty_slot_only(self, mh, monkeypatch):
|
|
523
|
+
sage = self._mat("Wall_Sage")
|
|
524
|
+
other = self._mat("Other")
|
|
525
|
+
obj = self._obj([other, None, None])
|
|
526
|
+
self._patch(monkeypatch, obj, sage)
|
|
527
|
+
result = mh.handle_assign_material({"object_name": "Wall", "material_name": "Wall_Sage"})
|
|
528
|
+
assert obj.data.materials == [other, sage, None]
|
|
529
|
+
assert result["slot"] == 1
|
|
530
|
+
|
|
531
|
+
def test_appends_when_no_slot_is_empty(self, mh, monkeypatch):
|
|
532
|
+
sage = self._mat("Wall_Sage")
|
|
533
|
+
other = self._mat("Other")
|
|
534
|
+
obj = self._obj([other])
|
|
535
|
+
self._patch(monkeypatch, obj, sage)
|
|
536
|
+
result = mh.handle_assign_material({"object_name": "Wall", "material_name": "Wall_Sage"})
|
|
537
|
+
assert obj.data.materials == [other, sage]
|
|
538
|
+
assert result["slot"] == 1
|
|
539
|
+
|
|
540
|
+
def test_already_assigned_is_reported(self, mh, monkeypatch):
|
|
541
|
+
sage = self._mat("Wall_Sage")
|
|
542
|
+
obj = self._obj([None, sage])
|
|
543
|
+
self._patch(monkeypatch, obj, sage)
|
|
544
|
+
result = mh.handle_assign_material({"object_name": "Wall", "material_name": "Wall_Sage"})
|
|
545
|
+
assert result["action"] == "already_assigned"
|
|
546
|
+
assert result["slot"] == 1
|
|
547
|
+
assert obj.data.materials == [None, sage]
|
|
@@ -437,3 +437,49 @@ class TestAnalyzeMeshQualityErrors:
|
|
|
437
437
|
|
|
438
438
|
with pytest.raises(ValueError, match="not a mesh"):
|
|
439
439
|
mod.handle_analyze_mesh_quality({"object_name": "Camera"})
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class TestRepairMeshOrder:
|
|
443
|
+
"""Dissolving degenerate faces can orphan vertices, so loose removal must run after it."""
|
|
444
|
+
|
|
445
|
+
def _run_repair(self, mod, params):
|
|
446
|
+
import bpy
|
|
447
|
+
mock_obj = MagicMock()
|
|
448
|
+
mock_obj.name = "Cube"
|
|
449
|
+
mock_obj.type = "MESH"
|
|
450
|
+
mock_obj.mode = "OBJECT"
|
|
451
|
+
mock_obj.data.vertices = []
|
|
452
|
+
mock_obj.data.edges = []
|
|
453
|
+
mock_obj.data.polygons = []
|
|
454
|
+
bpy.data.objects.get.return_value = mock_obj
|
|
455
|
+
bpy.ops.mesh.reset_mock()
|
|
456
|
+
return mod.handle_repair_mesh(params)
|
|
457
|
+
|
|
458
|
+
def test_dissolve_degenerate_runs_before_delete_loose(self, handler_and_bmesh):
|
|
459
|
+
mod, _ = handler_and_bmesh
|
|
460
|
+
import bpy
|
|
461
|
+
|
|
462
|
+
self._run_repair(mod, {"object_name": "Cube"})
|
|
463
|
+
|
|
464
|
+
names = [c[0] for c in bpy.ops.mesh.mock_calls]
|
|
465
|
+
assert "dissolve_degenerate" in names
|
|
466
|
+
assert "delete_loose" in names
|
|
467
|
+
assert names.index("dissolve_degenerate") < names.index("delete_loose")
|
|
468
|
+
|
|
469
|
+
def test_fill_holes_runs_before_delete_loose(self, handler_and_bmesh):
|
|
470
|
+
mod, _ = handler_and_bmesh
|
|
471
|
+
import bpy
|
|
472
|
+
|
|
473
|
+
self._run_repair(mod, {"object_name": "Cube", "fill_holes": True})
|
|
474
|
+
|
|
475
|
+
names = [c[0] for c in bpy.ops.mesh.mock_calls]
|
|
476
|
+
assert names.index("fill_holes") < names.index("delete_loose")
|
|
477
|
+
|
|
478
|
+
def test_repairs_applied_reports_every_enabled_step(self, handler_and_bmesh):
|
|
479
|
+
mod, _ = handler_and_bmesh
|
|
480
|
+
|
|
481
|
+
result = self._run_repair(mod, {"object_name": "Cube", "fill_holes": True})
|
|
482
|
+
|
|
483
|
+
assert set(result["repairs_applied"]) == {
|
|
484
|
+
"remove_loose", "dissolve_degenerate", "fill_holes"
|
|
485
|
+
}
|
{blenderwright-2.2.0 → blenderwright-2.3.0}/tests/test_addon/test_handlers/test_objects_handler.py
RENAMED
|
@@ -544,3 +544,65 @@ class TestCreateReturnsExtent:
|
|
|
544
544
|
result = objects_handler.handle_create_object({"type": "CUBE"})
|
|
545
545
|
assert result["name"] == "Fuselage"
|
|
546
546
|
assert "bounds" not in result
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ---------------------------------------------------------------------------
|
|
550
|
+
# create_object bakes the scale itself
|
|
551
|
+
# ---------------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
class TestCreateBakesScale:
|
|
555
|
+
"""Blender 5.1's primitive_plane_add and primitive_circle_add silently
|
|
556
|
+
ignore their scale argument, while primitive_cube_add bakes it into the
|
|
557
|
+
mesh. A hallway floor asked for at scale (3, 4.5, 1) came back 2m square.
|
|
558
|
+
The handler now creates every primitive at unit scale and bakes the scale
|
|
559
|
+
into the mesh itself, so every type behaves the way the cube always did."""
|
|
560
|
+
|
|
561
|
+
def _obj(self, bpy):
|
|
562
|
+
obj = MagicMock()
|
|
563
|
+
obj.name = "Floor"
|
|
564
|
+
obj.type = "MESH"
|
|
565
|
+
obj.location = [0, 0, 0]
|
|
566
|
+
obj.dimensions = [6.0, 9.0, 0.0]
|
|
567
|
+
obj.bound_box = None
|
|
568
|
+
for op in ("primitive_plane_add", "primitive_cube_add", "primitive_circle_add"):
|
|
569
|
+
getattr(bpy.ops.mesh, op).side_effect = None
|
|
570
|
+
bpy.context.active_object = obj
|
|
571
|
+
return obj
|
|
572
|
+
|
|
573
|
+
def test_plane_scale_is_baked_into_the_mesh(self, objects_handler):
|
|
574
|
+
import bpy
|
|
575
|
+
import mathutils
|
|
576
|
+
bpy.reset_mock()
|
|
577
|
+
mathutils.Matrix.reset_mock()
|
|
578
|
+
obj = self._obj(bpy)
|
|
579
|
+
objects_handler.handle_create_object({
|
|
580
|
+
"type": "PLANE", "location": (0, 4.5, 0), "scale": (3, 4.5, 1),
|
|
581
|
+
})
|
|
582
|
+
mathutils.Matrix.Diagonal.assert_called_once_with((3, 4.5, 1, 1))
|
|
583
|
+
obj.data.transform.assert_called_once_with(mathutils.Matrix.Diagonal.return_value)
|
|
584
|
+
|
|
585
|
+
def test_the_op_is_not_asked_to_scale(self, objects_handler):
|
|
586
|
+
"""Passing scale to the op as well would double it on the cube."""
|
|
587
|
+
import bpy
|
|
588
|
+
bpy.reset_mock()
|
|
589
|
+
self._obj(bpy)
|
|
590
|
+
objects_handler.handle_create_object({"type": "CUBE", "scale": (2, 2, 2)})
|
|
591
|
+
assert "scale" not in bpy.ops.mesh.primitive_cube_add.call_args.kwargs
|
|
592
|
+
|
|
593
|
+
def test_unit_scale_does_not_touch_the_mesh(self, objects_handler):
|
|
594
|
+
import bpy
|
|
595
|
+
bpy.reset_mock()
|
|
596
|
+
obj = self._obj(bpy)
|
|
597
|
+
objects_handler.handle_create_object({"type": "CIRCLE", "scale": (1, 1, 1)})
|
|
598
|
+
obj.data.transform.assert_not_called()
|
|
599
|
+
|
|
600
|
+
def test_empty_gets_object_scale(self, objects_handler):
|
|
601
|
+
"""An empty has no mesh to bake into, so its object scale is set."""
|
|
602
|
+
import bpy
|
|
603
|
+
bpy.reset_mock()
|
|
604
|
+
obj = self._obj(bpy)
|
|
605
|
+
obj.type = "EMPTY"
|
|
606
|
+
obj.data = None
|
|
607
|
+
objects_handler.handle_create_object({"type": "EMPTY", "scale": (2, 3, 4)})
|
|
608
|
+
assert tuple(obj.scale) == (2, 3, 4)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Unit tests for the imported mesh cleanup workflow prompt."""
|
|
2
|
+
|
|
3
|
+
from blenderwright.prompts.workflows import imported_mesh_cleanup
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestImportedMeshCleanupPrompt:
|
|
7
|
+
def test_returns_non_empty_string(self):
|
|
8
|
+
result = imported_mesh_cleanup()
|
|
9
|
+
assert isinstance(result, str)
|
|
10
|
+
assert len(result) > 0
|
|
11
|
+
|
|
12
|
+
def test_is_vendor_neutral(self):
|
|
13
|
+
result = imported_mesh_cleanup().lower()
|
|
14
|
+
for vendor in ("tripo", "meshy", "sketchfab", "luma", "rodin"):
|
|
15
|
+
assert vendor not in result
|
|
16
|
+
|
|
17
|
+
def test_chains_import_analyze_repair(self):
|
|
18
|
+
result = imported_mesh_cleanup()
|
|
19
|
+
assert "import_file" in result
|
|
20
|
+
assert "analyze_mesh_quality" in result
|
|
21
|
+
assert "repair_mesh" in result
|
|
22
|
+
assert result.index("import_file") < result.index("analyze_mesh_quality")
|
|
23
|
+
assert result.index("analyze_mesh_quality") < result.index("repair_mesh")
|
|
24
|
+
|
|
25
|
+
def test_merges_duplicates_before_repair(self):
|
|
26
|
+
result = imported_mesh_cleanup()
|
|
27
|
+
assert "merge_vertices" in result
|
|
28
|
+
assert "duplicate_vertex_count" in result
|
|
29
|
+
assert result.index("merge_vertices") < result.index("repair_mesh")
|
|
30
|
+
|
|
31
|
+
def test_fill_holes_is_called_out_as_opt_in(self):
|
|
32
|
+
result = imported_mesh_cleanup()
|
|
33
|
+
assert "fill_holes" in result
|
|
34
|
+
assert "non_manifold_edge_count" in result
|
|
35
|
+
|
|
36
|
+
def test_covers_density_and_normals(self):
|
|
37
|
+
result = imported_mesh_cleanup()
|
|
38
|
+
assert "decimate_mesh" in result
|
|
39
|
+
assert "recalculate_normals" in result
|
|
40
|
+
|
|
41
|
+
def test_covers_transforms_and_scale(self):
|
|
42
|
+
result = imported_mesh_cleanup()
|
|
43
|
+
assert "apply_transforms" in result
|
|
44
|
+
assert "scale_reference_guide" in result
|
|
45
|
+
|
|
46
|
+
def test_covers_shading_and_uvs(self):
|
|
47
|
+
result = imported_mesh_cleanup()
|
|
48
|
+
assert "shade_auto_smooth" in result
|
|
49
|
+
assert "smart_uv_project" in result
|
|
50
|
+
|
|
51
|
+
def test_names_common_sources(self):
|
|
52
|
+
result = imported_mesh_cleanup().lower()
|
|
53
|
+
assert "scan" in result
|
|
54
|
+
assert "generated" in result
|
|
55
|
+
|
|
56
|
+
def test_reanalyzes_after_repair(self):
|
|
57
|
+
result = imported_mesh_cleanup()
|
|
58
|
+
assert result.count("analyze_mesh_quality") >= 2
|
|
59
|
+
|
|
60
|
+
def test_ends_with_visual_check(self):
|
|
61
|
+
result = imported_mesh_cleanup()
|
|
62
|
+
assert "get_viewport_screenshot" in result
|
|
63
|
+
|
|
64
|
+
def test_frames_object_before_screenshot(self):
|
|
65
|
+
result = imported_mesh_cleanup()
|
|
66
|
+
assert "focus_on_object" in result
|
|
67
|
+
assert result.index("focus_on_object") < result.index("get_viewport_screenshot")
|
|
68
|
+
|
|
69
|
+
def test_uses_import_result_names(self):
|
|
70
|
+
result = imported_mesh_cleanup()
|
|
71
|
+
assert "imported_objects" in result
|
|
@@ -51,7 +51,7 @@ wheels = [
|
|
|
51
51
|
|
|
52
52
|
[[package]]
|
|
53
53
|
name = "blenderwright"
|
|
54
|
-
version = "2.
|
|
54
|
+
version = "2.3.0"
|
|
55
55
|
source = { editable = "." }
|
|
56
56
|
dependencies = [
|
|
57
57
|
{ name = "mcp" },
|
|
@@ -99,11 +99,11 @@ dev = [
|
|
|
99
99
|
|
|
100
100
|
[[package]]
|
|
101
101
|
name = "certifi"
|
|
102
|
-
version = "2026.
|
|
102
|
+
version = "2026.10.19"
|
|
103
103
|
source = { registry = "https://pypi.org/simple" }
|
|
104
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
104
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
|
|
105
105
|
wheels = [
|
|
106
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
106
|
+
{ url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
|
|
107
107
|
]
|
|
108
108
|
|
|
109
109
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|