Files
IFC-Exporter/utils/materials.py
T
NLSA fad461c767
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
update instancing
2026-03-25 13:49:27 +01:00

170 lines
6.1 KiB
Python

# =============================================================================
# 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._apply_count: int = 0
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 get_style_with_fallbacks(self, primary_app_id: str = None,
fallback_app_ids: list = None,
definition_id: str = None):
"""Try primary app_id first, then each fallback, then definition_id. Return style or None."""
if primary_app_id:
style = self.get_style(primary_app_id)
if style:
return style
for aid in (fallback_app_ids or []):
style = self.get_style(aid)
if style:
return style
if definition_id:
style = self.get_style(definition_id)
if style:
return style
return None
def apply_to_item(self, item, mesh_app_id: str):
"""Assign the material style to a single IFC geometry item (e.g. IfcPolygonalFaceSet)."""
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