Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdd030ba86 |
@@ -9,8 +9,9 @@ from utils.traversal import traverse, print_tree
|
||||
from utils.mapper import classify
|
||||
from utils.geometry import mesh_to_ifc, get_display_instances, _make_placement
|
||||
from utils.instances import is_instance, instance_to_ifc, build_definition_map, print_instance_stats
|
||||
from utils.properties import write_properties, write_common_properties
|
||||
from utils.properties import write_properties, write_common_properties, build_element_name, get_element_tag, get_ifc_guid
|
||||
from utils.writer import create_ifc_scaffold, StoreyManager
|
||||
from utils.type_manager import TypeManager
|
||||
|
||||
|
||||
SPATIAL_STRUCTURE_TYPES = {
|
||||
@@ -59,8 +60,6 @@ def automate_function(
|
||||
print(" Speckle -> IFC4.3 Exporter")
|
||||
print("=" * 60)
|
||||
|
||||
#version_root_object = automate_context.receive_version()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. Receive
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -87,6 +86,7 @@ def automate_function(
|
||||
# ------------------------------------------------------------------ #
|
||||
print("\n🎨 Building material map...")
|
||||
material_manager = MaterialManager(ifc, base)
|
||||
type_manager = TypeManager(ifc)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 4. Traverse & export
|
||||
@@ -106,7 +106,7 @@ def automate_function(
|
||||
skipped_spatial += 1
|
||||
continue
|
||||
|
||||
name = getattr(obj, "name", None) or getattr(obj, "applicationId", None) or getattr(obj, "id", "unnamed")
|
||||
name = build_element_name(obj)
|
||||
storey = storey_manager.get_or_create(level_name)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -114,13 +114,15 @@ def automate_function(
|
||||
# ------------------------------------------------------------------ #
|
||||
if is_instance(obj):
|
||||
rep, placement = instance_to_ifc(ifc, body_context, obj, definition_map, scale=scale, material_manager=material_manager)
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey)
|
||||
write_common_properties(ifc, element, obj, category_name)
|
||||
write_properties(ifc, element, obj)
|
||||
instance_count += 1
|
||||
total += 1
|
||||
if not rep:
|
||||
no_geometry += 1
|
||||
continue
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey,
|
||||
tag=get_element_tag(obj), guid=get_ifc_guid(obj))
|
||||
write_properties(ifc, element, obj, ifc_class=ifc_class, category_name=category_name)
|
||||
type_manager.assign(element, obj, ifc_class)
|
||||
instance_count += 1
|
||||
total += 1
|
||||
|
||||
else:
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -131,12 +133,12 @@ def automate_function(
|
||||
|
||||
# B1: Mesh geometry on the parent object
|
||||
rep, placement = mesh_to_ifc(ifc, body_context, obj, scale=scale, material_manager=material_manager)
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey)
|
||||
write_common_properties(ifc, element, obj, category_name)
|
||||
write_properties(ifc, element, obj)
|
||||
total += 1
|
||||
if not rep:
|
||||
no_geometry += 1
|
||||
if rep:
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey,
|
||||
tag=get_element_tag(obj), guid=get_ifc_guid(obj))
|
||||
write_properties(ifc, element, obj, ifc_class=ifc_class, category_name=category_name)
|
||||
type_manager.assign(element, obj, ifc_class)
|
||||
total += 1
|
||||
|
||||
# B2: Instance objects nested inside displayValue
|
||||
# Each becomes its own IFC element (same class as parent)
|
||||
@@ -146,15 +148,20 @@ def automate_function(
|
||||
inst_rep, inst_placement = instance_to_ifc(
|
||||
ifc, body_context, inst, definition_map, scale=scale, material_manager=material_manager
|
||||
)
|
||||
if not inst_rep:
|
||||
no_geometry += 1
|
||||
continue
|
||||
inst_element = _create_element(
|
||||
ifc, ifc_class, name, inst_rep, inst_placement, storey
|
||||
)
|
||||
write_common_properties(ifc, inst_element, obj, category_name)
|
||||
write_properties(ifc, inst_element, obj)
|
||||
write_properties(ifc, inst_element, obj, ifc_class=ifc_class, category_name=category_name)
|
||||
type_manager.assign(inst_element, obj, ifc_class)
|
||||
instance_count += 1
|
||||
total += 1
|
||||
if not inst_rep:
|
||||
no_geometry += 1
|
||||
|
||||
# Track if neither path produced geometry
|
||||
if not rep and not nested_instances:
|
||||
no_geometry += 1
|
||||
|
||||
if total % 100 == 0:
|
||||
print(f" ... processed {total} elements")
|
||||
@@ -162,6 +169,9 @@ def automate_function(
|
||||
# ------------------------------------------------------------------ #
|
||||
# 5. Write output
|
||||
# ------------------------------------------------------------------ #
|
||||
print("\n🔗 Flushing type relationships...")
|
||||
type_manager.flush()
|
||||
|
||||
file_name = function_inputs.file_name
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
@@ -181,13 +191,20 @@ def automate_function(
|
||||
print_instance_stats()
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
def _create_element(ifc, ifc_class, name, rep, placement, storey):
|
||||
def _create_element(ifc, ifc_class, name, rep, placement, storey, tag=None, guid=None):
|
||||
"""Helper: create an IFC element, assign geometry + placement + container."""
|
||||
element = ifcopenshell.api.run(
|
||||
"root.create_entity", ifc,
|
||||
ifc_class=ifc_class,
|
||||
name=str(name),
|
||||
)
|
||||
if tag:
|
||||
element.Tag = str(tag)
|
||||
if guid:
|
||||
try:
|
||||
element.GlobalId = guid
|
||||
except Exception:
|
||||
pass
|
||||
if rep and placement:
|
||||
element.Representation = ifc.createIfcProductDefinitionShape(
|
||||
Representations=(rep,)
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -370,4 +370,4 @@ def print_instance_stats():
|
||||
total = _stats["found"] + _stats["not_found"]
|
||||
print(f" Instance resolution: {_stats['found']}/{total} definitions found")
|
||||
if _stats["not_found"] > 0:
|
||||
print(f" ⚠️ {_stats['not_found']} instances had no definition geometry")
|
||||
print(f" ⚠️ {_stats['not_found']} instances had no definition geometry")
|
||||
+156
-33
@@ -1,16 +1,123 @@
|
||||
# =============================================================================
|
||||
# mapper.py
|
||||
# Maps Speckle speckle_type strings and Revit category names → IFC entity classes.
|
||||
# Maps Speckle objects → IFC entity classes.
|
||||
#
|
||||
# Strategy:
|
||||
# 1. Try to match speckle_type exactly or by prefix
|
||||
# 2. Fall back to Revit category name (e.g. "Floors" → IfcSlab)
|
||||
# 3. Fall back to IfcBuildingElementProxy if nothing matches
|
||||
# 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
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# --- speckle_type → IFC class ---
|
||||
# Covers Objects.BuiltElements.* from the Speckle Objects kit
|
||||
# --- 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_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_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",
|
||||
@@ -47,7 +154,7 @@ SPECKLE_TYPE_MAP: dict[str, str] = {
|
||||
"Objects.Geometry.Brep": "IfcBuildingElementProxy",
|
||||
}
|
||||
|
||||
# --- Revit category name → IFC class (fallback) ---
|
||||
# --- Display category name → IFC class (tertiary fallback) ---
|
||||
CATEGORY_MAP: dict[str, str] = {
|
||||
"Walls": "IfcWall",
|
||||
"Floors": "IfcSlab",
|
||||
@@ -85,39 +192,63 @@ CATEGORY_MAP: dict[str, str] = {
|
||||
"Parking": "IfcSpace",
|
||||
"Generic Models": "IfcBuildingElementProxy",
|
||||
"Mass": "IfcBuildingElementProxy",
|
||||
"Specialty Equipment": "IfcBuildingElementProxy",
|
||||
"Specialty Equipment": "IfcFurnishingElement",
|
||||
}
|
||||
|
||||
|
||||
def _get_builtin_category(obj) -> str | None:
|
||||
"""
|
||||
Read builtInCategory from obj.properties.builtInCategory.
|
||||
Returns the OST_ string or None.
|
||||
"""
|
||||
try:
|
||||
props = obj["properties"] or getattr(obj, "properties", None)
|
||||
if props is None:
|
||||
return None
|
||||
if hasattr(props, "__getitem__"):
|
||||
val = props["builtInCategory"]
|
||||
else:
|
||||
val = getattr(props, "builtInCategory", None)
|
||||
if val and isinstance(val, str):
|
||||
return val.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def classify(obj, category_name: str = "") -> str:
|
||||
"""
|
||||
Determine the IFC class for a Speckle object.
|
||||
|
||||
With the new Objects.Data.DataObject:Objects.Data.RevitObject speckle_type,
|
||||
category name is now the primary classification signal.
|
||||
|
||||
Args:
|
||||
obj: A specklepy Base object (leaf element).
|
||||
category_name: The Revit category string from the traversal context
|
||||
e.g. "Floors", "Walls", "Structural Columns"
|
||||
|
||||
Returns:
|
||||
An IFC class name string e.g. "IfcWall"
|
||||
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
|
||||
"""
|
||||
speckle_type = getattr(obj, "speckle_type", "") or ""
|
||||
# 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]
|
||||
|
||||
# 1. Category name — PRIMARY lookup for RevitObject types
|
||||
# 2. speckle_type
|
||||
speckle_type = getattr(obj, "speckle_type", "") or ""
|
||||
if speckle_type in SPECKLE_TYPE_MAP:
|
||||
return SPECKLE_TYPE_MAP[speckle_type]
|
||||
for key, ifc_class in SPECKLE_TYPE_MAP.items():
|
||||
if speckle_type.startswith(key):
|
||||
return ifc_class
|
||||
|
||||
# 3. category_name from traversal context
|
||||
if category_name:
|
||||
# Exact match
|
||||
if category_name in CATEGORY_MAP:
|
||||
return CATEGORY_MAP[category_name]
|
||||
# Partial match handles Revit appending IDs e.g. "Structural Framing [12345]"
|
||||
for key, ifc_class in CATEGORY_MAP.items():
|
||||
if key.lower() in category_name.lower():
|
||||
return ifc_class
|
||||
|
||||
# 2. Read 'category' directly off the object itself
|
||||
# Per docs: category is a TOP-LEVEL field on RevitObject, not inside properties
|
||||
# 4. obj.category field
|
||||
obj_category = getattr(obj, "category", None)
|
||||
if obj_category and isinstance(obj_category, str):
|
||||
if obj_category in CATEGORY_MAP:
|
||||
@@ -126,12 +257,4 @@ def classify(obj, category_name: str = "") -> str:
|
||||
if key.lower() in obj_category.lower():
|
||||
return ifc_class
|
||||
|
||||
# 3. speckle_type — fallback for non-RevitObject types (geometry, structural, etc.)
|
||||
if speckle_type in SPECKLE_TYPE_MAP:
|
||||
return SPECKLE_TYPE_MAP[speckle_type]
|
||||
for key, ifc_class in SPECKLE_TYPE_MAP.items():
|
||||
if speckle_type.startswith(key):
|
||||
return ifc_class
|
||||
|
||||
# 4. Last resort
|
||||
return "IfcBuildingElementProxy"
|
||||
+450
-140
@@ -1,177 +1,487 @@
|
||||
# =============================================================================
|
||||
# properties.py
|
||||
# Extracts Revit data from Speckle DataObjects and writes IFC property sets.
|
||||
# Writes IFC property sets matching the structure of Revit's native IFC export.
|
||||
#
|
||||
# Revit parameter structure from the Speckle connector:
|
||||
# obj.properties = {
|
||||
# "elementId": "704282",
|
||||
# "Parameters": {
|
||||
# "Type Parameters": {
|
||||
# "Dimensions": {
|
||||
# "Thickness": {"name": "Thickness", "value": 25.4, "units": "Millimeters", ...}
|
||||
# },
|
||||
# ...
|
||||
# },
|
||||
# "Instance Parameters": {
|
||||
# "Constraints": {
|
||||
# "Level": {"name": "Level", "value": "Level 1", ...}
|
||||
# },
|
||||
# ...
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# Revit native IFC export produces:
|
||||
# - Element Name: "Family:TypeName:ElementId" e.g. "Basic Roof:SG Metal Panels roof:243274"
|
||||
# - Element Tag: ElementId string e.g. "243274"
|
||||
# - Element GlobalId: from IFC Parameters.IfcGUID
|
||||
# - Pset_<EntityType>Common with typed properties (IfcBoolean, IfcIdentifier, etc.)
|
||||
# - Pset_EnvironmentalImpactIndicators with Reference = TypeName
|
||||
#
|
||||
# We flatten this into two IFC property sets:
|
||||
# Pset_RevitTypeParameters — from "Type Parameters"
|
||||
# Pset_RevitInstanceParameters — from "Instance Parameters"
|
||||
# Our Speckle source fields:
|
||||
# obj.family → Family name
|
||||
# obj.type → Type name (= Reference in all Common psets)
|
||||
# properties.elementId → Revit ElementId → Tag
|
||||
# properties.Parameters.Instance Parameters.IFC Parameters.IfcGUID.value → GlobalId
|
||||
# properties.Parameters.Type Parameters.* → typed IFC properties
|
||||
# properties.Parameters.Instance Parameters.* → typed IFC properties
|
||||
# =============================================================================
|
||||
|
||||
import ifcopenshell.api
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
|
||||
def _safe_val(value) -> str | None:
|
||||
"""Convert a value to a clean IFC-safe string."""
|
||||
# ---------------------------------------------------------------------------
|
||||
# IFC entity → standard Common pset name
|
||||
# ---------------------------------------------------------------------------
|
||||
COMMON_PSET: dict[str, str] = {
|
||||
"IfcWall": "Pset_WallCommon",
|
||||
"IfcWallStandardCase": "Pset_WallCommon",
|
||||
"IfcSlab": "Pset_SlabCommon",
|
||||
"IfcRoof": "Pset_RoofCommon",
|
||||
"IfcColumn": "Pset_ColumnCommon",
|
||||
"IfcBeam": "Pset_BeamCommon",
|
||||
"IfcMember": "Pset_MemberCommon",
|
||||
"IfcDoor": "Pset_DoorCommon",
|
||||
"IfcWindow": "Pset_WindowCommon",
|
||||
"IfcStair": "Pset_StairCommon",
|
||||
"IfcStairFlight": "Pset_StairFlightCommon",
|
||||
"IfcRamp": "Pset_RampCommon",
|
||||
"IfcRailing": "Pset_RailingCommon",
|
||||
"IfcCovering": "Pset_CoveringCommon",
|
||||
"IfcCurtainWall": "Pset_CurtainWallCommon",
|
||||
"IfcFooting": "Pset_FootingCommon",
|
||||
"IfcPile": "Pset_PileCommon",
|
||||
"IfcSpace": "Pset_SpaceCommon",
|
||||
"IfcSite": "Pset_SiteCommon",
|
||||
"IfcBuildingStorey": "Pset_BuildingStoreyCommon",
|
||||
"IfcBuilding": "Pset_BuildingCommon",
|
||||
"IfcBuildingElementProxy": "Pset_BuildingElementProxyCommon",
|
||||
"IfcFurnishingElement": "Pset_FurnitureTypeCommon",
|
||||
"IfcLightFixture": "Pset_LightFixtureTypeCommon",
|
||||
"IfcOpeningElement": "Pset_OpeningElementCommon",
|
||||
"IfcPlate": "Pset_PlateCommon",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Revit parameter internal names → (IFC pset property name, IFC value factory)
|
||||
# These are harvested from the Common psets Revit native export produces.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _bool(v):
|
||||
return ("IfcBoolean", bool(v))
|
||||
|
||||
def _identifier(v):
|
||||
return ("IfcIdentifier", str(v))
|
||||
|
||||
def _label(v):
|
||||
return ("IfcLabel", str(v))
|
||||
|
||||
def _real(v):
|
||||
return ("IfcReal", float(v))
|
||||
|
||||
def _thermal(v):
|
||||
return ("IfcThermalTransmittanceMeasure", float(v))
|
||||
|
||||
def _length(v):
|
||||
return ("IfcPositiveLengthMeasure", float(v))
|
||||
|
||||
def _count(v):
|
||||
return ("IfcCountMeasure", int(v))
|
||||
|
||||
def _angle(v):
|
||||
return ("IfcPlaneAngleMeasure", float(v))
|
||||
|
||||
|
||||
# Map: Revit internalDefinitionName → (IFC property name, value factory fn)
|
||||
REVIT_PARAM_TO_IFC: dict[str, tuple] = {
|
||||
# Wall
|
||||
"WALL_ATTR_ROOM_BOUNDING": ("IsExternal", _bool),
|
||||
"WALL_STRUCTURAL_SIGNIFICANT": ("LoadBearing", _bool),
|
||||
"WALL_STRUCTURAL_USAGE_PARAM": ("LoadBearing", _bool),
|
||||
"ANALYTICAL_THERMAL_RESISTANCE": ("ThermalTransmittance", _thermal),
|
||||
"ANALYTICAL_HEAT_TRANSFER_COEFFICIENT": ("ThermalTransmittance", _thermal),
|
||||
|
||||
# Slab / Roof / Floor
|
||||
"HOST_AREA_COMPUTED": ("NetArea", _real),
|
||||
"HOST_VOLUME_COMPUTED": ("NetVolume", _real),
|
||||
"ROOF_SLOPE": ("PitchAngle", _angle),
|
||||
|
||||
# Stair
|
||||
"STAIR_RISER_HEIGHT": ("RiserHeight", _length),
|
||||
"STAIR_TREAD_DEPTH": ("TreadLength", _length),
|
||||
"STAIR_NUMBER_OF_RISERS": ("NumberOfRiser", _count),
|
||||
"STAIR_NUMBER_OF_TREADS": ("NumberOfTreads", _count),
|
||||
"STAIR_NOSING_LENGTH": ("NosingLength", _length),
|
||||
|
||||
# Railing
|
||||
"RAILING_HEIGHT": ("Height", _length),
|
||||
|
||||
# Door / Window
|
||||
"DOOR_FIRE_RATING": ("FireExit", _bool),
|
||||
|
||||
# General identity
|
||||
"ALL_MODEL_FAMILY_NAME": ("Reference", _identifier),
|
||||
"ALL_MODEL_TYPE_NAME": ("Reference", _identifier),
|
||||
"ASSEMBLY_CODE": ("Reference", _identifier),
|
||||
}
|
||||
|
||||
# External category OST_ codes (used to infer IsExternal)
|
||||
EXTERNAL_CATEGORIES = {
|
||||
"OST_Walls", "OST_Roofs", "OST_Windows", "OST_Doors",
|
||||
"OST_CurtainWallPanels", "OST_CurtainWallMullions",
|
||||
"OST_StructuralColumns", "OST_StructuralFraming",
|
||||
"OST_Stairs", "OST_StairsRailing", "OST_Ramps",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_props_dict(obj: Base) -> dict:
|
||||
for key in ["properties", "@properties"]:
|
||||
try:
|
||||
p = obj[key]
|
||||
if p is None:
|
||||
continue
|
||||
if hasattr(p, "get_dynamic_member_names"):
|
||||
return {n: p[n] for n in p.get_dynamic_member_names()}
|
||||
if isinstance(p, dict):
|
||||
return p
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def _get_nested(d: dict, *keys):
|
||||
"""Safely walk nested dicts/objects."""
|
||||
cur = d
|
||||
for k in keys:
|
||||
if cur is None:
|
||||
return None
|
||||
if isinstance(cur, dict):
|
||||
cur = cur.get(k)
|
||||
else:
|
||||
try:
|
||||
cur = cur[k]
|
||||
except Exception:
|
||||
return None
|
||||
return cur
|
||||
|
||||
|
||||
def _param_value(params_block: dict, internal_name: str):
|
||||
"""
|
||||
Search all groups in a parameter block for a param with the given
|
||||
internalDefinitionName. Returns the raw value or None.
|
||||
"""
|
||||
if not isinstance(params_block, dict):
|
||||
return None
|
||||
for group in params_block.values():
|
||||
if not isinstance(group, dict):
|
||||
continue
|
||||
for entry in group.values():
|
||||
if isinstance(entry, dict) and entry.get("internalDefinitionName") == internal_name:
|
||||
return entry.get("value")
|
||||
return None
|
||||
|
||||
|
||||
def _make_prop(ifc, name: str, ifc_type: str, value) -> object | None:
|
||||
"""Create an IfcPropertySingleValue with the correct IFC measure type."""
|
||||
try:
|
||||
nominal = ifc.create_entity(ifc_type, wrappedValue=value)
|
||||
return ifc.create_entity(
|
||||
"IfcPropertySingleValue",
|
||||
Name=name,
|
||||
NominalValue=nominal,
|
||||
)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def _write_pset(ifc, element, pset_name: str, props: list):
|
||||
"""Write an IfcPropertySet with the given list of IfcProperty objects."""
|
||||
if not props:
|
||||
return
|
||||
try:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name=pset_name)
|
||||
# Directly attach the pre-built property objects
|
||||
pset.HasProperties = props
|
||||
except Exception as e:
|
||||
print(f" ⚠️ {pset_name}: {e}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Element name + tag (matching Revit native IFC format)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_element_name(obj: Base) -> str:
|
||||
"""
|
||||
Build element name in Revit native IFC format: "Family:TypeName:ElementId"
|
||||
Falls back gracefully if any part is missing.
|
||||
"""
|
||||
props = _get_props_dict(obj)
|
||||
family = getattr(obj, "family", None) or ""
|
||||
typ = getattr(obj, "type", None) or ""
|
||||
elem_id = props.get("elementId", "") or getattr(obj, "applicationId", "") or ""
|
||||
|
||||
# Treat literal "none" (case-insensitive) the same as empty — Revit exports
|
||||
# placeholder objects with family/type set to the string "none".
|
||||
if family.strip().lower() == "none":
|
||||
family = ""
|
||||
if typ.strip().lower() == "none":
|
||||
typ = ""
|
||||
|
||||
parts = [p for p in [family, typ] if p]
|
||||
if elem_id:
|
||||
parts.append(str(elem_id))
|
||||
return ":".join(parts) if parts else (getattr(obj, "id", None) or "unnamed")
|
||||
|
||||
|
||||
def get_element_tag(obj: Base) -> str | None:
|
||||
"""Return Revit ElementId as the IFC Tag."""
|
||||
props = _get_props_dict(obj)
|
||||
elem_id = props.get("elementId")
|
||||
return str(elem_id) if elem_id else None
|
||||
|
||||
|
||||
def get_ifc_guid(obj: Base) -> str | None:
|
||||
"""
|
||||
Read IfcGUID from the Revit IFC Parameters.
|
||||
Falls back to None (ifcopenshell will auto-generate a GUID).
|
||||
"""
|
||||
props = _get_props_dict(obj)
|
||||
params = props.get("Parameters") or {}
|
||||
inst = params.get("Instance Parameters") or {}
|
||||
ifc_p = inst.get("IFC Parameters") or {}
|
||||
entry = ifc_p.get("IfcGUID") or {}
|
||||
val = entry.get("value") if isinstance(entry, dict) else None
|
||||
return str(val) if val else None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Standard Common pset (Pset_WallCommon etc.)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def write_common_pset(ifc, element, obj: Base, ifc_class: str, category_name: str = ""):
|
||||
"""
|
||||
Write the standard Pset_<Entity>Common property set, matching Revit native export.
|
||||
Properties: Reference (TypeName), IsExternal, LoadBearing, ThermalTransmittance, etc.
|
||||
"""
|
||||
pset_name = COMMON_PSET.get(ifc_class)
|
||||
if not pset_name:
|
||||
return
|
||||
|
||||
props = _get_props_dict(obj)
|
||||
params = props.get("Parameters") or {}
|
||||
type_params = params.get("Type Parameters") or {}
|
||||
inst_params = params.get("Instance Parameters") or {}
|
||||
|
||||
ifc_props = []
|
||||
|
||||
# Reference = TypeName (always present in Revit IFC)
|
||||
type_name = getattr(obj, "type", None) or ""
|
||||
if type_name:
|
||||
p = _make_prop(ifc, "Reference", "IfcIdentifier", type_name)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# IsExternal — derive from builtInCategory or "Constraints" parameters
|
||||
bic = props.get("builtInCategory", "")
|
||||
is_external = bic in EXTERNAL_CATEGORIES
|
||||
if not is_external:
|
||||
# Some elements expose it directly as a parameter
|
||||
ext_val = _param_value(inst_params, "WALL_ATTR_ROOM_BOUNDING")
|
||||
if ext_val is not None:
|
||||
is_external = bool(ext_val)
|
||||
if ifc_class not in {"IfcSpace", "IfcSite", "IfcBuildingStorey", "IfcBuilding",
|
||||
"IfcFurnishingElement", "IfcOpeningElement"}:
|
||||
p = _make_prop(ifc, "IsExternal", "IfcBoolean", is_external)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# LoadBearing — walls, columns, beams, slabs
|
||||
if ifc_class in {"IfcWall", "IfcWallStandardCase", "IfcSlab", "IfcColumn", "IfcBeam"}:
|
||||
lb_val = (_param_value(inst_params, "WALL_STRUCTURAL_SIGNIFICANT") or
|
||||
_param_value(inst_params, "WALL_STRUCTURAL_USAGE_PARAM") or
|
||||
_param_value(type_params, "WALL_STRUCTURAL_SIGNIFICANT"))
|
||||
lb = bool(lb_val) if lb_val is not None else False
|
||||
p = _make_prop(ifc, "LoadBearing", "IfcBoolean", lb)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# ThermalTransmittance — walls, roofs, slabs, doors, windows
|
||||
if ifc_class in {"IfcWall", "IfcWallStandardCase", "IfcRoof", "IfcSlab",
|
||||
"IfcDoor", "IfcWindow"}:
|
||||
u_val = (_param_value(type_params, "ANALYTICAL_HEAT_TRANSFER_COEFFICIENT") or
|
||||
_param_value(inst_params, "ANALYTICAL_HEAT_TRANSFER_COEFFICIENT"))
|
||||
if u_val is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, "ThermalTransmittance", "IfcThermalTransmittanceMeasure", float(u_val))
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# PitchAngle — roofs/slabs
|
||||
if ifc_class in {"IfcRoof", "IfcSlab"}:
|
||||
slope = _param_value(inst_params, "ROOF_SLOPE")
|
||||
if slope is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, "PitchAngle", "IfcPlaneAngleMeasure", float(slope))
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stair-specific
|
||||
if ifc_class in {"IfcStair", "IfcStairFlight"}:
|
||||
for internal, prop_name, factory in [
|
||||
("STAIR_RISER_HEIGHT", "RiserHeight", "IfcPositiveLengthMeasure"),
|
||||
("STAIR_TREAD_DEPTH", "TreadLength", "IfcPositiveLengthMeasure"),
|
||||
("STAIR_NUMBER_OF_RISERS","NumberOfRiser", "IfcCountMeasure"),
|
||||
("STAIR_NUMBER_OF_TREADS","NumberOfTreads", "IfcCountMeasure"),
|
||||
]:
|
||||
v = _param_value(inst_params, internal) or _param_value(type_params, internal)
|
||||
if v is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, prop_name, factory, float(v) if "Measure" in factory else int(v))
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Railing height
|
||||
if ifc_class == "IfcRailing":
|
||||
h = _param_value(inst_params, "RAILING_HEIGHT") or _param_value(type_params, "RAILING_HEIGHT")
|
||||
if h is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, "Height", "IfcPositiveLengthMeasure", float(h))
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_write_pset(ifc, element, pset_name, ifc_props)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pset_EnvironmentalImpactIndicators (always written, Reference = TypeName)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def write_environmental_pset(ifc, element, obj: Base):
|
||||
"""Write Pset_EnvironmentalImpactIndicators with Reference = TypeName."""
|
||||
type_name = getattr(obj, "type", None) or ""
|
||||
if not type_name:
|
||||
return
|
||||
p = _make_prop(ifc, "Reference", "IfcIdentifier", type_name)
|
||||
if p:
|
||||
_write_pset(ifc, element, "Pset_EnvironmentalImpactIndicators", [p])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Custom Revit parameters pset (all remaining instance + type params)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _safe_str(value) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return "Yes" if value else "No"
|
||||
if isinstance(value, float):
|
||||
# Trim excessive decimals
|
||||
return f"{value:.6g}"
|
||||
if isinstance(value, (int, str)):
|
||||
s = str(value).strip()
|
||||
return s if s else None
|
||||
return str(value).strip() or None
|
||||
s = str(value).strip()
|
||||
return s or None
|
||||
|
||||
|
||||
def _extract_param(entry) -> tuple[str, str] | None:
|
||||
"""
|
||||
Given a Revit parameter entry dict like:
|
||||
{"name": "Thickness", "value": 25.4, "units": "Millimeters", ...}
|
||||
Returns (display_name, display_value) or None if unusable.
|
||||
"""
|
||||
if not isinstance(entry, dict):
|
||||
return None
|
||||
name = entry.get("name")
|
||||
value = entry.get("value")
|
||||
if not name or value is None:
|
||||
return None
|
||||
units = entry.get("units", "")
|
||||
# Skip non-informative unit labels
|
||||
def _flatten_params(params_block: dict) -> dict:
|
||||
"""Flatten Type or Instance parameter block into {name: display_value}."""
|
||||
result = {}
|
||||
skip_units = {"", "None", "General", "Currency", "Integer"}
|
||||
val_str = _safe_val(value)
|
||||
if val_str is None:
|
||||
return None
|
||||
if units and units not in skip_units:
|
||||
display = f"{val_str} {units}"
|
||||
else:
|
||||
display = val_str
|
||||
return str(name), display
|
||||
|
||||
|
||||
def _flatten_param_group(group: dict) -> dict:
|
||||
"""
|
||||
Flatten one parameter group (e.g. "Dimensions", "Constraints") dict.
|
||||
Each value is a Revit parameter entry {"name":..., "value":..., "units":...}.
|
||||
Returns {display_name: display_value}.
|
||||
"""
|
||||
result = {}
|
||||
if not isinstance(group, dict):
|
||||
return result
|
||||
for _internal_key, entry in group.items():
|
||||
pair = _extract_param(entry)
|
||||
if pair:
|
||||
name, val = pair
|
||||
result[name] = val
|
||||
return result
|
||||
|
||||
|
||||
def _extract_parameter_block(params_block: dict) -> dict:
|
||||
"""
|
||||
Flatten all groups in a parameter block (Type Parameters or Instance Parameters).
|
||||
Returns a merged {display_name: display_value} dict.
|
||||
"""
|
||||
result = {}
|
||||
if not isinstance(params_block, dict):
|
||||
return result
|
||||
for _group_name, group in params_block.items():
|
||||
result.update(_flatten_param_group(group))
|
||||
return result
|
||||
|
||||
|
||||
def _get_properties_dict(obj: Base) -> dict:
|
||||
"""Extract the raw properties dict from a DataObject."""
|
||||
for key in ["properties", "@properties", "_properties"]:
|
||||
try:
|
||||
props = obj[key]
|
||||
if props is None:
|
||||
continue
|
||||
if hasattr(props, "get_dynamic_member_names"):
|
||||
names = props.get_dynamic_member_names()
|
||||
return {n: props[n] for n in names}
|
||||
if isinstance(props, dict):
|
||||
return props
|
||||
except Exception:
|
||||
for group in params_block.values():
|
||||
if not isinstance(group, dict):
|
||||
continue
|
||||
return {}
|
||||
for entry in group.values():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
name = entry.get("name")
|
||||
value = entry.get("value")
|
||||
units = entry.get("units", "") or ""
|
||||
if not name or value is None:
|
||||
continue
|
||||
val_str = _safe_str(value)
|
||||
if val_str is None:
|
||||
continue
|
||||
display = f"{val_str} {units}".strip() if units not in skip_units else val_str
|
||||
result[name] = display
|
||||
return result
|
||||
|
||||
|
||||
def _write_pset(ifc, element, pset_name: str, props: dict):
|
||||
"""Write a property set if there are any properties."""
|
||||
if not props:
|
||||
return
|
||||
try:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name=pset_name)
|
||||
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties=props)
|
||||
except Exception as e:
|
||||
print(f" ⚠️ {pset_name}: {e}")
|
||||
|
||||
|
||||
def write_properties(ifc, element, obj: Base):
|
||||
def write_revit_params(ifc, element, obj: Base):
|
||||
"""
|
||||
Write Revit parameters as IFC property sets.
|
||||
Creates separate psets for Type and Instance parameters.
|
||||
Write remaining Revit parameters as two custom property sets
|
||||
using the vendor prefix 'RVT_' (not 'Pset_' which is reserved):
|
||||
RVT_TypeParameters — from Type Parameters
|
||||
RVT_InstanceParameters — from Instance Parameters
|
||||
"""
|
||||
props_dict = _get_properties_dict(obj)
|
||||
parameters = props_dict.get("Parameters") or {}
|
||||
props = _get_props_dict(obj)
|
||||
params = props.get("Parameters") or {}
|
||||
|
||||
# Type Parameters → Pset_RevitTypeParameters
|
||||
type_params = parameters.get("Type Parameters") or {}
|
||||
type_flat = _extract_parameter_block(type_params)
|
||||
_write_pset(ifc, element, "RVT_TypeParameters", type_flat)
|
||||
type_flat = _flatten_params(params.get("Type Parameters") or {})
|
||||
inst_flat = _flatten_params(params.get("Instance Parameters") or {})
|
||||
|
||||
# Instance Parameters → Pset_RevitInstanceParameters
|
||||
inst_params = parameters.get("Instance Parameters") or {}
|
||||
inst_flat = _extract_parameter_block(inst_params)
|
||||
_write_pset(ifc, element, "RVT_InstanceParameters", inst_flat)
|
||||
def build_str_props(flat: dict) -> list:
|
||||
out = []
|
||||
for name, val in flat.items():
|
||||
try:
|
||||
nominal = ifc.create_entity("IfcLabel", wrappedValue=val)
|
||||
p = ifc.create_entity("IfcPropertySingleValue", Name=name, NominalValue=nominal)
|
||||
out.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
# Top-level semantic fields → Pset_RevitIdentity
|
||||
type_props = build_str_props(type_flat)
|
||||
inst_props = build_str_props(inst_flat)
|
||||
|
||||
if type_props:
|
||||
_write_pset(ifc, element, "RVT_TypeParameters", type_props)
|
||||
if inst_props:
|
||||
_write_pset(ifc, element, "RVT_InstanceParameters", inst_props)
|
||||
|
||||
# Identity: family, type, elementId, builtInCategory
|
||||
identity = {}
|
||||
for field in ["type", "family", "category", "level"]:
|
||||
for field in ["family", "type", "category"]:
|
||||
val = getattr(obj, field, None)
|
||||
if val and isinstance(val, str) and val.strip():
|
||||
identity[field.capitalize()] = val.strip()
|
||||
# Also include elementId if present
|
||||
elem_id = props_dict.get("elementId")
|
||||
elem_id = props.get("elementId")
|
||||
if elem_id:
|
||||
identity["ElementId"] = str(elem_id)
|
||||
bic = props.get("builtInCategory")
|
||||
if bic:
|
||||
identity["BuiltInCategory"] = str(bic)
|
||||
|
||||
_write_pset(ifc, element, "RVT_Identity", identity)
|
||||
id_props = []
|
||||
for name, val in identity.items():
|
||||
try:
|
||||
nominal = ifc.create_entity("IfcLabel", wrappedValue=val)
|
||||
p = ifc.create_entity("IfcPropertySingleValue", Name=name, NominalValue=nominal)
|
||||
id_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
if id_props:
|
||||
_write_pset(ifc, element, "RVT_Identity", id_props)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — called from main.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def write_properties(ifc, element, obj: Base, ifc_class: str = "", category_name: str = ""):
|
||||
"""
|
||||
Write all property sets for an IFC element, matching Revit native IFC export structure:
|
||||
1. Pset_<Entity>Common — standard typed properties (Reference, IsExternal, etc.)
|
||||
2. Pset_EnvironmentalImpactIndicators — Reference = TypeName
|
||||
3. RVT_TypeParameters — all remaining Revit type parameters
|
||||
4. RVT_InstanceParameters — all remaining Revit instance parameters
|
||||
5. RVT_Identity — family, type, elementId, builtInCategory
|
||||
"""
|
||||
write_common_pset(ifc, element, obj, ifc_class, category_name)
|
||||
write_environmental_pset(ifc, element, obj)
|
||||
write_revit_params(ifc, element, obj)
|
||||
|
||||
|
||||
def write_common_properties(ifc, element, obj: Base, category_name: str = ""):
|
||||
"""
|
||||
Write Pset_SpeckleData for traceability back to the Speckle source object.
|
||||
"""
|
||||
props = {}
|
||||
speckle_id = getattr(obj, "id", None)
|
||||
app_id = getattr(obj, "applicationId", None)
|
||||
speckle_type = getattr(obj, "speckle_type", None)
|
||||
|
||||
if speckle_id: props["SpeckleId"] = str(speckle_id)
|
||||
if app_id: props["ApplicationId"] = str(app_id)
|
||||
if speckle_type: props["SpeckleType"] = str(speckle_type)
|
||||
if category_name: props["RevitCategory"] = str(category_name)
|
||||
|
||||
_write_pset(ifc, element, "RVT_SpeckleData", props)
|
||||
"""Legacy shim — kept for compatibility with main.py call sites."""
|
||||
pass # All handled by write_properties now
|
||||
@@ -0,0 +1,219 @@
|
||||
# =============================================================================
|
||||
# type_manager.py
|
||||
# Creates and caches IfcTypeObjects (IfcWallType, IfcRoofType, etc.) and
|
||||
# links element instances to them via IfcRelDefinesByType.
|
||||
#
|
||||
# Revit native IFC export pattern:
|
||||
# IfcWallType
|
||||
# Name = "Family:TypeName" (no ElementId)
|
||||
# Tag = Type's Revit ElementId (from Instance Parameters > Other > Type Id)
|
||||
# GlobalId = from Type IfcGUID param (from Type Parameters > IFC Parameters > Type IfcGUID)
|
||||
# HasPropertySets:
|
||||
# Pset_WallCommon: IsExternal, ThermalTransmittance (type-level)
|
||||
# Pset_EnvironmentalImpactIndicators: Reference = TypeName
|
||||
# RVT_TypeParameters: all remaining type params
|
||||
#
|
||||
# Type objects are SHARED — multiple instances of the same Revit type
|
||||
# map to one IfcTypeObject, keyed by (ifc_class, family, type_name).
|
||||
# =============================================================================
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
from specklepy.objects.base import Base
|
||||
from utils.properties import (
|
||||
_get_props_dict, _get_nested, _param_value, _make_prop, _write_pset,
|
||||
COMMON_PSET, EXTERNAL_CATEGORIES, _flatten_params
|
||||
)
|
||||
|
||||
|
||||
# IFC element class → IFC type class
|
||||
TYPE_CLASS_MAP: dict[str, str] = {
|
||||
"IfcWall": "IfcWallType",
|
||||
"IfcWallStandardCase": "IfcWallType",
|
||||
"IfcSlab": "IfcSlabType",
|
||||
"IfcRoof": "IfcRoofType",
|
||||
"IfcColumn": "IfcColumnType",
|
||||
"IfcBeam": "IfcBeamType",
|
||||
"IfcMember": "IfcMemberType",
|
||||
"IfcDoor": "IfcDoorType",
|
||||
"IfcWindow": "IfcWindowType",
|
||||
"IfcStair": "IfcStairType",
|
||||
"IfcStairFlight": "IfcStairFlightType",
|
||||
"IfcRamp": "IfcRampType",
|
||||
"IfcRailing": "IfcRailingType",
|
||||
"IfcCovering": "IfcCoveringType",
|
||||
"IfcCurtainWall": "IfcCurtainWallType",
|
||||
"IfcFooting": "IfcFootingType",
|
||||
"IfcBuildingElementProxy": "IfcBuildingElementProxyType",
|
||||
"IfcFurnishingElement": "IfcFurnitureType",
|
||||
"IfcLightFixture": "IfcLightFixtureType",
|
||||
"IfcElectricAppliance": "IfcElectricApplianceType",
|
||||
"IfcElectricDistributionBoard": "IfcElectricDistributionBoardType",
|
||||
"IfcSanitaryTerminal": "IfcSanitaryTerminalType",
|
||||
"IfcUnitaryEquipment": "IfcUnitaryEquipmentType",
|
||||
"IfcDuctSegment": "IfcDuctSegmentType",
|
||||
"IfcPipeSegment": "IfcPipeSegmentType",
|
||||
"IfcCableCarrierSegment": "IfcCableCarrierSegmentType",
|
||||
"IfcPlate": "IfcPlateType",
|
||||
}
|
||||
|
||||
|
||||
class TypeManager:
|
||||
"""
|
||||
Creates IfcTypeObjects on demand and caches them by (ifc_class, family, type_name).
|
||||
Call assign(element, obj, ifc_class) for each exported element.
|
||||
"""
|
||||
|
||||
def __init__(self, ifc: ifcopenshell.file):
|
||||
self._ifc = ifc
|
||||
# key: (ifc_class, family, type_name) → IfcTypeObject
|
||||
self._cache: dict[tuple, object] = {}
|
||||
# type_object → [element, ...] (for batched IfcRelDefinesByType)
|
||||
self._pending: dict[int, list] = {}
|
||||
|
||||
def assign(self, element, obj: Base, ifc_class: str):
|
||||
"""Create (or retrieve cached) type object and queue the assignment."""
|
||||
type_class = TYPE_CLASS_MAP.get(ifc_class)
|
||||
if not type_class:
|
||||
return
|
||||
|
||||
family = getattr(obj, "family", None) or ""
|
||||
type_name = getattr(obj, "type", None) or ""
|
||||
if not type_name:
|
||||
return
|
||||
|
||||
cache_key = (ifc_class, family, type_name)
|
||||
|
||||
if cache_key not in self._cache:
|
||||
type_obj = self._create_type(type_class, family, type_name, obj, ifc_class)
|
||||
self._cache[cache_key] = type_obj
|
||||
|
||||
type_obj = self._cache[cache_key]
|
||||
type_id = type_obj.id()
|
||||
|
||||
if type_id not in self._pending:
|
||||
self._pending[type_id] = []
|
||||
self._pending[type_id].append(element)
|
||||
|
||||
def flush(self):
|
||||
"""Write all IfcRelDefinesByType relationships."""
|
||||
for type_id, elements in self._pending.items():
|
||||
type_obj = self._ifc.by_id(type_id)
|
||||
ifcopenshell.api.run(
|
||||
"type.assign_type", self._ifc,
|
||||
related_objects=elements,
|
||||
relating_type=type_obj,
|
||||
)
|
||||
self._pending.clear()
|
||||
print(f" Type objects created: {len(self._cache)}")
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
def _create_type(self, type_class: str, family: str, type_name: str,
|
||||
obj: Base, ifc_class: str):
|
||||
"""Instantiate the IfcTypeObject with name, tag, GlobalId, and psets."""
|
||||
props = _get_props_dict(obj)
|
||||
params = props.get("Parameters") or {}
|
||||
type_params = params.get("Type Parameters") or {}
|
||||
inst_params = params.get("Instance Parameters") or {}
|
||||
|
||||
# Name: "Family:TypeName" (no ElementId)
|
||||
name_parts = [p for p in [family, type_name] if p]
|
||||
name = ":".join(name_parts)
|
||||
|
||||
# Tag: Type's Revit ElementId
|
||||
type_id_entry = _get_nested(inst_params, "Other", "Type Id")
|
||||
tag = str(type_id_entry.get("value")) if isinstance(type_id_entry, dict) else None
|
||||
|
||||
# GlobalId: from Type IfcGUID parameter
|
||||
type_guid_entry = _get_nested(type_params, "IFC Parameters", "Type IfcGUID")
|
||||
guid = None
|
||||
if isinstance(type_guid_entry, dict):
|
||||
guid = type_guid_entry.get("value")
|
||||
|
||||
# Create type entity
|
||||
type_obj = ifcopenshell.api.run(
|
||||
"root.create_entity", self._ifc,
|
||||
ifc_class=type_class,
|
||||
name=name,
|
||||
)
|
||||
if tag:
|
||||
try:
|
||||
type_obj.Tag = str(tag)
|
||||
except Exception:
|
||||
pass
|
||||
if guid:
|
||||
try:
|
||||
type_obj.GlobalId = str(guid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Write type-level property sets
|
||||
self._write_type_psets(type_obj, obj, ifc_class, type_name, props,
|
||||
type_params, inst_params)
|
||||
return type_obj
|
||||
|
||||
def _write_type_psets(self, type_obj, obj, ifc_class, type_name,
|
||||
props, type_params, inst_params):
|
||||
"""Write psets on the type object (type-level parameters only)."""
|
||||
ifc = self._ifc
|
||||
pset_name = COMMON_PSET.get(ifc_class)
|
||||
|
||||
# ── Standard Common pset on the type ──────────────────────────────
|
||||
if pset_name:
|
||||
type_ifc_props = []
|
||||
|
||||
# IsExternal (type-level)
|
||||
bic = props.get("builtInCategory", "")
|
||||
is_external = bic in EXTERNAL_CATEGORIES
|
||||
if ifc_class not in {"IfcSpace", "IfcSite", "IfcBuildingStorey",
|
||||
"IfcBuilding", "IfcFurnishingElement", "IfcOpeningElement"}:
|
||||
p = _make_prop(ifc, "IsExternal", "IfcBoolean", is_external)
|
||||
if p:
|
||||
type_ifc_props.append(p)
|
||||
|
||||
# ThermalTransmittance (from type parameters)
|
||||
if ifc_class in {"IfcWall", "IfcWallStandardCase", "IfcRoof",
|
||||
"IfcSlab", "IfcDoor", "IfcWindow"}:
|
||||
u_val = _param_value(type_params,
|
||||
"ANALYTICAL_HEAT_TRANSFER_COEFFICIENT")
|
||||
if u_val is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, "ThermalTransmittance",
|
||||
"IfcThermalTransmittanceMeasure", float(u_val))
|
||||
if p:
|
||||
type_ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# LoadBearing (from type parameters)
|
||||
if ifc_class in {"IfcWall", "IfcWallStandardCase", "IfcColumn",
|
||||
"IfcBeam", "IfcSlab"}:
|
||||
lb_val = _param_value(type_params, "WALL_STRUCTURAL_SIGNIFICANT")
|
||||
if lb_val is not None:
|
||||
p = _make_prop(ifc, "LoadBearing", "IfcBoolean", bool(lb_val))
|
||||
if p:
|
||||
type_ifc_props.append(p)
|
||||
|
||||
if type_ifc_props:
|
||||
_write_pset(ifc, type_obj, pset_name, type_ifc_props)
|
||||
|
||||
# ── Pset_EnvironmentalImpactIndicators on the type ─────────────────
|
||||
if type_name:
|
||||
p = _make_prop(ifc, "Reference", "IfcIdentifier", type_name)
|
||||
if p:
|
||||
_write_pset(ifc, type_obj, "Pset_EnvironmentalImpactIndicators", [p])
|
||||
|
||||
# ── RVT_TypeParameters — all type-level Revit params ──────────────
|
||||
type_flat = _flatten_params(type_params)
|
||||
if type_flat:
|
||||
type_str_props = []
|
||||
for name_p, val in type_flat.items():
|
||||
try:
|
||||
nominal = ifc.create_entity("IfcLabel", wrappedValue=val)
|
||||
prop = ifc.create_entity("IfcPropertySingleValue",
|
||||
Name=name_p, NominalValue=nominal)
|
||||
type_str_props.append(prop)
|
||||
except Exception:
|
||||
pass
|
||||
if type_str_props:
|
||||
_write_pset(ifc, type_obj, "RVT_TypeParameters", type_str_props)
|
||||
+1
-1
@@ -112,4 +112,4 @@ class StoreyManager:
|
||||
|
||||
@property
|
||||
def names(self) -> list[str]:
|
||||
return list(self._storeys.keys())
|
||||
return list(self._storeys.keys())
|
||||
Reference in New Issue
Block a user