1 Commits

Author SHA1 Message Date
NLSA bdd030ba86 Update props and instances
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2026-03-06 14:54:44 +01:00
7 changed files with 1257286 additions and 195 deletions
+37 -20
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+219
View File
@@ -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
View File
@@ -112,4 +112,4 @@ class StoreyManager:
@property
def names(self) -> list[str]:
return list(self._storeys.keys())
return list(self._storeys.keys())