856 lines
32 KiB
Python
856 lines
32 KiB
Python
# =============================================================================
|
|
# properties.py
|
|
# Writes IFC property sets matching the structure of Revit's native IFC export.
|
|
#
|
|
# 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
|
|
#
|
|
# 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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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",
|
|
"IfcGeographicElement": "Pset_SiteCommon",
|
|
"IfcPipeFitting": "Pset_PipeFittingTypeCommon",
|
|
"IfcSanitaryTerminal": "Pset_SanitaryTerminalTypeCommon",
|
|
"IfcReinforcingBar": "Pset_ReinforcingBarBendingsBECCommon",
|
|
"IfcMechanicalFastener": "Pset_MechanicalFastenerTypeCommon",
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_props_cache: dict[int, dict] = {} # id(obj) → props dict
|
|
|
|
|
|
def _get_props_dict(obj: Base) -> dict:
|
|
"""Get properties as a plain dict. Cached per object to avoid repeated conversion."""
|
|
oid = id(obj)
|
|
if oid in _props_cache:
|
|
return _props_cache[oid]
|
|
# Try getattr first — matches the pattern that works in other Speckle scripts
|
|
p = getattr(obj, "properties", None)
|
|
if p is None:
|
|
for key in ["properties", "@properties"]:
|
|
try:
|
|
p = obj[key]
|
|
if p is not None:
|
|
break
|
|
except Exception:
|
|
continue
|
|
if p is None:
|
|
_props_cache[oid] = {}
|
|
return {}
|
|
result = _to_dict(p)
|
|
_props_cache[oid] = result
|
|
return result
|
|
|
|
|
|
def _get_nested(d, *keys):
|
|
"""Safely walk nested dicts/objects."""
|
|
cur = d
|
|
for k in keys:
|
|
if cur is None:
|
|
return None
|
|
cur = _safe_get(cur, k)
|
|
return cur
|
|
|
|
|
|
_to_dict_cache: dict[int, dict] = {} # id(obj) → converted dict
|
|
|
|
|
|
def _to_dict(obj) -> dict:
|
|
"""Convert a Speckle Base object or dict to a plain dict. Returns {} on failure.
|
|
Cached per object identity to avoid repeated conversion."""
|
|
if obj is None:
|
|
return {}
|
|
if isinstance(obj, dict):
|
|
return obj
|
|
oid = id(obj)
|
|
if oid in _to_dict_cache:
|
|
return _to_dict_cache[oid]
|
|
# Try .get_dynamic_member_names() for Speckle Base objects
|
|
if hasattr(obj, "get_dynamic_member_names"):
|
|
result = {}
|
|
try:
|
|
names = obj.get_dynamic_member_names()
|
|
except Exception:
|
|
_to_dict_cache[oid] = {}
|
|
return {}
|
|
for n in names:
|
|
try:
|
|
result[n] = obj[n]
|
|
except Exception:
|
|
pass
|
|
_to_dict_cache[oid] = result
|
|
return result
|
|
# Last resort: try common dict-like patterns
|
|
if hasattr(obj, "items"):
|
|
try:
|
|
result = dict(obj.items())
|
|
_to_dict_cache[oid] = result
|
|
return result
|
|
except Exception:
|
|
pass
|
|
_to_dict_cache[oid] = {}
|
|
return {}
|
|
|
|
|
|
def _safe_get(obj, key, default=None):
|
|
"""Safe key access for both dicts and Speckle Base objects."""
|
|
if obj is None:
|
|
return default
|
|
if isinstance(obj, dict):
|
|
return obj.get(key, default)
|
|
# Try getattr first (works reliably for Speckle Base)
|
|
try:
|
|
val = getattr(obj, key, None)
|
|
if val is not None:
|
|
return val
|
|
except Exception:
|
|
pass
|
|
# Fallback to bracket access
|
|
try:
|
|
val = obj[key]
|
|
if val is not None:
|
|
return val
|
|
except Exception:
|
|
pass
|
|
return default
|
|
|
|
|
|
def _param_value(params_block, internal_name: str):
|
|
"""
|
|
Search all groups in a parameter block for a param with the given
|
|
internalDefinitionName. Returns the raw value or None.
|
|
Handles both plain dicts and Speckle Base objects.
|
|
"""
|
|
block = _to_dict(params_block)
|
|
if not block:
|
|
return None
|
|
for group in block.values():
|
|
group_d = _to_dict(group)
|
|
if not group_d:
|
|
continue
|
|
for entry in group_d.values():
|
|
entry_d = _to_dict(entry)
|
|
if not entry_d:
|
|
continue
|
|
if entry_d.get("internalDefinitionName") == internal_name:
|
|
return entry_d.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.
|
|
"""
|
|
family = getattr(obj, "family", None) or ""
|
|
typ = getattr(obj, "type", None) 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]
|
|
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 = _safe_get(props, "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 = _safe_get(props, "Parameters", {})
|
|
inst = _safe_get(params, "Instance Parameters", {})
|
|
ifc_p = _safe_get(inst, "IFC Parameters", {})
|
|
entry = _safe_get(ifc_p, "IfcGUID", {})
|
|
entry_d = _to_dict(entry) if not isinstance(entry, dict) else entry
|
|
val = entry_d.get("value") if entry_d 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 = _safe_get(props, "Parameters", {})
|
|
type_params = _safe_get(params, "Type Parameters", {})
|
|
inst_params = _safe_get(params, "Instance Parameters", {})
|
|
|
|
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 = _safe_get(props, "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
|
|
|
|
# IfcSpace-specific: set Name, LongName, Category, and BaseQuantities
|
|
if ifc_class == "IfcSpace":
|
|
_write_space_properties(ifc, element, obj, ifc_props)
|
|
|
|
_write_pset(ifc, element, pset_name, ifc_props)
|
|
|
|
|
|
def _write_space_properties(ifc, element, obj: Base, ifc_props: list):
|
|
"""
|
|
Set IfcSpace attributes and BaseQuantities from Revit Room parameters.
|
|
|
|
Uses internalDefinitionName to find values:
|
|
ROOM_NUMBER → IfcSpace.Name + Pset_SpaceCommon.Reference
|
|
ROOM_NAME → IfcSpace.LongName
|
|
Occupant → Pset_SpaceCommon.Category
|
|
ROOM_AREA → Qto_SpaceBaseQuantities.NetFloorArea
|
|
ROOM_VOLUME → Qto_SpaceBaseQuantities.NetVolume
|
|
"""
|
|
props = _get_props_dict(obj)
|
|
params = _safe_get(props, "Parameters", {})
|
|
inst_params = _safe_get(params, "Instance Parameters", {})
|
|
|
|
# --- Room Number → IfcSpace.Name + Pset_SpaceCommon.Reference ---
|
|
room_number = _param_value(inst_params, "ROOM_NUMBER")
|
|
if room_number:
|
|
room_number = str(room_number).strip()
|
|
element.Name = room_number
|
|
# Replace any existing Reference in ifc_props
|
|
ifc_props[:] = [p for p in ifc_props if p.Name != "Reference"]
|
|
p = _make_prop(ifc, "Reference", "IfcIdentifier", room_number)
|
|
if p:
|
|
ifc_props.append(p)
|
|
# Also add as explicit RoomNumber in the pset
|
|
p = _make_prop(ifc, "RoomNumber", "IfcLabel", room_number)
|
|
if p:
|
|
ifc_props.append(p)
|
|
|
|
# --- Room Name → IfcSpace.LongName + Pset_SpaceCommon.RoomName ---
|
|
room_name = _param_value(inst_params, "ROOM_NAME")
|
|
if not room_name:
|
|
# Fallback to the Speckle object's own name
|
|
room_name = getattr(obj, "name", None)
|
|
if room_name:
|
|
room_name = str(room_name).strip()
|
|
try:
|
|
element.LongName = room_name
|
|
except AttributeError:
|
|
pass
|
|
p = _make_prop(ifc, "RoomName", "IfcLabel", room_name)
|
|
if p:
|
|
ifc_props.append(p)
|
|
|
|
# --- Occupant → Pset_SpaceCommon.Category ---
|
|
occupant = _param_value(inst_params, "Occupant")
|
|
if occupant:
|
|
p = _make_prop(ifc, "Category", "IfcLabel", str(occupant).strip())
|
|
if p:
|
|
ifc_props.append(p)
|
|
|
|
# --- Area & Volume → Qto_SpaceBaseQuantities ---
|
|
quantities = []
|
|
|
|
area_val = _param_value(inst_params, "ROOM_AREA")
|
|
if area_val is not None:
|
|
try:
|
|
q = ifc.create_entity(
|
|
"IfcQuantityArea",
|
|
Name="NetFloorArea",
|
|
AreaValue=float(area_val),
|
|
)
|
|
quantities.append(q)
|
|
except Exception:
|
|
pass
|
|
|
|
volume_val = _param_value(inst_params, "ROOM_VOLUME")
|
|
if volume_val is not None:
|
|
try:
|
|
q = ifc.create_entity(
|
|
"IfcQuantityVolume",
|
|
Name="NetVolume",
|
|
VolumeValue=float(volume_val),
|
|
)
|
|
quantities.append(q)
|
|
except Exception:
|
|
pass
|
|
|
|
if quantities:
|
|
try:
|
|
qto = ifcopenshell.api.run(
|
|
"pset.add_qto", ifc,
|
|
product=element,
|
|
name="Qto_SpaceBaseQuantities",
|
|
)
|
|
qto.Quantities = quantities
|
|
except Exception as e:
|
|
print(f" ⚠️ Qto_SpaceBaseQuantities: {e}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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):
|
|
return f"{value:.6g}"
|
|
s = str(value).strip()
|
|
return s or None
|
|
|
|
|
|
def _flatten_params(params_block) -> dict:
|
|
"""Flatten Type or Instance parameter block into {name: display_value}.
|
|
Handles both plain dicts and Speckle Base objects at every nesting level."""
|
|
result = {}
|
|
skip_units = {"", "None", "General", "Currency", "Integer"}
|
|
block = _to_dict(params_block)
|
|
for group in block.values():
|
|
group_d = _to_dict(group)
|
|
if not group_d:
|
|
continue
|
|
for entry in group_d.values():
|
|
entry_d = _to_dict(entry)
|
|
if not entry_d:
|
|
continue
|
|
name = entry_d.get("name")
|
|
value = entry_d.get("value")
|
|
units = entry_d.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_revit_params(ifc, element, obj: Base):
|
|
"""
|
|
Write remaining Revit instance parameters as a custom property set
|
|
using the vendor prefix 'RVT_' (not 'Pset_' which is reserved):
|
|
RVT_InstanceParameters — from Instance Parameters
|
|
|
|
Note: RVT_TypeParameters are written on the IfcTypeObject (via TypeManager),
|
|
not on individual elements, to avoid duplication.
|
|
"""
|
|
props = _get_props_dict(obj)
|
|
params = _safe_get(props, "Parameters", {})
|
|
|
|
inst_flat = _flatten_params(_safe_get(params, "Instance Parameters", {}))
|
|
|
|
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
|
|
|
|
inst_props = build_str_props(inst_flat)
|
|
|
|
if inst_props:
|
|
_write_pset(ifc, element, "RVT_InstanceParameters", inst_props)
|
|
|
|
# Identity: family, type, elementId, builtInCategory
|
|
identity = {}
|
|
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()
|
|
elem_id = _safe_get(props, "elementId")
|
|
if elem_id:
|
|
identity["ElementId"] = str(elem_id)
|
|
bic = _safe_get(props, "builtInCategory")
|
|
if bic:
|
|
identity["BuiltInCategory"] = str(bic)
|
|
|
|
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_material_quantities(ifc, element, obj: Base):
|
|
"""
|
|
Write Material Quantities from Revit as IfcElementQuantity sets.
|
|
|
|
Source: properties."Material Quantities".<MaterialName>.{area, volume, density,
|
|
materialName, materialClass, materialCategory}
|
|
|
|
Each material produces one IfcElementQuantity named "Qto_<MaterialName>BaseQuantities" with:
|
|
- GrossArea (IfcQuantityArea)
|
|
- GrossVolume (IfcQuantityVolume)
|
|
- Density (IfcPropertySingleValue — no standard IFC quantity type)
|
|
- MaterialClass (IfcPropertySingleValue)
|
|
- MaterialCategory (IfcPropertySingleValue)
|
|
"""
|
|
props = _get_props_dict(obj)
|
|
mat_quantities = _safe_get(props, "Material Quantities")
|
|
if mat_quantities is None:
|
|
return
|
|
|
|
mat_dict = _to_dict(mat_quantities)
|
|
if not mat_dict:
|
|
return
|
|
|
|
for mat_key, mat_data in mat_dict.items():
|
|
mat_d = _to_dict(mat_data)
|
|
if not mat_d:
|
|
continue
|
|
|
|
mat_name = mat_d.get("materialName") or mat_key
|
|
quantities = []
|
|
|
|
# Area → IfcQuantityArea
|
|
area_entry = _to_dict(mat_d.get("area"))
|
|
if area_entry and area_entry.get("value") is not None:
|
|
try:
|
|
q = ifc.create_entity(
|
|
"IfcQuantityArea",
|
|
Name="GrossArea",
|
|
AreaValue=float(area_entry["value"]),
|
|
)
|
|
quantities.append(q)
|
|
except Exception:
|
|
pass
|
|
|
|
# Volume → IfcQuantityVolume
|
|
vol_entry = _to_dict(mat_d.get("volume"))
|
|
if vol_entry and vol_entry.get("value") is not None:
|
|
try:
|
|
q = ifc.create_entity(
|
|
"IfcQuantityVolume",
|
|
Name="GrossVolume",
|
|
VolumeValue=float(vol_entry["value"]),
|
|
)
|
|
quantities.append(q)
|
|
except Exception:
|
|
pass
|
|
|
|
# Density → IfcQuantityWeight (mass per volume, stored as weight)
|
|
density_entry = _to_dict(mat_d.get("density"))
|
|
if density_entry and density_entry.get("value") is not None:
|
|
try:
|
|
q = ifc.create_entity(
|
|
"IfcQuantityWeight",
|
|
Name="Density",
|
|
WeightValue=float(density_entry["value"]),
|
|
)
|
|
quantities.append(q)
|
|
except Exception:
|
|
pass
|
|
|
|
if not quantities:
|
|
continue
|
|
|
|
# Create IfcElementQuantity and link via IfcRelDefinesByProperties
|
|
qto_name = f"Qto_{mat_name}BaseQuantities"
|
|
try:
|
|
qto = ifcopenshell.api.run(
|
|
"pset.add_qto", ifc,
|
|
product=element,
|
|
name=qto_name,
|
|
)
|
|
qto.Quantities = quantities
|
|
except Exception as e:
|
|
print(f" ⚠️ {qto_name}: {e}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Qto_<EntityType>BaseQuantities — standard element-level quantities
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# IFC entity → Qto name (only entities with standard Qto sets)
|
|
_ENTITY_QTO_NAME: dict[str, str] = {
|
|
"IfcWall": "Qto_WallBaseQuantities",
|
|
"IfcWallStandardCase": "Qto_WallBaseQuantities",
|
|
"IfcSlab": "Qto_SlabBaseQuantities",
|
|
"IfcColumn": "Qto_ColumnBaseQuantities",
|
|
"IfcBeam": "Qto_BeamBaseQuantities",
|
|
"IfcDoor": "Qto_DoorBaseQuantities",
|
|
"IfcWindow": "Qto_WindowBaseQuantities",
|
|
"IfcRoof": "Qto_RoofBaseQuantities",
|
|
"IfcCovering": "Qto_CoveringBaseQuantities",
|
|
"IfcRailing": "Qto_RailingBaseQuantities",
|
|
"IfcStair": "Qto_StairBaseQuantities",
|
|
"IfcRamp": "Qto_RampBaseQuantities",
|
|
"IfcMember": "Qto_MemberBaseQuantities",
|
|
"IfcFooting": "Qto_FootingBaseQuantities",
|
|
"IfcCurtainWall": "Qto_CurtainWallBaseQuantities",
|
|
"IfcBuildingElementProxy": "Qto_BuildingElementProxyBaseQuantities",
|
|
"IfcPipeFitting": "Qto_PipeFittingBaseQuantities",
|
|
"IfcSanitaryTerminal": "Qto_SanitaryTerminalBaseQuantities",
|
|
"IfcReinforcingBar": "Qto_ReinforcingElementBaseQuantities",
|
|
"IfcMechanicalFastener": "Qto_MechanicalFastenerBaseQuantities",
|
|
}
|
|
|
|
# IFC quantity name → (IFC entity type, value attribute, [Revit param fallbacks])
|
|
# First matching Revit param wins for each quantity name.
|
|
_ELEMENT_QUANTITY_DEFS: list[tuple[str, str, str, list[str]]] = [
|
|
("GrossArea", "IfcQuantityArea", "AreaValue", ["HOST_AREA_COMPUTED"]),
|
|
("GrossVolume", "IfcQuantityVolume", "VolumeValue", ["HOST_VOLUME_COMPUTED"]),
|
|
("Length", "IfcQuantityLength", "LengthValue", [
|
|
"CURVE_ELEM_LENGTH", "INSTANCE_LENGTH_PARAM",
|
|
]),
|
|
("Height", "IfcQuantityLength", "LengthValue", [
|
|
"WALL_USER_HEIGHT_PARAM", "FAMILY_HEIGHT_PARAM",
|
|
"INSTANCE_HEAD_HEIGHT_PARAM",
|
|
]),
|
|
("Width", "IfcQuantityLength", "LengthValue", [
|
|
"INSTANCE_WIDTH_PARAM", "FURNITURE_WIDTH",
|
|
"FLOOR_ATTR_THICKNESS_PARAM",
|
|
]),
|
|
("Perimeter", "IfcQuantityLength", "LengthValue", [
|
|
"HOST_PERIMETER_COMPUTED",
|
|
]),
|
|
]
|
|
|
|
|
|
def write_element_quantities(ifc, element, obj: Base, ifc_class: str = ""):
|
|
"""
|
|
Write Qto_<EntityType>BaseQuantities from Revit computed instance parameters.
|
|
|
|
Reads HOST_AREA_COMPUTED, HOST_VOLUME_COMPUTED, CURVE_ELEM_LENGTH,
|
|
FURNITURE_WIDTH, FAMILY_HEIGHT_PARAM, etc.
|
|
IfcSpace is handled separately in _write_space_properties.
|
|
"""
|
|
if ifc_class == "IfcSpace":
|
|
return # Already handled by Qto_SpaceBaseQuantities
|
|
|
|
qto_name = _ENTITY_QTO_NAME.get(ifc_class)
|
|
if not qto_name:
|
|
return
|
|
|
|
props = _get_props_dict(obj)
|
|
params = _safe_get(props, "Parameters", {})
|
|
inst_params = _safe_get(params, "Instance Parameters", {})
|
|
if not inst_params:
|
|
return
|
|
|
|
quantities = []
|
|
|
|
for qty_name, ifc_entity, value_attr, revit_params in _ELEMENT_QUANTITY_DEFS:
|
|
val = None
|
|
for internal_name in revit_params:
|
|
val = _param_value(inst_params, internal_name)
|
|
if val is not None:
|
|
break
|
|
if val is None:
|
|
continue
|
|
try:
|
|
q = ifc.create_entity(ifc_entity, Name=qty_name, **{value_attr: float(val)})
|
|
quantities.append(q)
|
|
except Exception:
|
|
pass
|
|
|
|
if not quantities:
|
|
return
|
|
|
|
try:
|
|
qto = ifcopenshell.api.run(
|
|
"pset.add_qto", ifc,
|
|
product=element,
|
|
name=qto_name,
|
|
)
|
|
qto.Quantities = quantities
|
|
except Exception as e:
|
|
print(f" ⚠️ {qto_name}: {e}")
|
|
|
|
|
|
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
|
|
6. Qto_<EntityType>BaseQuantities — element-level quantities (area, volume, length)
|
|
7. Qto_<MaterialName>BaseQuantities — material quantities (area, volume, density)
|
|
"""
|
|
write_common_pset(ifc, element, obj, ifc_class, category_name)
|
|
write_revit_params(ifc, element, obj)
|
|
write_element_quantities(ifc, element, obj, ifc_class)
|
|
write_material_quantities(ifc, element, obj)
|
|
|
|
|
|
def write_common_properties(ifc, element, obj: Base, category_name: str = ""):
|
|
"""Legacy shim — kept for compatibility with main.py call sites."""
|
|
pass # All handled by write_properties now
|
|
|
|
|
|
def reset_caches():
|
|
"""Clear module-level caches (call at start of each export run)."""
|
|
_props_cache.clear()
|
|
_to_dict_cache.clear() |