Skip to main content
Glama
advanced_operations.py24.2 kB
""" Advanced Operations Tool MCP tool for performing advanced CAD operations in FreeCAD including extrude, revolve, loft, sweep, pipe, and helix operations. Author: jango-blockchained """ import math from typing import Any, Dict, List import FreeCAD as App import Part class AdvancedOperationsTool: """Tool for performing advanced CAD operations on objects.""" def __init__(self): """Initialize the advanced operations tool.""" self.name = "advanced_operations" self.description = "Perform advanced CAD operations on objects" def extrude_profile( self, profile_name: str, direction: tuple = (0, 0, 1), distance: float = 10.0, name: str = None, ) -> Dict[str, Any]: """Extrude a 2D profile to create a 3D solid. Args: profile_name: Name of the 2D profile object in FreeCAD direction: Direction vector for extrusion (x, y, z) distance: Extrusion distance in mm name: Optional object name Returns: Dictionary with operation result """ try: # Parameter validation if distance <= 0: return { "success": False, "error": "Distance must be positive", "message": f"Invalid distance: {distance}", } if len(direction) != 3: return { "success": False, "error": "Direction must be a 3-element tuple (x, y, z)", "message": f"Invalid direction: {direction}", } # Normalize direction vector dir_vector = App.Vector(*direction) if dir_vector.Length == 0: return { "success": False, "error": "Direction vector cannot be zero", "message": f"Invalid direction vector: {direction}", } dir_vector.normalize() # Get active document doc = App.ActiveDocument if not doc: return { "success": False, "error": "No active document", "message": "Please create or open a FreeCAD document", } # Find the profile object profile_obj = doc.getObject(profile_name) if not profile_obj: return { "success": False, "error": f"Profile object '{profile_name}' not found", "message": f"Available objects: {[obj.Name for obj in doc.Objects]}", } # Get the shape from the profile if hasattr(profile_obj, "Shape"): profile_shape = profile_obj.Shape else: return { "success": False, "error": "Profile object has no Shape attribute", "message": f"Object {profile_name} is not a valid geometric object", } # Check if profile is 2D (has faces or wires) if profile_shape.Faces: # Use the first face for extrusion face = profile_shape.Faces[0] extruded_shape = face.extrude(dir_vector * distance) elif profile_shape.Wires: # Create face from wire and extrude wire = profile_shape.Wires[0] try: face = Part.Face(wire) extruded_shape = face.extrude(dir_vector * distance) except: return { "success": False, "error": "Cannot create face from wire", "message": "Wire may not be closed or may be invalid", } else: return { "success": False, "error": "Profile must have faces or closed wires", "message": f"Profile {profile_name} has no extrudable geometry", } # Create FreeCAD object obj_name = name or f"Extrude_{profile_name}_{distance}mm" extruded_obj = doc.addObject("Part::Feature", obj_name) extruded_obj.Shape = extruded_shape extruded_obj.Label = obj_name # Recompute document doc.recompute() # Calculate volume volume = extruded_shape.Volume return { "success": True, "object_name": extruded_obj.Name, "label": extruded_obj.Label, "message": f"Extruded {profile_name} by {distance}mm in direction {direction}", "properties": { "source_profile": profile_name, "direction": direction, "distance": distance, "volume": round(volume, 2), }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to extrude profile: {str(e)}", } def revolve_profile( self, profile_name: str, axis_point: tuple = (0, 0, 0), axis_direction: tuple = (0, 0, 1), angle: float = 360.0, name: str = None, ) -> Dict[str, Any]: """Revolve a 2D profile around an axis to create a 3D solid. Args: profile_name: Name of the 2D profile object in FreeCAD axis_point: Point on the rotation axis (x, y, z) axis_direction: Direction of the rotation axis (x, y, z) angle: Rotation angle in degrees (0-360) name: Optional object name Returns: Dictionary with operation result """ try: # Parameter validation if angle <= 0 or angle > 360: return { "success": False, "error": "Angle must be between 0 and 360 degrees", "message": f"Invalid angle: {angle}", } if len(axis_point) != 3 or len(axis_direction) != 3: return { "success": False, "error": "Axis point and direction must be 3-element tuples", "message": f"Invalid axis: point={axis_point}, direction={axis_direction}", } # Normalize axis direction axis_vec = App.Vector(*axis_direction) if axis_vec.Length == 0: return { "success": False, "error": "Axis direction vector cannot be zero", "message": f"Invalid axis direction: {axis_direction}", } axis_vec.normalize() # Get active document doc = App.ActiveDocument if not doc: return { "success": False, "error": "No active document", "message": "Please create or open a FreeCAD document", } # Find the profile object profile_obj = doc.getObject(profile_name) if not profile_obj: return { "success": False, "error": f"Profile object '{profile_name}' not found", "message": f"Available objects: {[obj.Name for obj in doc.Objects]}", } # Get the shape from the profile if hasattr(profile_obj, "Shape"): profile_shape = profile_obj.Shape else: return { "success": False, "error": "Profile object has no Shape attribute", "message": f"Object {profile_name} is not a valid geometric object", } # Check if profile is 2D (has faces or wires) if profile_shape.Faces: # Use the first face for revolution face = profile_shape.Faces[0] revolved_shape = face.revolve( App.Vector(*axis_point), axis_vec, math.radians(angle) ) elif profile_shape.Wires: # Create face from wire and revolve wire = profile_shape.Wires[0] try: face = Part.Face(wire) revolved_shape = face.revolve( App.Vector(*axis_point), axis_vec, math.radians(angle) ) except: return { "success": False, "error": "Cannot create face from wire", "message": "Wire may not be closed or may be invalid", } else: return { "success": False, "error": "Profile must have faces or closed wires", "message": f"Profile {profile_name} has no revolvable geometry", } # Create FreeCAD object obj_name = name or f"Revolve_{profile_name}_{angle}deg" revolved_obj = doc.addObject("Part::Feature", obj_name) revolved_obj.Shape = revolved_shape revolved_obj.Label = obj_name # Recompute document doc.recompute() # Calculate volume volume = revolved_shape.Volume return { "success": True, "object_name": revolved_obj.Name, "label": revolved_obj.Label, "message": f"Revolved {profile_name} by {angle}° around axis at {axis_point}", "properties": { "source_profile": profile_name, "axis_point": axis_point, "axis_direction": axis_direction, "angle": angle, "volume": round(volume, 2), }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to revolve profile: {str(e)}", } def loft_profiles( self, profile_names: List[str], solid: bool = True, ruled: bool = False, name: str = None, ) -> Dict[str, Any]: """Create a loft between multiple 2D profiles. Args: profile_names: List of profile object names in FreeCAD solid: Create solid (True) or surface (False) ruled: Use ruled surface (True) or smooth (False) name: Optional object name Returns: Dictionary with operation result """ try: # Parameter validation if len(profile_names) < 2: return { "success": False, "error": "At least 2 profiles required for loft", "message": f"Only {len(profile_names)} profiles provided", } # Get active document doc = App.ActiveDocument if not doc: return { "success": False, "error": "No active document", "message": "Please create or open a FreeCAD document", } # Collect profile shapes profiles = [] for profile_name in profile_names: profile_obj = doc.getObject(profile_name) if not profile_obj: return { "success": False, "error": f"Profile object '{profile_name}' not found", "message": f"Available objects: {[obj.Name for obj in doc.Objects]}", } if hasattr(profile_obj, "Shape"): profile_shape = profile_obj.Shape else: return { "success": False, "error": f"Profile object '{profile_name}' has no Shape attribute", "message": f"Object {profile_name} is not a valid geometric object", } # Get wire or face from profile if profile_shape.Wires: profiles.append(profile_shape.Wires[0]) elif profile_shape.Faces: # Get outer wire from face profiles.append(profile_shape.Faces[0].OuterWire) else: return { "success": False, "error": f"Profile '{profile_name}' has no wires or faces", "message": f"Profile {profile_name} has no loftable geometry", } # Create loft try: lofted_shape = Part.makeLoft(profiles, solid, ruled) except Exception as e: return { "success": False, "error": f"Loft operation failed: {str(e)}", "message": "Profiles may be incompatible or incorrectly positioned", } # Create FreeCAD object obj_name = name or f"Loft_{len(profile_names)}profiles" lofted_obj = doc.addObject("Part::Feature", obj_name) lofted_obj.Shape = lofted_shape lofted_obj.Label = obj_name # Recompute document doc.recompute() # Calculate volume (if solid) volume = lofted_shape.Volume if solid else 0 return { "success": True, "object_name": lofted_obj.Name, "label": lofted_obj.Label, "message": f"Created loft from {len(profile_names)} profiles", "properties": { "source_profiles": profile_names, "solid": solid, "ruled": ruled, "volume": round(volume, 2) if solid else "N/A (surface)", }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to create loft: {str(e)}", } def sweep_profile( self, profile_name: str, path_name: str, frenet: bool = False, name: str = None ) -> Dict[str, Any]: """Sweep a 2D profile along a 3D path. Args: profile_name: Name of the 2D profile object in FreeCAD path_name: Name of the 3D path object in FreeCAD frenet: Use Frenet frame orientation (True) or fixed (False) name: Optional object name Returns: Dictionary with operation result """ try: # Get active document doc = App.ActiveDocument if not doc: return { "success": False, "error": "No active document", "message": "Please create or open a FreeCAD document", } # Find the profile object profile_obj = doc.getObject(profile_name) if not profile_obj: return { "success": False, "error": f"Profile object '{profile_name}' not found", "message": f"Available objects: {[obj.Name for obj in doc.Objects]}", } # Find the path object path_obj = doc.getObject(path_name) if not path_obj: return { "success": False, "error": f"Path object '{path_name}' not found", "message": f"Available objects: {[obj.Name for obj in doc.Objects]}", } # Get shapes if not hasattr(profile_obj, "Shape") or not hasattr(path_obj, "Shape"): return { "success": False, "error": "Profile or path object has no Shape attribute", "message": "Objects must be valid geometric objects", } profile_shape = profile_obj.Shape path_shape = path_obj.Shape # Get profile wire or face if profile_shape.Wires: profile_wire = profile_shape.Wires[0] elif profile_shape.Faces: profile_wire = profile_shape.Faces[0].OuterWire else: return { "success": False, "error": "Profile must have wires or faces", "message": f"Profile {profile_name} has no sweepable geometry", } # Get path wire if path_shape.Wires: path_wire = path_shape.Wires[0] elif path_shape.Edges: # Create wire from edges path_wire = Part.Wire(path_shape.Edges) else: return { "success": False, "error": "Path must have wires or edges", "message": f"Path {path_name} has no valid path geometry", } # Create sweep try: if frenet: swept_shape = Part.makePipeShell( [profile_wire], path_wire, True, True ) else: swept_shape = Part.makePipeShell( [profile_wire], path_wire, False, False ) except Exception as e: return { "success": False, "error": f"Sweep operation failed: {str(e)}", "message": "Profile and path may be incompatible", } # Create FreeCAD object obj_name = name or f"Sweep_{profile_name}_along_{path_name}" swept_obj = doc.addObject("Part::Feature", obj_name) swept_obj.Shape = swept_shape swept_obj.Label = obj_name # Recompute document doc.recompute() # Calculate volume volume = swept_shape.Volume return { "success": True, "object_name": swept_obj.Name, "label": swept_obj.Label, "message": f"Swept {profile_name} along {path_name}", "properties": { "source_profile": profile_name, "path": path_name, "frenet": frenet, "volume": round(volume, 2), }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to create sweep: {str(e)}", } def create_helix( self, radius: float = 5.0, pitch: float = 2.0, height: float = 20.0, direction: str = "right", position: tuple = (0, 0, 0), name: str = None, ) -> Dict[str, Any]: """Create a helical curve. Args: radius: Helix radius in mm pitch: Distance between turns in mm height: Total height of helix in mm direction: Helix direction ("right" or "left") position: (x, y, z) position tuple name: Optional object name Returns: Dictionary with operation result """ try: # Parameter validation if radius <= 0: return { "success": False, "error": "Radius must be positive", "message": f"Invalid radius: {radius}", } if pitch <= 0: return { "success": False, "error": "Pitch must be positive", "message": f"Invalid pitch: {pitch}", } if height <= 0: return { "success": False, "error": "Height must be positive", "message": f"Invalid height: {height}", } if direction not in ["right", "left"]: return { "success": False, "error": "Direction must be 'right' or 'left'", "message": f"Invalid direction: {direction}", } # Get or create active document doc = App.ActiveDocument if not doc: doc = App.newDocument("Untitled") # Calculate number of turns turns = height / pitch # Create helix # FreeCAD's makeHelix: radius, pitch, height, angle (optional), left_handed (optional) left_handed = direction == "left" helix_shape = Part.makeHelix(pitch, height, radius, 0, left_handed) # Create FreeCAD object obj_name = name or f"Helix_R{radius}_P{pitch}_H{height}_{direction}" helix_obj = doc.addObject("Part::Feature", obj_name) helix_obj.Shape = helix_shape helix_obj.Label = obj_name # Set position if position != (0, 0, 0): helix_obj.Placement.Base = App.Vector(*position) # Recompute document doc.recompute() # Calculate length length = helix_shape.Length return { "success": True, "object_name": helix_obj.Name, "label": helix_obj.Label, "message": f"Created {direction}-handed helix R{radius}mm P{pitch}mm H{height}mm at {position}", "properties": { "radius": radius, "pitch": pitch, "height": height, "direction": direction, "turns": round(turns, 2), "length": round(length, 2), "position": position, }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to create helix: {str(e)}", } def get_available_operations(self) -> Dict[str, Any]: """Get list of available advanced operations. Returns: Dictionary with available operations and their parameters """ return { "advanced_operations": { "extrude": { "description": "Extrude a 2D profile to create a 3D solid", "parameters": ["profile_name", "direction", "distance", "name"], }, "revolve": { "description": "Revolve a 2D profile around an axis", "parameters": [ "profile_name", "axis_point", "axis_direction", "angle", "name", ], }, "loft": { "description": "Create a loft between multiple 2D profiles", "parameters": ["profile_names", "solid", "ruled", "name"], }, "sweep": { "description": "Sweep a 2D profile along a 3D path", "parameters": ["profile_name", "path_name", "frenet", "name"], }, "helix": { "description": "Create a helical curve", "parameters": [ "radius", "pitch", "height", "direction", "position", "name", ], }, } }

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