freecad mcp

MIT License
39
  • Apple
  • Linux
import FreeCAD import FreeCADGui import ObjectsFem import contextlib import queue import base64 import io import os import tempfile import threading from dataclasses import dataclass, field from typing import Any from xmlrpc.server import SimpleXMLRPCServer from PySide2.QtCore import QTimer from .parts_library import get_parts_list, insert_part_from_library from .serialize import serialize_object rpc_server_thread = None rpc_server_instance = None # GUI task queue rpc_request_queue = queue.Queue() rpc_response_queue = queue.Queue() def process_gui_tasks(): while not rpc_request_queue.empty(): task = rpc_request_queue.get() res = task() if res is not None: rpc_response_queue.put(res) QTimer.singleShot(500, process_gui_tasks) @dataclass class Object: name: str type: str | None = None analysis: str | None = None properties: dict[str, Any] = field(default_factory=dict) def set_object_property( doc: FreeCAD.Document, obj: FreeCAD.DocumentObject, properties: dict[str, Any] ): for prop, val in properties.items(): try: if prop in obj.PropertiesList: if prop == "Placement" and isinstance(val, dict): if "Base" in val: pos = val["Base"] elif "Position" in val: pos = val["Position"] else: pos = {} rot = val.get("Rotation", {}) placement = FreeCAD.Placement( FreeCAD.Vector( pos.get("x", 0), pos.get("y", 0), pos.get("z", 0), ), FreeCAD.Rotation( FreeCAD.Vector( rot.get("Axis", {}).get("x", 0), rot.get("Axis", {}).get("y", 0), rot.get("Axis", {}).get("z", 1), ), rot.get("Angle", 0), ), ) setattr(obj, prop, placement) elif isinstance(getattr(obj, prop), FreeCAD.Vector) and isinstance( val, dict ): vector = FreeCAD.Vector( val.get("x", 0), val.get("y", 0), val.get("z", 0) ) setattr(obj, prop, vector) elif prop in ["Base", "Tool", "Source", "Profile"] and isinstance( val, str ): ref_obj = doc.getObject(val) if ref_obj: setattr(obj, prop, ref_obj) else: raise ValueError(f"Referenced object '{val}' not found.") elif prop == "References" and isinstance(val, list): refs = [] for ref_name, face in val: ref_obj = doc.getObject(ref_name) if ref_obj: refs.append((ref_obj, face)) else: raise ValueError(f"Referenced object '{ref_name}' not found.") setattr(obj, prop, refs) else: setattr(obj, prop, val) # ShapeColor is a property of the ViewObject elif prop == "ShapeColor" and isinstance(val, (list, tuple)): setattr(obj.ViewObject, prop, (float(val[0]), float(val[1]), float(val[2]), float(val[3]))) elif prop == "ViewObject" and isinstance(val, dict): for k, v in val.items(): if k == "ShapeColor": setattr(obj.ViewObject, k, (float(v[0]), float(v[1]), float(v[2]), float(v[3]))) else: setattr(obj.ViewObject, k, v) else: setattr(obj, prop, val) except Exception as e: FreeCAD.Console.PrintError(f"Property '{prop}' assignment error: {e}\n") class FreeCADRPC: """RPC server for FreeCAD""" def ping(self): return True def create_document(self, name="New_Document"): rpc_request_queue.put(lambda: self._create_document_gui(name)) res = rpc_response_queue.get() if res is True: return {"success": True, "document_name": name} else: return {"success": False, "error": res} def create_object(self, doc_name, obj_data: dict[str, Any]): obj = Object( name=obj_data.get("Name", "New_Object"), type=obj_data["Type"], analysis=obj_data.get("Analysis", None), properties=obj_data.get("Properties", {}), ) rpc_request_queue.put(lambda: self._create_object_gui(doc_name, obj)) res = rpc_response_queue.get() if res is True: return {"success": True, "object_name": obj.name} else: return {"success": False, "error": res} def edit_object(self, doc_name: str, obj_name: str, properties: dict[str, Any]) -> dict[str, Any]: obj = Object( name=obj_name, properties=properties.get("Properties", {}), ) rpc_request_queue.put(lambda: self._edit_object_gui(doc_name, obj)) res = rpc_response_queue.get() if res is True: return {"success": True, "object_name": obj.name} else: return {"success": False, "error": res} def delete_object(self, doc_name: str, obj_name: str): rpc_request_queue.put(lambda: self._delete_object_gui(doc_name, obj_name)) res = rpc_response_queue.get() if res is True: return {"success": True, "object_name": obj_name} else: return {"success": False, "error": res} def execute_code(self, code: str) -> dict[str, Any]: output_buffer = io.StringIO() def task(): try: with contextlib.redirect_stdout(output_buffer): exec(code, globals()) FreeCAD.Console.PrintMessage("Python code executed successfully.\n") return True except Exception as e: FreeCAD.Console.PrintError( f"Error executing Python code: {e}\n" ) return f"Error executing Python code: {e}\n" rpc_request_queue.put(task) res = rpc_response_queue.get() if res is True: return { "success": True, "message": "Python code execution scheduled. \nOutput: " + output_buffer.getvalue() } else: return {"success": False, "error": res} def get_objects(self, doc_name): doc = FreeCAD.getDocument(doc_name) if doc: return [serialize_object(obj) for obj in doc.Objects] else: return [] def get_object(self, doc_name, obj_name): doc = FreeCAD.getDocument(doc_name) if doc: return serialize_object(doc.getObject(obj_name)) else: return None def insert_part_from_library(self, relative_path): rpc_request_queue.put(lambda: self._insert_part_from_library(relative_path)) res = rpc_response_queue.get() if res is True: return {"success": True, "message": "Part inserted from library."} else: return {"success": False, "error": res} def list_documents(self): return list(FreeCAD.listDocuments().keys()) def get_parts_list(self): return get_parts_list() def get_active_screenshot(self, view_name: str = "Isometric") -> str: temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False) rpc_request_queue.put(lambda: self._save_active_screenshot(temp_file.name, view_name)) res = rpc_response_queue.get() if res is True: with open(temp_file.name, "rb") as image_file: image_bytes = image_file.read() os.remove(temp_file.name) return base64.b64encode(image_bytes).decode("utf-8") else: return None def _create_document_gui(self, name): doc = FreeCAD.newDocument(name) doc.recompute() FreeCAD.Console.PrintMessage(f"Document '{name}' created via RPC.\n") return True def _create_object_gui(self, doc_name, obj: Object): doc = FreeCAD.getDocument(doc_name) if doc: try: if obj.type == "Fem::FemMeshGmsh" and obj.analysis: from femmesh.gmshtools import GmshTools res = getattr(doc, obj.analysis).addObject(ObjectsFem.makeMeshGmsh(doc, obj.name))[0] if "Part" in obj.properties: target_obj = doc.getObject(obj.properties["Part"]) if target_obj: res.Part = target_obj else: raise ValueError(f"Referenced object '{obj.properties['Part']}' not found.") del obj.properties["Part"] else: raise ValueError("'Part' property not found in properties.") for param, value in obj.properties.items(): if hasattr(res, param): setattr(res, param, value) doc.recompute() gmsh_tools = GmshTools(res) gmsh_tools.create_mesh() FreeCAD.Console.PrintMessage( f"FEM Mesh '{res.Name}' generated successfully in '{doc_name}'.\n" ) elif obj.type.startswith("Fem::"): fem_make_methods = { "MaterialCommon": ObjectsFem.makeMaterialSolid, "AnalysisPython": ObjectsFem.makeAnalysis, } obj_type_short = obj.type.split("::")[1] method_name = "make" + obj_type_short make_method = fem_make_methods.get(obj_type_short, getattr(ObjectsFem, method_name, None)) if callable(make_method): res = make_method(doc, obj.name) set_object_property(doc, res, obj.properties) FreeCAD.Console.PrintMessage( f"FEM object '{res.Name}' created with '{method_name}'.\n" ) else: raise ValueError(f"No creation method '{method_name}' found in ObjectsFem.") if obj.type != "Fem::AnalysisPython" and obj.analysis: getattr(doc, obj.analysis).addObject(res) else: res = doc.addObject(obj.type, obj.name) set_object_property(doc, res, obj.properties) FreeCAD.Console.PrintMessage( f"{res.TypeId} '{res.Name}' added to '{doc_name}' via RPC.\n" ) doc.recompute() return True except Exception as e: return str(e) else: FreeCAD.Console.PrintError(f"Document '{doc_name}' not found.\n") return f"Document '{doc_name}' not found.\n" def _edit_object_gui(self, doc_name: str, obj: Object): doc = FreeCAD.getDocument(doc_name) if not doc: FreeCAD.Console.PrintError(f"Document '{doc_name}' not found.\n") return f"Document '{doc_name}' not found.\n" obj_ins = doc.getObject(obj.name) if not obj_ins: FreeCAD.Console.PrintError(f"Object '{obj.name}' not found in document '{doc_name}'.\n") return f"Object '{obj.name}' not found in document '{doc_name}'.\n" try: # For Fem::ConstraintFixed if hasattr(obj_ins, "References") and "References" in obj.properties: refs = [] for ref_name, face in obj.properties["References"]: ref_obj = doc.getObject(ref_name) if ref_obj: refs.append((ref_obj, face)) else: raise ValueError(f"Referenced object '{ref_name}' not found.") obj_ins.References = refs FreeCAD.Console.PrintMessage( f"References updated for '{obj.name}' in '{doc_name}'.\n" ) # delete References from properties del obj.properties["References"] set_object_property(doc, obj_ins, obj.properties) doc.recompute() FreeCAD.Console.PrintMessage(f"Object '{obj.name}' updated via RPC.\n") return True except Exception as e: return str(e) def _delete_object_gui(self, doc_name: str, obj_name: str): doc = FreeCAD.getDocument(doc_name) if not doc: FreeCAD.Console.PrintError(f"Document '{doc_name}' not found.\n") return f"Document '{doc_name}' not found.\n" try: doc.removeObject(obj_name) doc.recompute() FreeCAD.Console.PrintMessage(f"Object '{obj_name}' deleted via RPC.\n") return True except Exception as e: return str(e) def _insert_part_from_library(self, relative_path): try: insert_part_from_library(relative_path) return True except Exception as e: return str(e) def _save_active_screenshot(self, save_path: str, view_name: str = "Isometric"): try: view = FreeCADGui.ActiveDocument.ActiveView if view_name == "Isometric": view.viewIsometric() elif view_name == "Front": view.viewFront() elif view_name == "Top": view.viewTop() elif view_name == "Right": view.viewRight() elif view_name == "Back": view.viewBack() elif view_name == "Left": view.viewLeft() elif view_name == "Bottom": view.viewBottom() elif view_name == "Dimetric": view.viewDimetric() elif view_name == "Trimetric": view.viewTrimetric() else: raise ValueError(f"Invalid view name: {view_name}") view.fitAll() view.saveImage(save_path, 1) return True except Exception as e: return str(e) def start_rpc_server(host="localhost", port=9875): global rpc_server_thread, rpc_server_instance if rpc_server_instance: return "RPC Server already running." rpc_server_instance = SimpleXMLRPCServer( (host, port), allow_none=True, logRequests=False ) rpc_server_instance.register_instance(FreeCADRPC()) def server_loop(): FreeCAD.Console.PrintMessage(f"RPC Server started at {host}:{port}\n") rpc_server_instance.serve_forever() rpc_server_thread = threading.Thread(target=server_loop, daemon=True) rpc_server_thread.start() QTimer.singleShot(500, process_gui_tasks) return f"RPC Server started at {host}:{port}." def stop_rpc_server(): global rpc_server_instance, rpc_server_thread if rpc_server_instance: rpc_server_instance.shutdown() rpc_server_thread.join() rpc_server_instance = None rpc_server_thread = None FreeCAD.Console.PrintMessage("RPC Server stopped.\n") return "RPC Server stopped." return "RPC Server was not running." class StartRPCServerCommand: def GetResources(self): return {"MenuText": "Start RPC Server", "ToolTip": "Start RPC Server"} def Activated(self): msg = start_rpc_server() FreeCAD.Console.PrintMessage(msg + "\n") def IsActive(self): return True class StopRPCServerCommand: def GetResources(self): return {"MenuText": "Stop RPC Server", "ToolTip": "Stop RPC Server"} def Activated(self): msg = stop_rpc_server() FreeCAD.Console.PrintMessage(msg + "\n") def IsActive(self): return True FreeCADGui.addCommand("Start_RPC_Server", StartRPCServerCommand()) FreeCADGui.addCommand("Stop_RPC_Server", StopRPCServerCommand())