Skip to main content
Glama
cad_context_extractor.py11.9 kB
""" CAD Context Extractor - Extracts current FreeCAD state for AI context This module provides functionality to extract relevant information from the current FreeCAD document and workspace to provide context to AI models. """ import json from typing import Any, Dict, List, Optional class CADContextExtractor: """Extracts CAD context information from FreeCAD.""" def __init__(self): """Initialize the CAD context extractor.""" self.freecad_available = False self.freecad = None self.freecad_gui = None try: import FreeCAD self.freecad = FreeCAD self.freecad_available = True try: import FreeCADGui self.freecad_gui = FreeCADGui except ImportError: pass except ImportError: pass def get_full_context(self) -> Dict[str, Any]: """Get complete CAD context information.""" if not self.freecad_available: return { "status": "FreeCAD not available", "documents": [], "workbench": "Unknown", "selection": [], "view": {}, } context = { "status": "Available", "freecad_version": self._get_freecad_version(), "documents": self._get_documents_info(), "active_document": self._get_active_document_info(), "workbench": self._get_current_workbench(), "selection": self._get_selection_info(), "view": self._get_view_info(), "preferences": self._get_relevant_preferences(), } return context def get_compact_context(self) -> str: """Get a compact, human-readable context summary.""" if not self.freecad_available: return "FreeCAD is not available in this environment." context_parts = [] # FreeCAD version version = self._get_freecad_version() if version: context_parts.append(f"FreeCAD Version: {version}") # Active document doc_info = self._get_active_document_info() if doc_info and doc_info.get("name"): context_parts.append(f"Active Document: {doc_info['name']}") objects = doc_info.get("objects", []) if objects: context_parts.append(f"Objects in document: {len(objects)}") # List object types object_types = {} for obj in objects: obj_type = obj.get("type", "Unknown") object_types[obj_type] = object_types.get(obj_type, 0) + 1 if object_types: type_summary = ", ".join( [ f"{count} {type_name}" for type_name, count in object_types.items() ] ) context_parts.append(f"Object types: {type_summary}") else: context_parts.append("No active document") # Current workbench workbench = self._get_current_workbench() if workbench: context_parts.append(f"Current Workbench: {workbench}") # Selection selection = self._get_selection_info() if selection: context_parts.append(f"Selected: {len(selection)} object(s)") return "\n".join(context_parts) if context_parts else "No CAD context available" def _get_freecad_version(self) -> Optional[str]: """Get FreeCAD version information.""" if not self.freecad: return None try: version_info = [] if hasattr(self.freecad, "Version"): version_info.append(f"v{'.'.join(self.freecad.Version()[:3])}") if hasattr(self.freecad, "BuildVersionInfo"): build_info = self.freecad.BuildVersionInfo() if "BuildRevision" in build_info: version_info.append(f"Build {build_info['BuildRevision']}") return " ".join(version_info) if version_info else "Unknown" except: return "Unknown" def _get_documents_info(self) -> List[Dict[str, Any]]: """Get information about all open documents.""" if not self.freecad: return [] documents = [] try: for doc in self.freecad.listDocuments().values(): doc_info = { "name": doc.Name, "label": getattr(doc, "Label", doc.Name), "file_path": getattr(doc, "FileName", ""), "object_count": len(doc.Objects), "modified": getattr(doc, "UndoMode", False), } documents.append(doc_info) except: pass return documents def _get_active_document_info(self) -> Optional[Dict[str, Any]]: """Get detailed information about the active document.""" if not self.freecad: return None try: doc = self.freecad.ActiveDocument if not doc: return None objects_info = [] for obj in doc.Objects: obj_info = { "name": obj.Name, "label": getattr(obj, "Label", obj.Name), "type": obj.TypeId, "visible": ( getattr(obj, "Visibility", True) if hasattr(obj, "Visibility") else True ), } # Add geometry information if available if hasattr(obj, "Shape") and obj.Shape: try: shape = obj.Shape obj_info["geometry"] = { "type": ( shape.ShapeType if hasattr(shape, "ShapeType") else "Unknown" ), "volume": ( round(shape.Volume, 3) if hasattr(shape, "Volume") else None ), "area": ( round(shape.Area, 3) if hasattr(shape, "Area") else None ), "length": ( round(shape.Length, 3) if hasattr(shape, "Length") else None ), } except: pass objects_info.append(obj_info) return { "name": doc.Name, "label": getattr(doc, "Label", doc.Name), "file_path": getattr(doc, "FileName", ""), "objects": objects_info, "object_count": len(objects_info), } except: return None def _get_current_workbench(self) -> Optional[str]: """Get the current active workbench.""" if not self.freecad_gui: return None try: workbench = self.freecad_gui.activeWorkbench() if workbench: return getattr( workbench, "MenuText", getattr(workbench, "__class__", {}).get("__name__", "Unknown"), ) except: pass return None def _get_selection_info(self) -> List[Dict[str, Any]]: """Get information about currently selected objects.""" if not self.freecad_gui: return [] selection_info = [] try: selection = self.freecad_gui.Selection.getSelection() for obj in selection: obj_info = { "name": obj.Name, "label": getattr(obj, "Label", obj.Name), "type": obj.TypeId, } # Add shape information if available if hasattr(obj, "Shape") and obj.Shape: try: shape = obj.Shape obj_info["shape_type"] = ( shape.ShapeType if hasattr(shape, "ShapeType") else "Unknown" ) except: pass selection_info.append(obj_info) except: pass return selection_info def _get_view_info(self) -> Dict[str, Any]: """Get current view information.""" if not self.freecad_gui: return {} view_info = {} try: view = ( self.freecad_gui.ActiveDocument.ActiveView if self.freecad_gui.ActiveDocument else None ) if view: # Get camera information camera = view.getCameraNode() if camera: view_info["camera_type"] = ( "Perspective" if camera.getField("heightAngle") else "Orthographic" ) # Get view direction view_dir = view.getViewDirection() if view_dir: view_info["view_direction"] = { "x": round(view_dir.x, 3), "y": round(view_dir.y, 3), "z": round(view_dir.z, 3), } except: pass return view_info def _get_relevant_preferences(self) -> Dict[str, Any]: """Get relevant FreeCAD preferences.""" if not self.freecad: return {} preferences = {} try: # Get units param_get = self.freecad.ParamGet( "User parameter:BaseApp/Preferences/Units" ) if param_get: unit_system = param_get.GetInt("UserSchema", 0) unit_names = { 0: "Standard (mm/kg/s)", 1: "MKS (m/kg/s)", 2: "US customary", 3: "Imperial decimal", 4: "Building Euro", 5: "Building US", 6: "Metric small parts", 7: "Imperial for FEM", } preferences["units"] = unit_names.get( unit_system, f"Custom ({unit_system})" ) # Get precision settings param_get = self.freecad.ParamGet( "User parameter:BaseApp/Preferences/Units" ) if param_get: preferences["decimal_places"] = param_get.GetInt("Decimals", 2) except: pass return preferences def format_for_ai_prompt(self, include_full_details: bool = False) -> str: """Format CAD context for inclusion in AI prompts.""" if include_full_details: context = self.get_full_context() return f"Current FreeCAD Context:\n{json.dumps(context, indent=2)}" else: return f"Current FreeCAD Context:\n{self.get_compact_context()}" # Global instance _context_extractor = None def get_cad_context_extractor() -> CADContextExtractor: """Get the global CAD context extractor instance.""" global _context_extractor if _context_extractor is None: _context_extractor = CADContextExtractor() return _context_extractor

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jango-blockchained/mcp-freecad'

If you have feedback or need assistance with the MCP directory API, please join our Discord server