code refactoring: removed create, delete, modify functions
This commit is contained in:
parent
9b3b327853
commit
e0e8095892
267
addon.py
267
addon.py
@ -1,3 +1,5 @@
|
||||
# Code created by Siddharth Ahuja: www.github.com/ahujasid © 2025
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
import json
|
||||
@ -14,7 +16,7 @@ from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
|
||||
bl_info = {
|
||||
"name": "Blender MCP",
|
||||
"author": "BlenderMCP",
|
||||
"version": (0, 2),
|
||||
"version": (1, 2),
|
||||
"blender": (3, 0, 0),
|
||||
"location": "View3D > Sidebar > BlenderMCP",
|
||||
"description": "Connect Blender to Claude via MCP",
|
||||
@ -172,18 +174,8 @@ class BlenderMCPServer:
|
||||
|
||||
def execute_command(self, command):
|
||||
"""Execute a command in the main Blender thread"""
|
||||
try:
|
||||
cmd_type = command.get("type")
|
||||
params = command.get("params", {})
|
||||
|
||||
# Ensure we're in the right context
|
||||
if cmd_type in ["create_object", "modify_object", "delete_object"]:
|
||||
override = bpy.context.copy()
|
||||
override['area'] = [area for area in bpy.context.screen.areas if area.type == 'VIEW_3D'][0]
|
||||
with bpy.context.temp_override(**override):
|
||||
return self._execute_command_internal(command)
|
||||
else:
|
||||
return self._execute_command_internal(command)
|
||||
try:
|
||||
return self._execute_command_internal(command)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error executing command: {str(e)}")
|
||||
@ -202,12 +194,8 @@ class BlenderMCPServer:
|
||||
# Base handlers that are always available
|
||||
handlers = {
|
||||
"get_scene_info": self.get_scene_info,
|
||||
"create_object": self.create_object,
|
||||
"modify_object": self.modify_object,
|
||||
"delete_object": self.delete_object,
|
||||
"get_object_info": self.get_object_info,
|
||||
"execute_code": self.execute_code,
|
||||
"set_material": self.set_material,
|
||||
"get_polyhaven_status": self.get_polyhaven_status,
|
||||
"get_hyper3d_status": self.get_hyper3d_status,
|
||||
}
|
||||
@ -246,13 +234,6 @@ class BlenderMCPServer:
|
||||
return {"status": "error", "message": f"Unknown command type: {cmd_type}"}
|
||||
|
||||
|
||||
def get_simple_info(self):
|
||||
"""Get basic Blender information"""
|
||||
return {
|
||||
"blender_version": ".".join(str(v) for v in bpy.app.version),
|
||||
"scene_name": bpy.context.scene.name,
|
||||
"object_count": len(bpy.context.scene.objects)
|
||||
}
|
||||
|
||||
def get_scene_info(self):
|
||||
"""Get information about the current Blender scene"""
|
||||
@ -308,140 +289,7 @@ class BlenderMCPServer:
|
||||
[*min_corner], [*max_corner]
|
||||
]
|
||||
|
||||
def create_object(self, type="CUBE", name=None, location=(0, 0, 0), rotation=(0, 0, 0), scale=(1, 1, 1),
|
||||
align="WORLD", major_segments=48, minor_segments=12, mode="MAJOR_MINOR",
|
||||
major_radius=1.0, minor_radius=0.25, abso_major_rad=1.25, abso_minor_rad=0.75, generate_uvs=True):
|
||||
"""Create a new object in the scene"""
|
||||
try:
|
||||
# Deselect all objects first
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
# Create the object based on type
|
||||
if type == "CUBE":
|
||||
bpy.ops.mesh.primitive_cube_add(location=location, rotation=rotation, scale=scale)
|
||||
elif type == "SPHERE":
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(location=location, rotation=rotation, scale=scale)
|
||||
elif type == "CYLINDER":
|
||||
bpy.ops.mesh.primitive_cylinder_add(location=location, rotation=rotation, scale=scale)
|
||||
elif type == "PLANE":
|
||||
bpy.ops.mesh.primitive_plane_add(location=location, rotation=rotation, scale=scale)
|
||||
elif type == "CONE":
|
||||
bpy.ops.mesh.primitive_cone_add(location=location, rotation=rotation, scale=scale)
|
||||
elif type == "TORUS":
|
||||
bpy.ops.mesh.primitive_torus_add(
|
||||
align=align,
|
||||
location=location,
|
||||
rotation=rotation,
|
||||
major_segments=major_segments,
|
||||
minor_segments=minor_segments,
|
||||
mode=mode,
|
||||
major_radius=major_radius,
|
||||
minor_radius=minor_radius,
|
||||
abso_major_rad=abso_major_rad,
|
||||
abso_minor_rad=abso_minor_rad,
|
||||
generate_uvs=generate_uvs
|
||||
)
|
||||
elif type == "EMPTY":
|
||||
bpy.ops.object.empty_add(location=location, rotation=rotation, scale=scale)
|
||||
elif type == "CAMERA":
|
||||
bpy.ops.object.camera_add(location=location, rotation=rotation)
|
||||
elif type == "LIGHT":
|
||||
bpy.ops.object.light_add(type='POINT', location=location, rotation=rotation, scale=scale)
|
||||
else:
|
||||
raise ValueError(f"Unsupported object type: {type}")
|
||||
|
||||
# Force update the view layer
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# Get the active object (which should be our newly created object)
|
||||
obj = bpy.context.view_layer.objects.active
|
||||
|
||||
# If we don't have an active object, something went wrong
|
||||
if obj is None:
|
||||
raise RuntimeError("Failed to create object - no active object")
|
||||
|
||||
# Make sure it's selected
|
||||
obj.select_set(True)
|
||||
|
||||
# Rename if name is provided
|
||||
if name:
|
||||
obj.name = name
|
||||
if obj.data:
|
||||
obj.data.name = name
|
||||
|
||||
# Patch for PLANE: scale don't work with bpy.ops.mesh.primitive_plane_add()
|
||||
if type in {"PLANE"}:
|
||||
obj.scale = scale
|
||||
|
||||
# Return the object info
|
||||
result = {
|
||||
"name": obj.name,
|
||||
"type": obj.type,
|
||||
"location": [obj.location.x, obj.location.y, obj.location.z],
|
||||
"rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z],
|
||||
"scale": [obj.scale.x, obj.scale.y, obj.scale.z],
|
||||
}
|
||||
|
||||
if obj.type == "MESH":
|
||||
bounding_box = self._get_aabb(obj)
|
||||
result["world_bounding_box"] = bounding_box
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"Error in create_object: {str(e)}")
|
||||
traceback.print_exc()
|
||||
return {"error": str(e)}
|
||||
|
||||
def modify_object(self, name, location=None, rotation=None, scale=None, visible=None):
|
||||
"""Modify an existing object in the scene"""
|
||||
# Find the object by name
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object not found: {name}")
|
||||
|
||||
# Modify properties as requested
|
||||
if location is not None:
|
||||
obj.location = location
|
||||
|
||||
if rotation is not None:
|
||||
obj.rotation_euler = rotation
|
||||
|
||||
if scale is not None:
|
||||
obj.scale = scale
|
||||
|
||||
if visible is not None:
|
||||
obj.hide_viewport = not visible
|
||||
obj.hide_render = not visible
|
||||
|
||||
result = {
|
||||
"name": obj.name,
|
||||
"type": obj.type,
|
||||
"location": [obj.location.x, obj.location.y, obj.location.z],
|
||||
"rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z],
|
||||
"scale": [obj.scale.x, obj.scale.y, obj.scale.z],
|
||||
"visible": obj.visible_get(),
|
||||
}
|
||||
|
||||
if obj.type == "MESH":
|
||||
bounding_box = self._get_aabb(obj)
|
||||
result["world_bounding_box"] = bounding_box
|
||||
|
||||
return result
|
||||
|
||||
def delete_object(self, name):
|
||||
"""Delete an object from the scene"""
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object not found: {name}")
|
||||
|
||||
# Store the name to return
|
||||
obj_name = obj.name
|
||||
|
||||
# Select and delete the object
|
||||
if obj:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
return {"deleted": obj_name}
|
||||
|
||||
def get_object_info(self, name):
|
||||
"""Get detailed information about a specific object"""
|
||||
@ -491,108 +339,7 @@ class BlenderMCPServer:
|
||||
except Exception as e:
|
||||
raise Exception(f"Code execution error: {str(e)}")
|
||||
|
||||
def set_material(self, object_name, material_name=None, create_if_missing=True, color=None):
|
||||
"""Set or create a material for an object"""
|
||||
try:
|
||||
# Get the object
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object not found: {object_name}")
|
||||
|
||||
# Make sure object can accept materials
|
||||
if not hasattr(obj, 'data') or not hasattr(obj.data, 'materials'):
|
||||
raise ValueError(f"Object {object_name} cannot accept materials")
|
||||
|
||||
# Create or get material
|
||||
if material_name:
|
||||
mat = bpy.data.materials.get(material_name)
|
||||
if not mat and create_if_missing:
|
||||
mat = bpy.data.materials.new(name=material_name)
|
||||
print(f"Created new material: {material_name}")
|
||||
else:
|
||||
# Generate unique material name if none provided
|
||||
mat_name = f"{object_name}_material"
|
||||
mat = bpy.data.materials.get(mat_name)
|
||||
if not mat:
|
||||
mat = bpy.data.materials.new(name=mat_name)
|
||||
material_name = mat_name
|
||||
print(f"Using material: {mat_name}")
|
||||
|
||||
# Set up material nodes if needed
|
||||
if mat:
|
||||
if not mat.use_nodes:
|
||||
mat.use_nodes = True
|
||||
|
||||
# Get or create Principled BSDF
|
||||
principled = mat.node_tree.nodes.get('Principled BSDF')
|
||||
if not principled:
|
||||
principled = mat.node_tree.nodes.new('ShaderNodeBsdfPrincipled')
|
||||
# Get or create Material Output
|
||||
output = mat.node_tree.nodes.get('Material Output')
|
||||
if not output:
|
||||
output = mat.node_tree.nodes.new('ShaderNodeOutputMaterial')
|
||||
# Link if not already linked
|
||||
if not principled.outputs[0].links:
|
||||
mat.node_tree.links.new(principled.outputs[0], output.inputs[0])
|
||||
|
||||
# Set color if provided
|
||||
if color and len(color) >= 3:
|
||||
principled.inputs['Base Color'].default_value = (
|
||||
color[0],
|
||||
color[1],
|
||||
color[2],
|
||||
1.0 if len(color) < 4 else color[3]
|
||||
)
|
||||
print(f"Set material color to {color}")
|
||||
|
||||
# Assign material to object if not already assigned
|
||||
if mat:
|
||||
if not obj.data.materials:
|
||||
obj.data.materials.append(mat)
|
||||
else:
|
||||
# Only modify first material slot
|
||||
obj.data.materials[0] = mat
|
||||
|
||||
print(f"Assigned material {mat.name} to object {object_name}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"object": object_name,
|
||||
"material": mat.name,
|
||||
"color": color if color else None
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Failed to create or find material: {material_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in set_material: {str(e)}")
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e),
|
||||
"object": object_name,
|
||||
"material": material_name if 'material_name' in locals() else None
|
||||
}
|
||||
|
||||
def render_scene(self, output_path=None, resolution_x=None, resolution_y=None):
|
||||
"""Render the current scene"""
|
||||
if resolution_x is not None:
|
||||
bpy.context.scene.render.resolution_x = resolution_x
|
||||
|
||||
if resolution_y is not None:
|
||||
bpy.context.scene.render.resolution_y = resolution_y
|
||||
|
||||
if output_path:
|
||||
bpy.context.scene.render.filepath = output_path
|
||||
|
||||
# Render the scene
|
||||
bpy.ops.render.render(write_still=bool(output_path))
|
||||
|
||||
return {
|
||||
"rendered": True,
|
||||
"output_path": output_path if output_path else "[not saved]",
|
||||
"resolution": [bpy.context.scene.render.resolution_x, bpy.context.scene.render.resolution_y],
|
||||
}
|
||||
|
||||
def get_polyhaven_categories(self, asset_type):
|
||||
"""Get categories for a specific asset type from Polyhaven"""
|
||||
@ -1630,9 +1377,9 @@ class BLENDERMCP_PT_Panel(bpy.types.Panel):
|
||||
layout.operator("blendermcp.set_hyper3d_free_trial_api_key", text="Set Free Trial API Key")
|
||||
|
||||
if not scene.blendermcp_server_running:
|
||||
layout.operator("blendermcp.start_server", text="Start MCP Server")
|
||||
layout.operator("blendermcp.start_server", text="Connect to MCP server")
|
||||
else:
|
||||
layout.operator("blendermcp.stop_server", text="Stop MCP Server")
|
||||
layout.operator("blendermcp.stop_server", text="Disconnect from MCP server")
|
||||
layout.label(text=f"Running on port {scene.blendermcp_port}")
|
||||
|
||||
# Operator to set Hyper3D API Key
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "blender-mcp"
|
||||
version = "1.1.1"
|
||||
version = "1.1.3"
|
||||
description = "Blender integration through the Model Context Protocol"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@ -268,186 +268,10 @@ def get_object_info(ctx: Context, object_name: str) -> str:
|
||||
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_object(
|
||||
ctx: Context,
|
||||
type: str = "CUBE",
|
||||
name: str = None,
|
||||
location: List[float] = None,
|
||||
rotation: List[float] = None,
|
||||
scale: List[float] = None,
|
||||
# Torus-specific parameters
|
||||
align: str = "WORLD",
|
||||
major_segments: int = 48,
|
||||
minor_segments: int = 12,
|
||||
mode: str = "MAJOR_MINOR",
|
||||
major_radius: float = 1.0,
|
||||
minor_radius: float = 0.25,
|
||||
abso_major_rad: float = 1.25,
|
||||
abso_minor_rad: float = 0.75,
|
||||
generate_uvs: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Create a new object in the Blender scene.
|
||||
|
||||
Parameters:
|
||||
- type: Object type (CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT)
|
||||
- name: Optional name for the object
|
||||
- location: Optional [x, y, z] location coordinates
|
||||
- rotation: Optional [x, y, z] rotation in radians
|
||||
- scale: Optional [x, y, z] scale factors (not used for TORUS)
|
||||
|
||||
Torus-specific parameters (only used when type == "TORUS"):
|
||||
- align: How to align the torus ('WORLD', 'VIEW', or 'CURSOR')
|
||||
- major_segments: Number of segments for the main ring
|
||||
- minor_segments: Number of segments for the cross-section
|
||||
- mode: Dimension mode ('MAJOR_MINOR' or 'EXT_INT')
|
||||
- major_radius: Radius from the origin to the center of the cross sections
|
||||
- minor_radius: Radius of the torus' cross section
|
||||
- abso_major_rad: Total exterior radius of the torus
|
||||
- abso_minor_rad: Total interior radius of the torus
|
||||
- generate_uvs: Whether to generate a default UV map
|
||||
|
||||
Returns:
|
||||
A message indicating the created object name.
|
||||
"""
|
||||
try:
|
||||
# Get the global connection
|
||||
blender = get_blender_connection()
|
||||
|
||||
# Set default values for missing parameters
|
||||
loc = location or [0, 0, 0]
|
||||
rot = rotation or [0, 0, 0]
|
||||
sc = scale or [1, 1, 1]
|
||||
|
||||
params = {
|
||||
"type": type,
|
||||
"location": loc,
|
||||
"rotation": rot,
|
||||
}
|
||||
|
||||
if name:
|
||||
params["name"] = name
|
||||
|
||||
if type == "TORUS":
|
||||
# For torus, the scale is not used.
|
||||
params.update({
|
||||
"align": align,
|
||||
"major_segments": major_segments,
|
||||
"minor_segments": minor_segments,
|
||||
"mode": mode,
|
||||
"major_radius": major_radius,
|
||||
"minor_radius": minor_radius,
|
||||
"abso_major_rad": abso_major_rad,
|
||||
"abso_minor_rad": abso_minor_rad,
|
||||
"generate_uvs": generate_uvs
|
||||
})
|
||||
result = blender.send_command("create_object", params)
|
||||
return f"Created {type} object: {result['name']}"
|
||||
else:
|
||||
# For non-torus objects, include scale
|
||||
params["scale"] = sc
|
||||
result = blender.send_command("create_object", params)
|
||||
return f"Created {type} object: {result['name']}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating object: {str(e)}")
|
||||
return f"Error creating object: {str(e)}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def modify_object(
|
||||
ctx: Context,
|
||||
name: str,
|
||||
location: List[float] = None,
|
||||
rotation: List[float] = None,
|
||||
scale: List[float] = None,
|
||||
visible: bool = None
|
||||
) -> str:
|
||||
"""
|
||||
Modify an existing object in the Blender scene.
|
||||
|
||||
Parameters:
|
||||
- name: Name of the object to modify
|
||||
- location: Optional [x, y, z] location coordinates
|
||||
- rotation: Optional [x, y, z] rotation in radians
|
||||
- scale: Optional [x, y, z] scale factors
|
||||
- visible: Optional boolean to set visibility
|
||||
"""
|
||||
try:
|
||||
# Get the global connection
|
||||
blender = get_blender_connection()
|
||||
|
||||
params = {"name": name}
|
||||
|
||||
if location is not None:
|
||||
params["location"] = location
|
||||
if rotation is not None:
|
||||
params["rotation"] = rotation
|
||||
if scale is not None:
|
||||
params["scale"] = scale
|
||||
if visible is not None:
|
||||
params["visible"] = visible
|
||||
|
||||
result = blender.send_command("modify_object", params)
|
||||
return f"Modified object: {result['name']}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error modifying object: {str(e)}")
|
||||
return f"Error modifying object: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def delete_object(ctx: Context, name: str) -> str:
|
||||
"""
|
||||
Delete an object from the Blender scene.
|
||||
|
||||
Parameters:
|
||||
- name: Name of the object to delete
|
||||
"""
|
||||
try:
|
||||
# Get the global connection
|
||||
blender = get_blender_connection()
|
||||
|
||||
result = blender.send_command("delete_object", {"name": name})
|
||||
return f"Deleted object: {name}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting object: {str(e)}")
|
||||
return f"Error deleting object: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def set_material(
|
||||
ctx: Context,
|
||||
object_name: str,
|
||||
material_name: str = None,
|
||||
color: List[float] = None
|
||||
) -> str:
|
||||
"""
|
||||
Set or create a material for an object.
|
||||
|
||||
Parameters:
|
||||
- object_name: Name of the object to apply the material to
|
||||
- material_name: Optional name of the material to use or create
|
||||
- color: Optional [R, G, B] color values (0.0-1.0)
|
||||
"""
|
||||
try:
|
||||
# Get the global connection
|
||||
blender = get_blender_connection()
|
||||
|
||||
params = {"object_name": object_name}
|
||||
|
||||
if material_name:
|
||||
params["material_name"] = material_name
|
||||
if color:
|
||||
params["color"] = color
|
||||
|
||||
result = blender.send_command("set_material", params)
|
||||
return f"Applied material to {object_name}: {result.get('material_name', 'unknown')}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting material: {str(e)}")
|
||||
return f"Error setting material: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def execute_blender_code(ctx: Context, code: str) -> str:
|
||||
"""
|
||||
Execute arbitrary Python code in Blender.
|
||||
Execute arbitrary Python code in Blender. Make sure to do it step-by-step by breaking it into smaller chunks.
|
||||
|
||||
Parameters:
|
||||
- code: The Python code to execute
|
||||
@ -885,7 +709,7 @@ def asset_creation_strategy() -> str:
|
||||
Hyper3D Rodin is good at generating 3D models for single item.
|
||||
So don't try to:
|
||||
1. Generate the whole scene with one shot
|
||||
2. Generate ground using Rodin
|
||||
2. Generate ground using Hyper3D
|
||||
3. Generate parts of the items separately and put them together afterwards
|
||||
|
||||
Use get_hyper3d_status() to verify its status
|
||||
@ -907,21 +731,12 @@ def asset_creation_strategy() -> str:
|
||||
|
||||
You can reuse assets previous generated by running python code to duplicate the object, without creating another generation task.
|
||||
|
||||
2. If all integrations are disabled or when falling back to basic tools:
|
||||
- create_object() for basic primitives (CUBE, SPHERE, CYLINDER, etc.)
|
||||
- set_material() for basic colors and materials
|
||||
|
||||
3. When including an object into scene, ALWAYS make sure that the name of the object is meanful.
|
||||
|
||||
4. Always check the world_bounding_box for each item so that:
|
||||
3. Always check the world_bounding_box for each item so that:
|
||||
- Ensure that all objects that should not be clipping are not clipping.
|
||||
- Items have right spatial relationship.
|
||||
|
||||
5. After giving the tool location/scale/rotation information (via create_object() and modify_object()),
|
||||
double check the related object's location, scale, rotation, and world_bounding_box using get_object_info(),
|
||||
so that the object is in the desired location.
|
||||
|
||||
Only fall back to basic creation tools when:
|
||||
Only fall back to scripting when:
|
||||
- PolyHaven and Hyper3D are disabled
|
||||
- A simple primitive is explicitly requested
|
||||
- No suitable PolyHaven asset exists
|
||||
|
||||
Loading…
Reference in New Issue
Block a user