Skip to main content
Glama
constraint.py31 kB
import logging from typing import Any, Dict, List, Optional from urllib.parse import parse_qs, urlparse from ..extractor.cad_context import CADContextExtractor from ..resources.base import ResourceProvider logger = logging.getLogger(__name__) class ConstraintResourceProvider(ResourceProvider): """Resource provider for constraints in CAD models.""" def __init__(self, freecad_app=None): """ Initialize the constraint resource provider. Args: freecad_app: Optional FreeCAD application instance. If None, will try to import FreeCAD. """ self.extractor = CADContextExtractor(freecad_app) self.app = freecad_app if self.app is None: try: import FreeCAD self.app = FreeCAD logger.info("Connected to FreeCAD for constraint data") except ImportError: logger.warning( "Could not import FreeCAD. Make sure it's installed and in your Python path." ) self.app = None async def get_resource( self, uri: str, params: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """ Retrieve constraint data from the CAD model. Args: uri: The resource URI in format "cad://constraints/[resource_type]/[object_name]" params: Optional parameters for the constraints request Returns: The constraint data """ logger.info(f"Retrieving constraint resource: {uri}") # Parse the URI parsed_uri = urlparse(uri) if parsed_uri.scheme != "cad": raise ValueError(f"Invalid URI scheme: {parsed_uri.scheme}, expected 'cad'") path_parts = parsed_uri.path.strip("/").split("/") if len(path_parts) < 1 or path_parts[0] != "constraints": raise ValueError( f"Invalid URI format: {uri}, expected 'cad://constraints/...'" ) # Handle different resource types if len(path_parts) == 1: # Return summary of all constraints in active document return await self._get_all_constraints() resource_type = path_parts[1] if resource_type == "sketch": # Return constraints for a specific sketch if len(path_parts) < 3: return {"error": "No sketch specified"} return await self._get_sketch_constraints(path_parts[2]) elif resource_type == "assembly": # Return assembly constraints if len(path_parts) < 3: # Return all assembly constraints in the document return await self._get_assembly_constraints() # Return assembly constraints for a specific object return await self._get_object_assembly_constraints(path_parts[2]) elif resource_type == "types": # Return available constraint types return await self._get_constraint_types() else: raise ValueError(f"Unknown resource type: {resource_type}") async def _get_all_constraints(self) -> Dict[str, Any]: """Get summary of all constraints in the active document.""" if self.app is None: return self._mock_all_constraints() try: # Get the active document doc = self.app.ActiveDocument if not doc: return {"error": "No active document"} # Collect all sketch constraints sketch_constraints = [] assembly_constraints = [] # Find all sketch objects for obj in doc.Objects: if obj.TypeId == "Sketcher::SketchObject": sketch_name = obj.Name constraints_count = ( len(obj.Constraints) if hasattr(obj, "Constraints") else 0 ) sketch_constraints.append( { "sketch_name": sketch_name, "label": obj.Label, "constraints_count": constraints_count, } ) # Find all assembly constraints (if Assembly workbench is available) # This is workbench-specific and would need to be adapted based on which # assembly workbench is being used (A2Plus, Assembly4, etc.) for obj in doc.Objects: if hasattr(obj, "Type") and "Constraint" in obj.Type: if any(asm_type in obj.Type for asm_type in ["Assembly", "A2p"]): assembly_constraints.append( {"name": obj.Name, "label": obj.Label, "type": obj.Type} ) return { "resource_type": "all_constraints", "document": doc.Name, "sketch_constraints": { "count": len(sketch_constraints), "sketches": sketch_constraints, }, "assembly_constraints": { "count": len(assembly_constraints), "constraints": assembly_constraints, }, } except Exception as e: logger.error(f"Error getting all constraints: {e}") return {"error": f"Error getting all constraints: {str(e)}"} def _mock_all_constraints(self) -> Dict[str, Any]: """Provide mock constraint data when FreeCAD is not available.""" return { "resource_type": "all_constraints", "document": "Document", "sketch_constraints": { "count": 2, "sketches": [ { "sketch_name": "Sketch", "label": "Sketch", "constraints_count": 12, }, { "sketch_name": "Sketch001", "label": "Sketch001", "constraints_count": 8, }, ], }, "assembly_constraints": { "count": 3, "constraints": [ { "name": "Constraint001", "label": "FixedConstraint", "type": "Assembly2Constraint", }, { "name": "Constraint002", "label": "PlaneConstraint", "type": "Assembly2Constraint", }, { "name": "Constraint003", "label": "AxisConstraint", "type": "Assembly2Constraint", }, ], }, "note": "Mock data (FreeCAD not available)", } async def _get_sketch_constraints(self, sketch_name: str) -> Dict[str, Any]: """Get constraints for a specific sketch.""" if self.app is None: return self._mock_sketch_constraints(sketch_name) try: # Get the active document doc = self.app.ActiveDocument if not doc: return {"error": "No active document"} # Get the sketch object sketch = doc.getObject(sketch_name) if not sketch: return {"error": f"Sketch not found: {sketch_name}"} if sketch.TypeId != "Sketcher::SketchObject": return {"error": f"Object is not a sketch: {sketch_name}"} # Get constraints constraints = [] if hasattr(sketch, "Constraints"): for i, constraint in enumerate(sketch.Constraints): constraint_data = { "index": i, "name": constraint.Name, "type": constraint.Type, "driven": getattr(constraint, "Driven", False), } # Add type-specific properties if constraint.Type == "Distance": constraint_data["value"] = constraint.Value constraint_data["first_pos"] = getattr(constraint, "First", -1) constraint_data["second_pos"] = getattr( constraint, "Second", -1 ) elif constraint.Type == "Angle": constraint_data["value"] = constraint.Value constraint_data["first_pos"] = getattr(constraint, "First", -1) constraint_data["second_pos"] = getattr( constraint, "Second", -1 ) elif constraint.Type in ["Horizontal", "Vertical"]: constraint_data["pos"] = getattr(constraint, "First", -1) elif constraint.Type in ["Coincident", "Parallel", "Perpendicular"]: constraint_data["first_pos"] = getattr(constraint, "First", -1) constraint_data["second_pos"] = getattr( constraint, "Second", -1 ) constraints.append(constraint_data) # Get geometry elements in the sketch geometry = [] if hasattr(sketch, "Geometry"): for i, geo in enumerate(sketch.Geometry): geo_type = geo.TypeId.split(":")[-1] # Extract type from TypeId geo_data = { "index": i, "type": geo_type, "construction": geo.Construction, } # Add type-specific properties if geo_type == "LineSegment": geo_data["start"] = [ geo.StartPoint.x, geo.StartPoint.y, geo.StartPoint.z, ] geo_data["end"] = [ geo.EndPoint.x, geo.EndPoint.y, geo.EndPoint.z, ] elif geo_type == "Circle": geo_data["center"] = [geo.Center.x, geo.Center.y, geo.Center.z] geo_data["radius"] = geo.Radius elif geo_type == "Arc": geo_data["center"] = [geo.Center.x, geo.Center.y, geo.Center.z] geo_data["radius"] = geo.Radius geo_data["start_angle"] = geo.FirstAngle geo_data["end_angle"] = geo.LastAngle geometry.append(geo_data) return { "resource_type": "sketch_constraints", "sketch_name": sketch_name, "label": sketch.Label, "constraints": {"count": len(constraints), "items": constraints}, "geometry": {"count": len(geometry), "items": geometry}, } except Exception as e: logger.error(f"Error getting sketch constraints: {e}") return {"error": f"Error getting sketch constraints: {str(e)}"} def _mock_sketch_constraints(self, sketch_name: str) -> Dict[str, Any]: """Provide mock sketch constraint data when FreeCAD is not available.""" return { "resource_type": "sketch_constraints", "sketch_name": sketch_name, "label": sketch_name, "constraints": { "count": 6, "items": [ { "index": 0, "name": "Horizontal1", "type": "Horizontal", "driven": False, "pos": 0, }, { "index": 1, "name": "Vertical1", "type": "Vertical", "driven": False, "pos": 1, }, { "index": 2, "name": "Distance1", "type": "Distance", "driven": False, "value": 10.0, "first_pos": 0, "second_pos": -1, }, { "index": 3, "name": "Distance2", "type": "Distance", "driven": False, "value": 20.0, "first_pos": 1, "second_pos": -1, }, { "index": 4, "name": "Coincident1", "type": "Coincident", "driven": False, "first_pos": 0, "second_pos": 1, }, { "index": 5, "name": "Parallel1", "type": "Parallel", "driven": False, "first_pos": 2, "second_pos": 3, }, ], }, "geometry": { "count": 4, "items": [ { "index": 0, "type": "LineSegment", "construction": False, "start": [0.0, 0.0, 0.0], "end": [10.0, 0.0, 0.0], }, { "index": 1, "type": "LineSegment", "construction": False, "start": [10.0, 0.0, 0.0], "end": [10.0, 20.0, 0.0], }, { "index": 2, "type": "LineSegment", "construction": False, "start": [10.0, 20.0, 0.0], "end": [0.0, 20.0, 0.0], }, { "index": 3, "type": "LineSegment", "construction": False, "start": [0.0, 20.0, 0.0], "end": [0.0, 0.0, 0.0], }, ], }, "note": "Mock data (FreeCAD not available)", } async def _get_assembly_constraints(self) -> Dict[str, Any]: """Get all assembly constraints in the active document.""" if self.app is None: return self._mock_assembly_constraints() try: # Get the active document doc = self.app.ActiveDocument if not doc: return {"error": "No active document"} # Assembly constraints are workbench-specific # This implementation is based on common patterns found in Assembly workbenches # It would need to be adapted based on which assembly workbench is being used constraints = [] # Look for objects that might be assembly constraints for obj in doc.Objects: if hasattr(obj, "Type") and "Constraint" in obj.Type: if any(asm_type in obj.Type for asm_type in ["Assembly", "A2p"]): constraint_data = { "name": obj.Name, "label": obj.Label, "type": obj.Type, } # Get linked objects (different workbenches handle this differently) linked_objects = [] # A2Plus approach if hasattr(obj, "Object1"): linked_objects.append(obj.Object1) if hasattr(obj, "Object2"): linked_objects.append(obj.Object2) # Assembly4 approach if hasattr(obj, "References"): for ref in obj.References: linked_objects.append(ref[0].Name) constraint_data["linked_objects"] = linked_objects # Get constraint parameters parameters = {} for prop in obj.PropertiesList: if prop not in [ "Name", "Label", "Type", "Object1", "Object2", "References", ]: try: parameters[prop] = getattr(obj, prop) except: pass constraint_data["parameters"] = parameters constraints.append(constraint_data) return { "resource_type": "assembly_constraints", "document": doc.Name, "count": len(constraints), "constraints": constraints, } except Exception as e: logger.error(f"Error getting assembly constraints: {e}") return {"error": f"Error getting assembly constraints: {str(e)}"} def _mock_assembly_constraints(self) -> Dict[str, Any]: """Provide mock assembly constraint data when FreeCAD is not available.""" return { "resource_type": "assembly_constraints", "document": "Document", "count": 3, "constraints": [ { "name": "Constraint001", "label": "FixedConstraint", "type": "Assembly2Constraint", "linked_objects": ["Part001"], "parameters": {"Offset": 0.0}, }, { "name": "Constraint002", "label": "PlaneConstraint", "type": "Assembly2Constraint", "linked_objects": ["Part001", "Part002"], "parameters": { "Offset": 0.0, "Plane1": "XY_Plane", "Plane2": "XY_Plane", }, }, { "name": "Constraint003", "label": "AxisConstraint", "type": "Assembly2Constraint", "linked_objects": ["Part002", "Part003"], "parameters": {"Offset": 0.0, "Axis1": "X_Axis", "Axis2": "X_Axis"}, }, ], "note": "Mock data (FreeCAD not available)", } async def _get_object_assembly_constraints( self, object_name: str ) -> Dict[str, Any]: """Get assembly constraints for a specific object.""" if self.app is None: return self._mock_object_assembly_constraints(object_name) try: # Get the active document doc = self.app.ActiveDocument if not doc: return {"error": "No active document"} # Get the object obj = doc.getObject(object_name) if not obj: return {"error": f"Object not found: {object_name}"} # Find constraints related to this object constraints = [] for doc_obj in doc.Objects: if hasattr(doc_obj, "Type") and "Constraint" in doc_obj.Type: if any( asm_type in doc_obj.Type for asm_type in ["Assembly", "A2p"] ): is_related = False # A2Plus approach if hasattr(doc_obj, "Object1") and doc_obj.Object1 == obj.Name: is_related = True if hasattr(doc_obj, "Object2") and doc_obj.Object2 == obj.Name: is_related = True # Assembly4 approach if hasattr(doc_obj, "References"): for ref in doc_obj.References: if ref[0].Name == obj.Name: is_related = True break if is_related: constraint_data = { "name": doc_obj.Name, "label": doc_obj.Label, "type": doc_obj.Type, } # Get linked objects linked_objects = [] # A2Plus approach if hasattr(doc_obj, "Object1"): linked_objects.append(doc_obj.Object1) if hasattr(doc_obj, "Object2"): linked_objects.append(doc_obj.Object2) # Assembly4 approach if hasattr(doc_obj, "References"): for ref in doc_obj.References: linked_objects.append(ref[0].Name) constraint_data["linked_objects"] = linked_objects # Get constraint parameters parameters = {} for prop in doc_obj.PropertiesList: if prop not in [ "Name", "Label", "Type", "Object1", "Object2", "References", ]: try: parameters[prop] = getattr(doc_obj, prop) except: pass constraint_data["parameters"] = parameters constraints.append(constraint_data) return { "resource_type": "object_assembly_constraints", "object": object_name, "label": obj.Label, "count": len(constraints), "constraints": constraints, } except Exception as e: logger.error(f"Error getting object assembly constraints: {e}") return {"error": f"Error getting object assembly constraints: {str(e)}"} def _mock_object_assembly_constraints(self, object_name: str) -> Dict[str, Any]: """Provide mock object assembly constraint data when FreeCAD is not available.""" return { "resource_type": "object_assembly_constraints", "object": object_name, "label": object_name, "count": 2, "constraints": [ { "name": "Constraint001", "label": "PlaneConstraint", "type": "Assembly2Constraint", "linked_objects": [object_name, "Part002"], "parameters": { "Offset": 0.0, "Plane1": "XY_Plane", "Plane2": "XY_Plane", }, }, { "name": "Constraint002", "label": "AxisConstraint", "type": "Assembly2Constraint", "linked_objects": [object_name, "Part003"], "parameters": {"Offset": 0.0, "Axis1": "X_Axis", "Axis2": "X_Axis"}, }, ], "note": "Mock data (FreeCAD not available)", } async def _get_constraint_types(self) -> Dict[str, Any]: """Get available constraint types.""" if self.app is None: return self._mock_constraint_types() try: # This information is largely static and based on FreeCAD's capabilities # We'll return a comprehensive list of constraint types for different workbenches sketch_constraints = [ { "name": "Coincident", "description": "Forces two points to be coincident", "workbench": "Sketcher", }, { "name": "Point on Object", "description": "Forces a point to lie on another object", "workbench": "Sketcher", }, { "name": "Horizontal", "description": "Forces a line segment to be horizontal", "workbench": "Sketcher", }, { "name": "Vertical", "description": "Forces a line segment to be vertical", "workbench": "Sketcher", }, { "name": "Parallel", "description": "Forces two line segments to be parallel", "workbench": "Sketcher", }, { "name": "Perpendicular", "description": "Forces two line segments to be perpendicular", "workbench": "Sketcher", }, { "name": "Tangent", "description": "Forces an edge to be tangent to another edge", "workbench": "Sketcher", }, { "name": "Equal", "description": "Forces two parameters to be equal", "workbench": "Sketcher", }, { "name": "Symmetric", "description": "Forces two points to be symmetric with respect to a line", "workbench": "Sketcher", }, { "name": "Distance", "description": "Sets the distance between two elements", "workbench": "Sketcher", }, { "name": "Angle", "description": "Sets the angle between two line segments", "workbench": "Sketcher", }, ] assembly_constraints = [ { "name": "Fixed", "description": "Fixes an object in the assembly", "workbench": "Assembly", }, { "name": "Plane Alignment", "description": "Aligns planes between two objects", "workbench": "Assembly", }, { "name": "Axis Alignment", "description": "Aligns axes between two objects", "workbench": "Assembly", }, { "name": "Point on Point", "description": "Aligns points between two objects", "workbench": "Assembly", }, { "name": "Point on Line", "description": "Places a point on a line", "workbench": "Assembly", }, { "name": "Point on Plane", "description": "Places a point on a plane", "workbench": "Assembly", }, ] return { "resource_type": "constraint_types", "sketch_constraints": sketch_constraints, "assembly_constraints": assembly_constraints, } except Exception as e: logger.error(f"Error getting constraint types: {e}") return {"error": f"Error getting constraint types: {str(e)}"} def _mock_constraint_types(self) -> Dict[str, Any]: """Provide mock constraint types when FreeCAD is not available.""" return { "resource_type": "constraint_types", "sketch_constraints": [ { "name": "Coincident", "description": "Forces two points to be coincident", "workbench": "Sketcher", }, { "name": "Point on Object", "description": "Forces a point to lie on another object", "workbench": "Sketcher", }, { "name": "Horizontal", "description": "Forces a line segment to be horizontal", "workbench": "Sketcher", }, { "name": "Vertical", "description": "Forces a line segment to be vertical", "workbench": "Sketcher", }, { "name": "Parallel", "description": "Forces two line segments to be parallel", "workbench": "Sketcher", }, { "name": "Perpendicular", "description": "Forces two line segments to be perpendicular", "workbench": "Sketcher", }, ], "assembly_constraints": [ { "name": "Fixed", "description": "Fixes an object in the assembly", "workbench": "Assembly", }, { "name": "Plane Alignment", "description": "Aligns planes between two objects", "workbench": "Assembly", }, { "name": "Axis Alignment", "description": "Aligns axes between two objects", "workbench": "Assembly", }, ], "note": "Mock data (FreeCAD not available)", }

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