blender_addon_enhanced.py•13.3 kB
"""
Blender Addon Enhanced for VeFrank Integration
Installs as a Blender addon to handle VeFrank component generation
"""
bl_info = {
"name": "VeFrank MCP Enhanced",
"blender": (3, 0, 0),
"category": "Import-Export",
"version": (2, 0, 0),
"author": "VeFrank Team",
"description": "Enhanced MCP server integration for VeFrank automotive components",
}
import bpy
import json
import socket
import threading
import math
from pathlib import Path
from mathutils import Vector
class VeFrankServer:
"""Socket server for receiving commands from VeFrank."""
def __init__(self, host="localhost", port=9876):
self.host = host
self.port = port
self.server_socket = None
self.running = False
self.thread = None
def start(self):
"""Start the server in a separate thread."""
if not self.running:
self.running = True
self.thread = threading.Thread(target=self.run)
self.thread.daemon = True
self.thread.start()
return True
return False
def stop(self):
"""Stop the server."""
self.running = False
if self.server_socket:
self.server_socket.close()
def run(self):
"""Main server loop."""
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
self.server_socket.settimeout(1.0)
print(f"VeFrank server listening on {self.host}:{self.port}")
while self.running:
try:
client_socket, address = self.server_socket.accept()
data = client_socket.recv(8192).decode('utf-8')
if data:
response = self.handle_command(json.loads(data))
client_socket.send(json.dumps(response).encode('utf-8'))
client_socket.close()
except socket.timeout:
continue
except Exception as e:
print(f"Server error: {e}")
def handle_command(self, command):
"""Handle incoming commands."""
cmd_type = command.get("type")
params = command.get("params", {})
handlers = {
"import_obj": self.import_obj,
"apply_material": self.apply_material,
"add_connectors": self.add_connectors,
"capture_viewport": self.capture_viewport,
"get_scene_info": self.get_scene_info,
"execute_code": self.execute_code,
}
handler = handlers.get(cmd_type)
if handler:
try:
result = handler(params)
return {"status": "success", "result": result}
except Exception as e:
return {"status": "error", "message": str(e)}
else:
return {"status": "error", "message": f"Unknown command: {cmd_type}"}
def import_obj(self, params):
"""Import OBJ file into Blender."""
filepath = params.get("filepath")
name = params.get("name", "Component")
# Import OBJ
bpy.ops.import_scene.obj(filepath=filepath)
# Rename imported object
if bpy.context.selected_objects:
obj = bpy.context.selected_objects[0]
obj.name = name
# Center and scale
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
obj.location = (0, 0, 0)
# Ensure reasonable scale
max_dim = max(obj.dimensions)
if max_dim > 10:
scale_factor = 5 / max_dim
obj.scale = (scale_factor, scale_factor, scale_factor)
return {"object_name": obj.name}
return {"error": "No object imported"}
def apply_material(self, params):
"""Apply material to object."""
object_name = params.get("object_name")
material_props = params.get("material", {})
obj = bpy.data.objects.get(object_name)
if not obj:
return {"error": f"Object {object_name} not found"}
# Create material
mat_name = f"{object_name}_Material"
mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(mat_name)
mat.use_nodes = True
# Configure material nodes
nodes = mat.node_tree.nodes
nodes.clear()
# Add Principled BSDF
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
bsdf.inputs['Base Color'].default_value = material_props.get('base_color', [0.5, 0.5, 0.5, 1])
bsdf.inputs['Metallic'].default_value = material_props.get('metallic', 0.5)
bsdf.inputs['Roughness'].default_value = material_props.get('roughness', 0.5)
# Add output node
output = nodes.new(type='ShaderNodeOutputMaterial')
mat.node_tree.links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
# Apply to object
if obj.data.materials:
obj.data.materials[0] = mat
else:
obj.data.materials.append(mat)
return {"material_applied": mat_name}
def add_connectors(self, params):
"""Add connector geometry to component."""
object_name = params.get("object_name")
connectors = params.get("connectors", [])
obj = bpy.data.objects.get(object_name)
if not obj:
return {"error": f"Object {object_name} not found"}
created_connectors = []
for conn in connectors:
# Create connector mesh
conn_type = conn.get("type", "generic")
pin_count = conn.get("pins", 4)
position = conn.get("position", {"x": 0, "y": 0, "z": 0})
# Create connector geometry based on type
if conn_type in ["weatherpack", "deutsch"]:
# Rectangular connector
bpy.ops.mesh.primitive_cube_add(
size=0.5,
location=(position["x"], position["y"], position["z"])
)
connector = bpy.context.active_object
connector.scale = (0.3 * (pin_count / 4), 0.2, 0.15)
else:
# Cylindrical connector
bpy.ops.mesh.primitive_cylinder_add(
radius=0.1 * math.sqrt(pin_count / 4),
depth=0.2,
location=(position["x"], position["y"], position["z"])
)
connector = bpy.context.active_object
connector.name = f"{object_name}_conn_{len(created_connectors)}"
# Parent to main object
connector.parent = obj
# Apply dark material for connectors
mat = bpy.data.materials.new(f"{connector.name}_Mat")
mat.use_nodes = True
mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.1, 0.1, 0.1, 1)
connector.data.materials.append(mat)
created_connectors.append(connector.name)
return {"connectors_added": created_connectors}
def capture_viewport(self, params):
"""Capture viewport screenshot."""
output_path = params.get("output_path", "screenshot.png")
# Ensure output directory exists
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
# Set render settings
bpy.context.scene.render.filepath = output_path
bpy.context.scene.render.resolution_x = 1920
bpy.context.scene.render.resolution_y = 1080
# Render viewport
bpy.ops.render.opengl(write_still=True, view_context=True)
return {"path": output_path}
def get_scene_info(self, params):
"""Get information about current scene."""
objects = []
for obj in bpy.data.objects:
if obj.type == 'MESH':
objects.append({
"name": obj.name,
"vertices": len(obj.data.vertices),
"faces": len(obj.data.polygons),
"location": list(obj.location),
"dimensions": list(obj.dimensions)
})
return {
"objects": objects,
"total_objects": len(objects)
}
def execute_code(self, params):
"""Execute Python code in Blender context."""
code = params.get("code", "")
# Execute in Blender's Python context
try:
exec(code)
return {"executed": True}
except Exception as e:
return {"error": str(e)}
# Global server instance
vefrank_server = None
class VEFRANK_PT_panel(bpy.types.Panel):
"""Creates a Panel in the 3D viewport N panel."""
bl_label = "VeFrank MCP Enhanced"
bl_idname = "VEFRANK_PT_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "VeFrank"
def draw(self, context):
layout = self.layout
row = layout.row()
if context.scene.vefrank_server_running:
row.operator("vefrank.stop_server", text="Stop Server", icon='PAUSE')
row = layout.row()
row.label(text="Server running on localhost:9876", icon='CHECKMARK')
else:
row.operator("vefrank.start_server", text="Start Server", icon='PLAY')
row = layout.row()
row.label(text="Server stopped", icon='X')
layout.separator()
# Quick import section
col = layout.column()
col.label(text="Quick Import:")
col.operator("vefrank.import_component", text="Import Component")
# Info section
layout.separator()
col = layout.column()
col.label(text="Components in Scene:")
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.name.startswith(('ecu', 'pcm', 'bcm', 'alternator')):
col.label(text=f" • {obj.name}", icon='MESH_DATA')
class VEFRANK_OT_start_server(bpy.types.Operator):
"""Start VeFrank MCP server."""
bl_idname = "vefrank.start_server"
bl_label = "Start VeFrank Server"
def execute(self, context):
global vefrank_server
if not vefrank_server:
vefrank_server = VeFrankServer()
if vefrank_server.start():
context.scene.vefrank_server_running = True
self.report({'INFO'}, "VeFrank server started")
else:
self.report({'WARNING'}, "Server already running")
return {'FINISHED'}
class VEFRANK_OT_stop_server(bpy.types.Operator):
"""Stop VeFrank MCP server."""
bl_idname = "vefrank.stop_server"
bl_label = "Stop VeFrank Server"
def execute(self, context):
global vefrank_server
if vefrank_server:
vefrank_server.stop()
context.scene.vefrank_server_running = False
self.report({'INFO'}, "VeFrank server stopped")
return {'FINISHED'}
class VEFRANK_OT_import_component(bpy.types.Operator):
"""Import VeFrank component."""
bl_idname = "vefrank.import_component"
bl_label = "Import VeFrank Component"
component_type: bpy.props.EnumProperty(
name="Component Type",
items=[
('alternator', 'Alternator', 'Automotive alternator'),
('ecu', 'ECU', 'Engine Control Unit'),
('pcm', 'PCM', 'Powertrain Control Module'),
('bcm', 'BCM', 'Body Control Module'),
('sensor', 'Sensor', 'Generic sensor'),
]
)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
# Look for OBJ file
obj_path = Path(__file__).parent.parent / "assets" / "3d_models" / "automotive" / f"{self.component_type}.obj"
if obj_path.exists():
bpy.ops.import_scene.obj(filepath=str(obj_path))
self.report({'INFO'}, f"Imported {self.component_type}")
else:
self.report({'ERROR'}, f"Component file not found: {obj_path}")
return {'FINISHED'}
classes = [
VEFRANK_PT_panel,
VEFRANK_OT_start_server,
VEFRANK_OT_stop_server,
VEFRANK_OT_import_component,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
# Add property to track server state
bpy.types.Scene.vefrank_server_running = bpy.props.BoolProperty(
name="VeFrank Server Running",
default=False
)
def unregister():
global vefrank_server
if vefrank_server:
vefrank_server.stop()
for cls in classes:
bpy.utils.unregister_class(cls)
del bpy.types.Scene.vefrank_server_running
if __name__ == "__main__":
register()