Files
IFC-Exporter/utils/mapper.py
T
2026-03-13 08:56:29 +01:00

313 lines
14 KiB
Python

# =============================================================================
# mapper.py
# Maps Speckle objects → IFC entity classes.
#
# Strategy (priority order):
# 1. builtInCategory (OST_ enum from properties.builtInCategory) — most reliable
# 2. speckle_type prefix match — for typed Speckle objects
# 3. category_name string (traversal context) — display name fallback
# 4. IfcBuildingElementProxy — last resort
#
# builtInCategory values: https://www.revitapidocs.com/2019/ba1c5b30-242f-5fdc-8ea9-ec3b61e6e722.htm
# =============================================================================
# --- OST_ BuiltInCategory → IFC class (primary lookup) ---
BUILTIN_CATEGORY_MAP: dict[str, str] = {
# Architectural - Walls
"OST_Walls": "IfcWall",
"OST_CurtainWallPanels": "IfcCurtainWall",
"OST_CurtainWallMullions": "IfcMember",
"OST_Fascia": "IfcCovering",
"OST_Gutters": "IfcPipeSegment",
# Architectural - Floors / Roofs / Ceilings
"OST_Floors": "IfcSlab",
"OST_Roofs": "IfcRoof",
"OST_Ceilings": "IfcCovering",
"OST_RoofSoffit": "IfcCovering",
# Architectural - Doors / Windows / Openings
"OST_Doors": "IfcDoor",
"OST_Windows": "IfcWindow",
"OST_CurtainWallFamilies": "IfcCurtainWall",
"OST_Skylights": "IfcWindow",
# Architectural - Stairs / Ramps / Railings
"OST_Stairs": "IfcStair",
"OST_StairsRailing": "IfcRailing",
"OST_RailingTopRail": "IfcRailing",
"OST_Ramps": "IfcRamp",
"OST_StairsLandings": "IfcStairFlight",
"OST_StairsRuns": "IfcStairFlight",
"OST_StairsSupports": "IfcMember",
# Architectural - Rooms / Spaces
"OST_Rooms": "IfcSpace",
"OST_Parking": "IfcSpace",
"OST_Areas": "IfcSpace",
# Architectural - Furniture / Casework
"OST_Furniture": "IfcFurnishingElement",
"OST_FurnitureSystems": "IfcFurnishingElement",
"OST_Casework": "IfcFurnishingElement",
"OST_SpecialtyEquipment": "IfcFurnishingElement",
"OST_Entourage": "IfcFurnishingElement",
# Structural
"OST_StructuralColumns": "IfcColumn",
"OST_Columns": "IfcColumn",
"OST_StructuralFraming": "IfcBeam",
"OST_StructuralFoundation": "IfcFooting",
"OST_FoundationSlab": "IfcSlab",
"OST_StructuralStiffener": "IfcMember",
"OST_StructuralTruss": "IfcMember",
"OST_StructuralConnectionModel": "IfcMechanicalFastener",
"OST_Rebar": "IfcReinforcingBar",
"OST_FabricAreas": "IfcReinforcingMesh",
"OST_FabricReinforcement": "IfcReinforcingMesh",
# MEP - HVAC
"OST_DuctCurves": "IfcDuctSegment",
"OST_DuctFitting": "IfcDuctFitting",
"OST_DuctAccessory": "IfcDuctSegment",
"OST_DuctTerminal": "IfcAirTerminal",
"OST_FlexDuctCurves": "IfcDuctSegment",
"OST_MechanicalEquipment": "IfcUnitaryEquipment",
"OST_AirTerminal": "IfcAirTerminal",
# MEP - Plumbing
"OST_PipeCurves": "IfcPipeSegment",
"OST_PipeFitting": "IfcPipeFitting",
"OST_PipeAccessory": "IfcPipeSegment",
"OST_FlexPipeCurves": "IfcPipeSegment",
"OST_PlumbingFixtures": "IfcSanitaryTerminal",
"OST_Sprinklers": "IfcFireSuppressionTerminal",
# MEP - Electrical
"OST_ElectricalEquipment": "IfcElectricDistributionBoard",
"OST_ElectricalFixtures": "IfcElectricAppliance",
"OST_LightingFixtures": "IfcLightFixture",
"OST_LightingDevices": "IfcLightFixture",
"OST_CableTray": "IfcCableCarrierSegment",
"OST_CableTrayFitting": "IfcCableCarrierFitting",
"OST_Conduit": "IfcCableCarrierSegment",
"OST_ConduitFitting": "IfcCableCarrierFitting",
"OST_CommunicationDevices": "IfcElectricAppliance",
"OST_DataDevices": "IfcElectricAppliance",
"OST_FireAlarmDevices": "IfcAlarm",
"OST_SecurityDevices": "IfcAlarm",
"OST_NurseCallDevices": "IfcElectricAppliance",
# Site / Civil
"OST_Site": "IfcSite",
"OST_Topography": "IfcGeographicElement",
"OST_Toposolid": "IfcGeographicElement",
"OST_Roads": "IfcRoad",
"OST_Hardscape": "IfcPavement",
"OST_Planting": "IfcGeographicElement",
"OST_SiteSurface": "IfcGeographicElement",
# Generic / Annotation (skip or proxy)
"OST_GenericModel": "IfcBuildingElementProxy",
"OST_Mass": "IfcBuildingElementProxy",
"OST_DetailComponents": "IfcAnnotation",
"OST_Lines": "IfcAnnotation",
"OST_Grids": "IfcGrid",
"OST_Levels": "IfcBuildingStorey",
"OST_Views": "IfcAnnotation",
}
# --- speckle_type → IFC class (secondary lookup) ---
SPECKLE_TYPE_MAP: dict[str, str] = {
"Objects.BuiltElements.Wall": "IfcWall",
"Objects.BuiltElements.Floor": "IfcSlab",
"Objects.BuiltElements.Roof": "IfcRoof",
"Objects.BuiltElements.Column": "IfcColumn",
"Objects.BuiltElements.Beam": "IfcBeam",
"Objects.BuiltElements.Brace": "IfcMember",
"Objects.BuiltElements.Duct": "IfcDuctSegment",
"Objects.BuiltElements.Pipe": "IfcPipeSegment",
"Objects.BuiltElements.Wire": "IfcCableCarrierSegment",
"Objects.BuiltElements.Opening": "IfcOpeningElement",
"Objects.BuiltElements.Room": "IfcSpace",
"Objects.BuiltElements.Ceiling": "IfcCovering",
"Objects.BuiltElements.Stair": "IfcStair",
"Objects.BuiltElements.Ramp": "IfcRamp",
"Objects.BuiltElements.Foundation": "IfcFooting",
"Objects.BuiltElements.Grid": "IfcGrid",
"Objects.BuiltElements.Level": "IfcBuildingStorey",
"Objects.BuiltElements.Revit.RevitWall": "IfcWall",
"Objects.BuiltElements.Revit.RevitFloor": "IfcSlab",
"Objects.BuiltElements.Revit.RevitRoof": "IfcRoof",
"Objects.BuiltElements.Revit.RevitColumn": "IfcColumn",
"Objects.BuiltElements.Revit.RevitBeam": "IfcBeam",
"Objects.BuiltElements.Revit.RevitBrace": "IfcMember",
"Objects.BuiltElements.Revit.RevitDuct": "IfcDuctSegment",
"Objects.BuiltElements.Revit.RevitPipe": "IfcPipeSegment",
"Objects.BuiltElements.Revit.RevitRoom": "IfcSpace",
"Objects.BuiltElements.Revit.RevitStair": "IfcStair",
"Objects.BuiltElements.Revit.RevitRailing": "IfcRailing",
"Objects.BuiltElements.Revit.RevitCeiling": "IfcCovering",
"Objects.BuiltElements.Revit.RevitTopography": "IfcGeographicElement",
"Objects.BuiltElements.Revit.RevitElementType": "IfcBuildingElementProxy",
"Objects.Geometry.Mesh": "IfcBuildingElementProxy",
"Objects.Geometry.Brep": "IfcBuildingElementProxy",
}
# --- Display category name → IFC class (tertiary fallback) ---
CATEGORY_MAP: dict[str, str] = {
"Walls": "IfcWall",
"Floors": "IfcSlab",
"Roofs": "IfcRoof",
"Structural Columns": "IfcColumn",
"Columns": "IfcColumn",
"Structural Framing": "IfcBeam",
"Beams": "IfcBeam",
"Ducts": "IfcDuctSegment",
"Pipes": "IfcPipeSegment",
"Conduits": "IfcCableCarrierSegment",
"Cable Trays": "IfcCableCarrierSegment",
"Rooms": "IfcSpace",
"Spaces": "IfcSpace",
"Ceilings": "IfcCovering",
"Stairs": "IfcStair",
"Ramps": "IfcRamp",
"Railings": "IfcRailing",
"Top Rails": "IfcRailing",
"Curtain Panels": "IfcCurtainWall",
"Curtain Wall Mullions": "IfcMember",
"Doors": "IfcDoor",
"Windows": "IfcWindow",
"Furniture": "IfcFurnishingElement",
"Furniture Systems": "IfcFurnishingElement",
"Casework": "IfcFurnishingElement",
"Plumbing Fixtures": "IfcSanitaryTerminal",
"Electrical Fixtures": "IfcElectricAppliance",
"Lighting Fixtures": "IfcLightFixture",
"Mechanical Equipment": "IfcUnitaryEquipment",
"Electrical Equipment": "IfcElectricDistributionBoard",
"Structural Foundations": "IfcFooting",
"Foundation Slabs": "IfcSlab",
"Topography": "IfcGeographicElement",
"Toposolid": "IfcGeographicElement",
"Planting": "IfcGeographicElement",
"Site": "IfcSite",
"Parking": "IfcSpace",
"Generic Models": "IfcBuildingElementProxy",
"Mass": "IfcBuildingElementProxy",
"Specialty Equipment": "IfcFurnishingElement",
}
_bic_cache: dict[int, str | None] = {} # id(obj) → builtInCategory
def _get_builtin_category(obj) -> str | None:
"""
Read builtInCategory from obj.properties.builtInCategory.
Returns the OST_ string or None. Cached per object.
"""
oid = id(obj)
if oid in _bic_cache:
return _bic_cache[oid]
result = None
try:
props = getattr(obj, "properties", None)
if props is None:
try:
props = obj["properties"]
except Exception:
pass
if props is not None:
val = getattr(props, "builtInCategory", None)
if val is None:
try:
val = props["builtInCategory"]
except Exception:
pass
if val and isinstance(val, str):
result = val.strip()
except Exception:
pass
_bic_cache[oid] = result
return result
# Pre-computed: sorted prefixes longest-first for early exit on prefix match
_SPECKLE_PREFIXES: list[tuple[str, str]] = sorted(
SPECKLE_TYPE_MAP.items(), key=lambda x: len(x[0]), reverse=True
)
# Pre-computed lowercase category map for substring matching
_CATEGORY_MAP_LOWER: list[tuple[str, str]] = [
(k.lower(), v) for k, v in CATEGORY_MAP.items()
]
# Classification cache: (obj_id, category_name) → ifc_class
_classify_cache: dict[tuple, str] = {}
def classify(obj, category_name: str = "") -> str:
"""
Determine the IFC class for a Speckle object.
Priority:
1. properties.builtInCategory (OST_ enum) — definitive Revit classification
2. speckle_type prefix match
3. category_name from traversal context (display string)
4. obj.category field
5. IfcBuildingElementProxy fallback
"""
cache_key = (id(obj), category_name)
if cache_key in _classify_cache:
return _classify_cache[cache_key]
result = _classify_impl(obj, category_name)
_classify_cache[cache_key] = result
return result
def _classify_impl(obj, category_name: str) -> str:
# 1. builtInCategory — most reliable, direct Revit enum
bic = _get_builtin_category(obj)
if bic and bic in BUILTIN_CATEGORY_MAP:
return BUILTIN_CATEGORY_MAP[bic]
# 2. speckle_type — exact match first, then longest-prefix match
speckle_type = getattr(obj, "speckle_type", "") or ""
if speckle_type:
if speckle_type in SPECKLE_TYPE_MAP:
return SPECKLE_TYPE_MAP[speckle_type]
for prefix, ifc_class in _SPECKLE_PREFIXES:
if speckle_type.startswith(prefix):
return ifc_class
# 3. category_name from traversal context — exact match first
if category_name:
if category_name in CATEGORY_MAP:
return CATEGORY_MAP[category_name]
cat_lower = category_name.lower()
for key_lower, ifc_class in _CATEGORY_MAP_LOWER:
if key_lower in cat_lower:
return ifc_class
# 4. obj.category field
obj_category = getattr(obj, "category", None)
if obj_category and isinstance(obj_category, str):
if obj_category in CATEGORY_MAP:
return CATEGORY_MAP[obj_category]
obj_cat_lower = obj_category.lower()
for key_lower, ifc_class in _CATEGORY_MAP_LOWER:
if key_lower in obj_cat_lower:
return ifc_class
return "IfcBuildingElementProxy"
def reset_caches():
"""Clear module-level caches (call at start of each export run)."""
_bic_cache.clear()
_classify_cache.clear()