Files

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()