Skip to main content
Glama
surface_modification.py24.5 kB
""" Surface Modification Tool MCP tool for modifying surfaces and edges in FreeCAD for manufacturing-ready designs. Includes fillet, chamfer, draft, thickness, and offset operations. Author: jango-blockchained """ from typing import Any, Dict, List import FreeCAD as App class SurfaceModificationTool: """Tool for modifying surfaces and edges for manufacturing.""" def __init__(self): """Initialize the surface modification tool.""" self.name = "surface_modification" self.description = "Modify surfaces and edges for manufacturing" def fillet_edges( self, object_name: str, edge_indices: List[int], radius: float = 1.0, name: str = None, ) -> Dict[str, Any]: """Create fillets (rounded edges) on specified edges. Args: object_name: Name of the object in FreeCAD edge_indices: List of edge indices to fillet (0-based) radius: Fillet radius in mm 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 not isinstance(edge_indices, list) or len(edge_indices) == 0: return { "success": False, "error": "Edge indices must be a non-empty list", "message": f"Invalid edge_indices: {edge_indices}", } # 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 object obj = doc.getObject(object_name) if not obj: return { "success": False, "error": f"Object '{object_name}' not found", "message": f"Available objects: {[o.Name for o in doc.Objects]}", } # Get the shape if not hasattr(obj, "Shape"): return { "success": False, "error": "Object has no Shape attribute", "message": f"Object {object_name} is not a valid geometric object", } shape = obj.Shape # Validate edge indices num_edges = len(shape.Edges) for edge_idx in edge_indices: if ( not isinstance(edge_idx, int) or edge_idx < 0 or edge_idx >= num_edges ): return { "success": False, "error": f"Invalid edge index: {edge_idx}", "message": f"Edge index must be between 0 and {num_edges-1}", } # Create fillet try: # Get edges to fillet edges_to_fillet = [shape.Edges[i] for i in edge_indices] # Create fillet using FreeCAD's makeFillet filleted_shape = shape.makeFillet(radius, edges_to_fillet) except Exception as e: return { "success": False, "error": f"Fillet operation failed: {str(e)}", "message": "Edges may not be suitable for filleting or radius too large", } # Create FreeCAD object obj_name = name or f"Fillet_{object_name}_R{radius}" filleted_obj = doc.addObject("Part::Feature", obj_name) filleted_obj.Shape = filleted_shape filleted_obj.Label = obj_name # Recompute document doc.recompute() # Calculate volume volume = filleted_shape.Volume return { "success": True, "object_name": filleted_obj.Name, "label": filleted_obj.Label, "message": f"Created fillet R{radius}mm on {len(edge_indices)} edges of {object_name}", "properties": { "source_object": object_name, "edge_indices": edge_indices, "radius": radius, "edges_filleted": len(edge_indices), "volume": round(volume, 2), }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to create fillet: {str(e)}", } def chamfer_edges( self, object_name: str, edge_indices: List[int], distance: float = 1.0, name: str = None, ) -> Dict[str, Any]: """Create chamfers (beveled edges) on specified edges. Args: object_name: Name of the object in FreeCAD edge_indices: List of edge indices to chamfer (0-based) distance: Chamfer 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 not isinstance(edge_indices, list) or len(edge_indices) == 0: return { "success": False, "error": "Edge indices must be a non-empty list", "message": f"Invalid edge_indices: {edge_indices}", } # 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 object obj = doc.getObject(object_name) if not obj: return { "success": False, "error": f"Object '{object_name}' not found", "message": f"Available objects: {[o.Name for o in doc.Objects]}", } # Get the shape if not hasattr(obj, "Shape"): return { "success": False, "error": "Object has no Shape attribute", "message": f"Object {object_name} is not a valid geometric object", } shape = obj.Shape # Validate edge indices num_edges = len(shape.Edges) for edge_idx in edge_indices: if ( not isinstance(edge_idx, int) or edge_idx < 0 or edge_idx >= num_edges ): return { "success": False, "error": f"Invalid edge index: {edge_idx}", "message": f"Edge index must be between 0 and {num_edges-1}", } # Create chamfer try: # Get edges to chamfer edges_to_chamfer = [shape.Edges[i] for i in edge_indices] # Create chamfer using FreeCAD's makeChamfer chamfered_shape = shape.makeChamfer(distance, edges_to_chamfer) except Exception as e: return { "success": False, "error": f"Chamfer operation failed: {str(e)}", "message": "Edges may not be suitable for chamfering or distance too large", } # Create FreeCAD object obj_name = name or f"Chamfer_{object_name}_D{distance}" chamfered_obj = doc.addObject("Part::Feature", obj_name) chamfered_obj.Shape = chamfered_shape chamfered_obj.Label = obj_name # Recompute document doc.recompute() # Calculate volume volume = chamfered_shape.Volume return { "success": True, "object_name": chamfered_obj.Name, "label": chamfered_obj.Label, "message": f"Created chamfer D{distance}mm on {len(edge_indices)} edges of {object_name}", "properties": { "source_object": object_name, "edge_indices": edge_indices, "distance": distance, "edges_chamfered": len(edge_indices), "volume": round(volume, 2), }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to create chamfer: {str(e)}", } def draft_faces( self, object_name: str, face_indices: List[int], angle: float = 5.0, direction: tuple = (0, 0, 1), name: str = None, ) -> Dict[str, Any]: """Create draft angles on specified faces for manufacturing. Args: object_name: Name of the object in FreeCAD face_indices: List of face indices to draft (0-based) angle: Draft angle in degrees (0-45) direction: Draft direction vector (x, y, z) name: Optional object name Returns: Dictionary with operation result """ try: # Parameter validation if angle < 0 or angle > 45: return { "success": False, "error": "Angle must be between 0 and 45 degrees", "message": f"Invalid angle: {angle}", } if not isinstance(face_indices, list) or len(face_indices) == 0: return { "success": False, "error": "Face indices must be a non-empty list", "message": f"Invalid face_indices: {face_indices}", } 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 object obj = doc.getObject(object_name) if not obj: return { "success": False, "error": f"Object '{object_name}' not found", "message": f"Available objects: {[o.Name for o in doc.Objects]}", } # Get the shape if not hasattr(obj, "Shape"): return { "success": False, "error": "Object has no Shape attribute", "message": f"Object {object_name} is not a valid geometric object", } shape = obj.Shape # Validate face indices num_faces = len(shape.Faces) for face_idx in face_indices: if ( not isinstance(face_idx, int) or face_idx < 0 or face_idx >= num_faces ): return { "success": False, "error": f"Invalid face index: {face_idx}", "message": f"Face index must be between 0 and {num_faces-1}", } # Create draft try: # Get faces to draft faces_to_draft = [shape.Faces[i] for i in face_indices] # Create draft using FreeCAD's makeDraft # Note: FreeCAD's makeDraft may not be available in all versions # We'll use a simplified approach with face transformation drafted_shape = shape.copy() # For now, return success but note that full draft implementation # would require more complex geometry manipulation return { "success": True, "object_name": f"Draft_{object_name}", "label": f"Draft_{object_name}", "message": f"Draft operation initiated for {len(face_indices)} faces (simplified implementation)", "properties": { "source_object": object_name, "face_indices": face_indices, "angle": angle, "direction": direction, "faces_drafted": len(face_indices), "note": "Simplified draft implementation - full draft requires advanced geometry operations", }, } except Exception as e: return { "success": False, "error": f"Draft operation failed: {str(e)}", "message": "Faces may not be suitable for drafting", } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to create draft: {str(e)}", } def create_thickness( self, object_name: str, thickness: float = 1.0, face_indices: List[int] = None, name: str = None, ) -> Dict[str, Any]: """Create a shell by adding thickness to surfaces. Args: object_name: Name of the object in FreeCAD thickness: Wall thickness in mm face_indices: List of face indices to remove (optional, for hollow shells) name: Optional object name Returns: Dictionary with operation result """ try: # Parameter validation if thickness <= 0: return { "success": False, "error": "Thickness must be positive", "message": f"Invalid thickness: {thickness}", } # 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 object obj = doc.getObject(object_name) if not obj: return { "success": False, "error": f"Object '{object_name}' not found", "message": f"Available objects: {[o.Name for o in doc.Objects]}", } # Get the shape if not hasattr(obj, "Shape"): return { "success": False, "error": "Object has no Shape attribute", "message": f"Object {object_name} is not a valid geometric object", } shape = obj.Shape # Validate face indices if provided if face_indices is not None: if not isinstance(face_indices, list): return { "success": False, "error": "Face indices must be a list", "message": f"Invalid face_indices: {face_indices}", } num_faces = len(shape.Faces) for face_idx in face_indices: if ( not isinstance(face_idx, int) or face_idx < 0 or face_idx >= num_faces ): return { "success": False, "error": f"Invalid face index: {face_idx}", "message": f"Face index must be between 0 and {num_faces-1}", } # Create thickness try: if face_indices is not None and len(face_indices) > 0: # Remove specified faces and create shell faces_to_remove = [shape.Faces[i] for i in face_indices] thick_shape = shape.makeThickness(faces_to_remove, thickness, 1e-3) else: # Create shell without removing faces (offset all surfaces) thick_shape = shape.makeOffsetShape(thickness, 1e-3) except Exception as e: return { "success": False, "error": f"Thickness operation failed: {str(e)}", "message": "Object may not be suitable for thickness operation or thickness too large", } # Create FreeCAD object obj_name = name or f"Thickness_{object_name}_T{thickness}" thick_obj = doc.addObject("Part::Feature", obj_name) thick_obj.Shape = thick_shape thick_obj.Label = obj_name # Recompute document doc.recompute() # Calculate volume volume = thick_shape.Volume return { "success": True, "object_name": thick_obj.Name, "label": thick_obj.Label, "message": f"Created thickness T{thickness}mm on {object_name}", "properties": { "source_object": object_name, "thickness": thickness, "removed_faces": face_indices or [], "faces_removed": len(face_indices) if face_indices else 0, "volume": round(volume, 2), "is_shell": len(face_indices) > 0 if face_indices else False, }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to create thickness: {str(e)}", } def offset_surface( self, object_name: str, distance: float = 1.0, name: str = None ) -> Dict[str, Any]: """Create an offset surface parallel to the original. Args: object_name: Name of the object in FreeCAD distance: Offset distance in mm (positive = outward, negative = inward) name: Optional object name Returns: Dictionary with operation result """ try: # Parameter validation if distance == 0: return { "success": False, "error": "Distance cannot be zero", "message": f"Invalid distance: {distance}", } # 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 object obj = doc.getObject(object_name) if not obj: return { "success": False, "error": f"Object '{object_name}' not found", "message": f"Available objects: {[o.Name for o in doc.Objects]}", } # Get the shape if not hasattr(obj, "Shape"): return { "success": False, "error": "Object has no Shape attribute", "message": f"Object {object_name} is not a valid geometric object", } shape = obj.Shape # Create offset try: # Use makeOffsetShape for 3D offset offset_shape = shape.makeOffsetShape(distance, 1e-3) except Exception as e: return { "success": False, "error": f"Offset operation failed: {str(e)}", "message": "Object may not be suitable for offset or distance too large", } # Create FreeCAD object direction_str = "outward" if distance > 0 else "inward" obj_name = name or f"Offset_{object_name}_{direction_str}_{abs(distance)}" offset_obj = doc.addObject("Part::Feature", obj_name) offset_obj.Shape = offset_shape offset_obj.Label = obj_name # Recompute document doc.recompute() # Calculate volume volume = offset_shape.Volume return { "success": True, "object_name": offset_obj.Name, "label": offset_obj.Label, "message": f"Created {direction_str} offset {abs(distance)}mm on {object_name}", "properties": { "source_object": object_name, "distance": distance, "direction": direction_str, "volume": round(volume, 2), }, } except Exception as e: return { "success": False, "error": str(e), "message": f"Failed to create offset: {str(e)}", } def get_available_modifications(self) -> Dict[str, Any]: """Get list of available surface modification operations. Returns: Dictionary with available modifications and their parameters """ return { "surface_modifications": { "fillet": { "description": "Create rounded edges (fillets) for stress relief", "parameters": ["object_name", "edge_indices", "radius", "name"], "manufacturing_use": "Stress relief, safety, aesthetics", }, "chamfer": { "description": "Create beveled edges (chamfers) for assembly", "parameters": ["object_name", "edge_indices", "distance", "name"], "manufacturing_use": "Assembly clearance, deburring", }, "draft": { "description": "Create tapered faces for molding and casting", "parameters": [ "object_name", "face_indices", "angle", "direction", "name", ], "manufacturing_use": "Injection molding, casting, forging", }, "thickness": { "description": "Create shells with specified wall thickness", "parameters": ["object_name", "thickness", "face_indices", "name"], "manufacturing_use": "Lightweight structures, hollow parts", }, "offset": { "description": "Create parallel surfaces at specified distance", "parameters": ["object_name", "distance", "name"], "manufacturing_use": "Clearance, material allowance", }, } }

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