# ============================================================================= # materials.py # Reads renderMaterialProxies from the Speckle root object and applies # IfcSurfaceStyle colours to IFC geometry. # # Structure of renderMaterialProxies: # root.renderMaterialProxies = [ # { # id: "636259b3..." # value: RenderMaterial { # name: "Glass" # diffuse: -16744256 ← ARGB packed int (A=255, R=0, G=128, B=192) # opacity: 0.1 ← 0=transparent, 1=opaque # } # objects: ["a1a6b0c2-...", "d5dd3127-...", ...] ← mesh applicationIds # }, # ... # ] # # Usage: # mgr = MaterialManager(ifc, root) # mgr.apply_to_item(brep_item, mesh_app_id) # ============================================================================= import ifcopenshell import ifcopenshell.api from specklepy.objects.base import Base def _argb_to_rgb(argb_int: int) -> tuple[float, float, float]: """Unpack a signed ARGB int to normalised (R, G, B) floats 0..1.""" unsigned = argb_int & 0xFFFFFFFF r = ((unsigned >> 16) & 0xFF) / 255.0 g = ((unsigned >> 8) & 0xFF) / 255.0 b = (unsigned & 0xFF) / 255.0 return r, g, b def _get(obj, key, default=None): try: val = getattr(obj, key, None) if val is not None: return val except Exception: pass try: val = obj[key] if val is not None: return val except Exception: pass return default class MaterialManager: """ Builds a lookup from mesh applicationId → IfcSurfaceStyle, then applies styles to IFC geometry items. """ def __init__(self, ifc: ifcopenshell.file, root: Base): self._ifc = ifc # mesh applicationId (lowercase) → IfcSurfaceStyle (populated lazily) self._style_map: dict[str, object] = {} # name → IfcSurfaceStyle (cache to avoid duplicates) self._style_cache: dict[str, object] = {} self._build(root) def _build(self, root: Base): """ Parse renderMaterialProxies and store raw material data keyed by mesh applicationId. IFC styles are created lazily (only when actually assigned to geometry) to avoid orphaned IfcSurfaceStyle instances that would fail IFC105 validation. """ proxies = _get(root, "renderMaterialProxies") or [] if not isinstance(proxies, list): proxies = list(proxies) if proxies else [] # mesh applicationId (lowercase) → (name, diffuse_argb, transparency) self._material_data: dict[str, tuple] = {} for proxy in proxies: material = _get(proxy, "value") if material is None: continue name = _get(material, "name") or "Unnamed" diffuse = _get(material, "diffuse") opacity = _get(material, "opacity") if diffuse is None: continue opacity_val = float(opacity) if opacity is not None else 1.0 transparency = max(0.0, min(1.0, 1.0 - opacity_val)) objects = _get(proxy, "objects") or [] for app_id in (objects if isinstance(objects, list) else []): if app_id: self._material_data[str(app_id).lower()] = (name, int(diffuse), transparency) print(f" Materials: {len(self._material_data)} mesh mappings (styles created on demand)") def _get_or_create_style(self, name: str, diffuse_argb: int, transparency: float): """Return cached style or create a new IfcSurfaceStyle.""" cache_key = f"{name}|{diffuse_argb}|{transparency:.4f}" if cache_key in self._style_cache: return self._style_cache[cache_key] r, g, b = _argb_to_rgb(diffuse_argb) style = ifcopenshell.api.run("style.add_style", self._ifc, name=name) ifcopenshell.api.run( "style.add_surface_style", self._ifc, style=style, ifc_class="IfcSurfaceStyleRendering", attributes={ "SurfaceColour": {"Name": None, "Red": r, "Green": g, "Blue": b}, "Transparency": transparency, "ReflectanceMethod": "NOTDEFINED", }, ) self._style_cache[cache_key] = style return style def get_style(self, mesh_app_id: str): """Return the IfcSurfaceStyle for a mesh applicationId (created on demand), or None.""" key = str(mesh_app_id).lower() # Return already-created style if cached if key in self._style_map: return self._style_map[key] # Create style now only if this mesh has material data data = self._material_data.get(key) if data is None: return None name, diffuse, transparency = data style = self._get_or_create_style(name, diffuse, transparency) self._style_map[key] = style return style def apply_to_item(self, item, mesh_app_id: str): """Assign the material style to a single IFC geometry item (e.g. IfcFacetedBrep).""" style = self.get_style(mesh_app_id) if style is None: return try: ifcopenshell.api.run( "style.assign_item_style", self._ifc, item=item, style=style, ) except Exception as e: pass # Non-fatal — geometry still exports without colour