Skip to main content
Glama
material.py19 kB
import logging import os 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 MaterialResourceProvider(ResourceProvider): """Resource provider for materials in CAD models.""" def __init__(self, freecad_app=None): """ Initialize the material 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 self._materials_cache = None if self.app is None: try: import FreeCAD self.app = FreeCAD logger.info("Connected to FreeCAD for material 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 material data from the CAD model. Args: uri: The resource URI in format "cad://materials/[resource_type]/[object_name]" params: Optional parameters for the material request Returns: The material data """ logger.info(f"Retrieving material 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] != "materials": raise ValueError( f"Invalid URI format: {uri}, expected 'cad://materials/...'" ) # Handle different resource types if len(path_parts) == 1: # Return list of available materials return await self._get_available_materials() resource_type = path_parts[1] if resource_type == "library": # Return materials from the material library return await self._get_material_library() elif resource_type == "object": # Return material assigned to a specific object if len(path_parts) < 3: return {"error": "No object specified"} return await self._get_object_material(path_parts[2]) elif resource_type == "info": # Return detailed information about a specific material if len(path_parts) < 3: return {"error": "No material specified"} return await self._get_material_info(path_parts[2]) else: raise ValueError(f"Unknown resource type: {resource_type}") async def _get_available_materials(self) -> Dict[str, Any]: """Get list of all available materials.""" if self.app is None: return self._mock_available_materials() try: # Cache materials if not already done if self._materials_cache is None: self._materials_cache = self._load_material_library() # Create a list of material names and categories materials = [] for material_name, material_data in self._materials_cache.items(): materials.append( { "name": material_name, "category": material_data.get("category", "Unknown"), "description": material_data.get("description", ""), } ) return { "resource_type": "materials", "count": len(materials), "materials": materials, } except Exception as e: logger.error(f"Error getting available materials: {e}") return {"error": f"Error getting available materials: {str(e)}"} def _mock_available_materials(self) -> Dict[str, Any]: """Provide mock material data when FreeCAD is not available.""" return { "resource_type": "materials", "count": 5, "materials": [ { "name": "Steel", "category": "Metal", "description": "Standard steel material", }, { "name": "Aluminum", "category": "Metal", "description": "Standard aluminum alloy", }, { "name": "PLA", "category": "Plastic", "description": "Standard PLA plastic for 3D printing", }, { "name": "Wood", "category": "Natural", "description": "Generic wood material", }, { "name": "Glass", "category": "Ceramic", "description": "Standard glass material", }, ], "note": "Mock data (FreeCAD not available)", } async def _get_material_library(self) -> Dict[str, Any]: """Get the full material library structure.""" if self.app is None: return self._mock_material_library() try: # Cache materials if not already done if self._materials_cache is None: self._materials_cache = self._load_material_library() # Organize materials by category categories = {} for material_name, material_data in self._materials_cache.items(): category = material_data.get("category", "Unknown") if category not in categories: categories[category] = [] categories[category].append( { "name": material_name, "description": material_data.get("description", ""), } ) return { "resource_type": "material_library", "categories": [ {"name": category, "materials": materials} for category, materials in categories.items() ], } except Exception as e: logger.error(f"Error getting material library: {e}") return {"error": f"Error getting material library: {str(e)}"} def _mock_material_library(self) -> Dict[str, Any]: """Provide mock material library when FreeCAD is not available.""" return { "resource_type": "material_library", "categories": [ { "name": "Metal", "materials": [ {"name": "Steel", "description": "Standard steel material"}, {"name": "Aluminum", "description": "Standard aluminum alloy"}, {"name": "Copper", "description": "Standard copper material"}, ], }, { "name": "Plastic", "materials": [ { "name": "PLA", "description": "Standard PLA plastic for 3D printing", }, { "name": "ABS", "description": "Acrylonitrile Butadiene Styrene plastic", }, ], }, { "name": "Natural", "materials": [ {"name": "Wood", "description": "Generic wood material"} ], }, { "name": "Ceramic", "materials": [ {"name": "Glass", "description": "Standard glass material"} ], }, ], "note": "Mock data (FreeCAD not available)", } async def _get_object_material(self, object_name: str) -> Dict[str, Any]: """Get material assigned to a specific object.""" if self.app is None: return self._mock_object_material(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}"} # Check if object has material material_obj = None # In FreeCAD, material can be attached in different ways depending on the workbench # This is a simplified approach # Check Material property if hasattr(obj, "Material") and obj.Material: material_obj = obj.Material # Check for material group link elif hasattr(obj, "MaterialList") and obj.MaterialList: material_obj = obj.MaterialList[0] if obj.MaterialList else None # If no material found if not material_obj: return { "resource_type": "object_material", "object": object_name, "has_material": False, } # Get material properties material_props = {} if hasattr(material_obj, "Material") and material_obj.Material: material_card = material_obj.Material for key, value in material_card.items(): material_props[key] = value return { "resource_type": "object_material", "object": object_name, "has_material": True, "material_name": material_obj.Label, "properties": material_props, } except Exception as e: logger.error(f"Error getting object material: {e}") return {"error": f"Error getting object material: {str(e)}"} def _mock_object_material(self, object_name: str) -> Dict[str, Any]: """Provide mock object material data when FreeCAD is not available.""" # Simulate having material for some object names has_material = object_name.lower() in ["box001", "cylinder001", "part001"] if not has_material: return { "resource_type": "object_material", "object": object_name, "has_material": False, "note": "Mock data (FreeCAD not available)", } # Mock material data return { "resource_type": "object_material", "object": object_name, "has_material": True, "material_name": "Steel", "properties": { "Density": "7900 kg/m^3", "YoungModulus": "210000 MPa", "PoissonRatio": "0.3", "Color": "0.4, 0.4, 0.4", "ThermalConductivity": "50 W/m/K", "ThermalExpansionCoefficient": "12 µm/m/K", }, "note": "Mock data (FreeCAD not available)", } async def _get_material_info(self, material_name: str) -> Dict[str, Any]: """Get detailed information about a specific material.""" if self.app is None: return self._mock_material_info(material_name) try: # Cache materials if not already done if self._materials_cache is None: self._materials_cache = self._load_material_library() # Find the material if material_name not in self._materials_cache: return {"error": f"Material not found: {material_name}"} material_data = self._materials_cache[material_name] return { "resource_type": "material_info", "name": material_name, "category": material_data.get("category", "Unknown"), "description": material_data.get("description", ""), "properties": material_data.get("properties", {}), } except Exception as e: logger.error(f"Error getting material info: {e}") return {"error": f"Error getting material info: {str(e)}"} def _mock_material_info(self, material_name: str) -> Dict[str, Any]: """Provide mock material information when FreeCAD is not available.""" # Material properties based on name if material_name.lower() == "steel": return { "resource_type": "material_info", "name": "Steel", "category": "Metal", "description": "Standard steel material", "properties": { "Density": "7900 kg/m^3", "YoungModulus": "210000 MPa", "PoissonRatio": "0.3", "Color": "0.4, 0.4, 0.4", "ThermalConductivity": "50 W/m/K", "ThermalExpansionCoefficient": "12 µm/m/K", }, "note": "Mock data (FreeCAD not available)", } elif material_name.lower() == "aluminum": return { "resource_type": "material_info", "name": "Aluminum", "category": "Metal", "description": "Standard aluminum alloy", "properties": { "Density": "2700 kg/m^3", "YoungModulus": "70000 MPa", "PoissonRatio": "0.35", "Color": "0.8, 0.8, 0.8", "ThermalConductivity": "237 W/m/K", "ThermalExpansionCoefficient": "23 µm/m/K", }, "note": "Mock data (FreeCAD not available)", } elif material_name.lower() == "pla": return { "resource_type": "material_info", "name": "PLA", "category": "Plastic", "description": "Standard PLA plastic for 3D printing", "properties": { "Density": "1240 kg/m^3", "YoungModulus": "3500 MPa", "PoissonRatio": "0.36", "Color": "0.9, 0.9, 0.9", "ThermalConductivity": "0.13 W/m/K", "GlassTransitionTemperature": "60 °C", }, "note": "Mock data (FreeCAD not available)", } else: return { "resource_type": "material_info", "name": material_name, "category": "Unknown", "description": "Generic material", "properties": { "Density": "1000 kg/m^3", "YoungModulus": "10000 MPa", "PoissonRatio": "0.3", "Color": "0.8, 0.8, 0.8", }, "note": "Mock data (FreeCAD not available)", } def _load_material_library(self) -> Dict[str, Dict[str, Any]]: """Load materials from FreeCAD's material library.""" materials = {} if not self.app: return materials try: # Get the material library paths if hasattr(self.app, "getResourceDir"): resource_dir = self.app.getResourceDir() material_dirs = [ os.path.join(resource_dir, "Mod", "Material", "StandardMaterial"), os.path.join(resource_dir, "Mod", "Material", "FluidMaterial"), ] # Add user material directory if it exists user_material_dir = os.path.join( self.app.getUserAppDataDir(), "Material" ) if os.path.exists(user_material_dir): material_dirs.append(user_material_dir) # Scan directories for material files for material_dir in material_dirs: if os.path.exists(material_dir): for filename in os.listdir(material_dir): if filename.endswith(".FCMat"): material_path = os.path.join(material_dir, filename) material_name = os.path.splitext(filename)[0] # Read and parse the material file material_data = self._parse_material_file(material_path) if material_data: materials[material_name] = material_data return materials except Exception as e: logger.error(f"Error loading material library: {e}") return {} def _parse_material_file(self, file_path: str) -> Dict[str, Any]: """Parse a FreeCAD material file.""" material_data = {"properties": {}} try: with open(file_path, "r", encoding="utf-8") as f: category = "Unknown" for line in f: line = line.strip() # Skip comments and empty lines if not line or line.startswith(";") or line.startswith("#"): continue # Check for section headers if line.startswith("[") and line.endswith("]"): section = line[1:-1] if section == "FCMat": continue else: category = section continue # Parse key-value pairs if "=" in line: key, value = [x.strip() for x in line.split("=", 1)] if key == "Name": material_data["name"] = value elif key == "Description": material_data["description"] = value elif key == "Father": material_data["parent"] = value else: material_data["properties"][key] = value material_data["category"] = category return material_data except Exception as e: logger.error(f"Error parsing material file {file_path}: {e}") return {}

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