diff --git a/flatten.py b/flatten.py deleted file mode 100644 index c578c65..0000000 --- a/flatten.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Helper module for a simple speckle object tree flattening.""" - -from collections.abc import Iterable - -from specklepy.objects import Base - - -def flatten_base(base: Base) -> Iterable[Base]: - """Flatten a base object into an iterable of bases. - - This function recursively traverses the `elements` or `@elements` attribute of the - base object, yielding each nested base object. - - Args: - base (Base): The base object to flatten. - - Yields: - Base: Each nested base object in the hierarchy. - """ - # Attempt to get the elements attribute, fallback to @elements if necessary - elements = getattr(base, "elements", getattr(base, "@elements", None)) - - if elements is not None: - for element in elements: - yield from flatten_base(element) - - yield base diff --git a/main.py b/main.py index a2aa120..dbf8378 100644 --- a/main.py +++ b/main.py @@ -162,26 +162,37 @@ def automate_function( total += 1 # B2: Instance objects nested inside displayValue - # Each becomes its own IFC element (same class as parent) - # Use the parent object's name — the InstanceProxy has no meaningful name + # All instances are parts of the SAME element (e.g. window frame + glass + sill) + # Merge all into a single IFC element with combined geometry nested_instances = get_display_instances(obj) - for inst in nested_instances: - 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, - storey_manager=storey_manager, - tag=get_element_tag(obj), guid=None, - object_type=getattr(obj, "type", None), - ) - 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 nested_instances: + mapped_items = [] + inst_placement = None + for inst in nested_instances: + inst_rep, inst_pl = instance_to_ifc( + ifc, body_context, inst, definition_map, scale=scale, material_manager=material_manager + ) + if inst_rep: + mapped_items.extend(inst_rep.Items) + if inst_placement is None: + inst_placement = inst_pl + if mapped_items: + combined_rep = ifc.createIfcShapeRepresentation( + ContextOfItems=body_context, + RepresentationIdentifier="Body", + RepresentationType="MappedRepresentation", + Items=mapped_items, + ) + element = _create_element( + ifc, ifc_class, name, combined_rep, inst_placement, storey, + storey_manager=storey_manager, + tag=get_element_tag(obj), guid=get_ifc_guid(obj), + object_type=getattr(obj, "type", None), + ) + 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 # Track if neither path produced geometry if not rep and not nested_instances: @@ -205,12 +216,12 @@ def automate_function( ifc.write(ifc_filename) print(f"\nšŸ’¾ IFC file written: {ifc_filename}") - try: - automate_context.mark_run_success("Success! You can download the IF file below.") - automate_context.store_file_result(f"./{ifc_filename}") - except Exception as e: - print(f" āš ļø Could not upload file result (network issue?): {e}") - automate_context.mark_run_failed(f"Something went wrong when storing file result. Exception detail: {e}") + # try: + # automate_context.mark_run_success("Success! You can download the IF file below.") + # automate_context.store_file_result(f"./{ifc_filename}") + # except Exception as e: + # print(f" āš ļø Could not upload file result (network issue?): {e}") + # automate_context.mark_run_failed(f"Something went wrong when storing file result. Exception detail: {e}") print(f"\n{'=' * 60}") print(f" Export complete!") diff --git a/utils/mapper.py b/utils/mapper.py index 81cbb00..97d6b37 100644 --- a/utils/mapper.py +++ b/utils/mapper.py @@ -4,9 +4,8 @@ # # 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 +# 2. category_name string (traversal context) — display name fallback +# 3. IfcBuildingElementProxy — last resort # # builtInCategory values: https://www.revitapidocs.com/2019/ba1c5b30-242f-5fdc-8ea9-ec3b61e6e722.htm # ============================================================================= @@ -119,44 +118,7 @@ BUILTIN_CATEGORY_MAP: dict[str, str] = { } -# --- speckle_type → IFC class (secondary lookup) --- -SPECKLE_TYPE_MAP: dict[str, str] = { - "Objects.BuiltElements.Wall": "IfcWall", - "Objects.BuiltElements.Floor": "IfcSlab", - "Objects.BuiltElements.Roof": "IfcRoof", - "Objects.BuiltElements.Column": "IfcColumn", - "Objects.BuiltElements.Beam": "IfcBeam", - "Objects.BuiltElements.Brace": "IfcMember", - "Objects.BuiltElements.Duct": "IfcDuctSegment", - "Objects.BuiltElements.Pipe": "IfcPipeSegment", - "Objects.BuiltElements.Wire": "IfcCableCarrierSegment", - "Objects.BuiltElements.Opening": "IfcOpeningElement", - "Objects.BuiltElements.Room": "IfcSpace", - "Objects.BuiltElements.Ceiling": "IfcCovering", - "Objects.BuiltElements.Stair": "IfcStair", - "Objects.BuiltElements.Ramp": "IfcRamp", - "Objects.BuiltElements.Foundation": "IfcFooting", - "Objects.BuiltElements.Grid": "IfcGrid", - "Objects.BuiltElements.Level": "IfcBuildingStorey", - "Objects.BuiltElements.Revit.RevitWall": "IfcWall", - "Objects.BuiltElements.Revit.RevitFloor": "IfcSlab", - "Objects.BuiltElements.Revit.RevitRoof": "IfcRoof", - "Objects.BuiltElements.Revit.RevitColumn": "IfcColumn", - "Objects.BuiltElements.Revit.RevitBeam": "IfcBeam", - "Objects.BuiltElements.Revit.RevitBrace": "IfcMember", - "Objects.BuiltElements.Revit.RevitDuct": "IfcDuctSegment", - "Objects.BuiltElements.Revit.RevitPipe": "IfcPipeSegment", - "Objects.BuiltElements.Revit.RevitRoom": "IfcSpace", - "Objects.BuiltElements.Revit.RevitStair": "IfcStair", - "Objects.BuiltElements.Revit.RevitRailing": "IfcRailing", - "Objects.BuiltElements.Revit.RevitCeiling": "IfcCovering", - "Objects.BuiltElements.Revit.RevitTopography": "IfcGeographicElement", - "Objects.BuiltElements.Revit.RevitElementType": "IfcBuildingElementProxy", - "Objects.Geometry.Mesh": "IfcBuildingElementProxy", - "Objects.Geometry.Brep": "IfcBuildingElementProxy", -} - -# --- Display category name → IFC class (tertiary fallback) --- +# --- Display category name → IFC class (secondary fallback) --- CATEGORY_MAP: dict[str, str] = { "Walls": "IfcWall", "Floors": "IfcSlab", @@ -235,11 +197,6 @@ def _get_builtin_category(obj) -> str | None: return result -# Pre-computed: sorted prefixes longest-first for early exit on prefix match -_SPECKLE_PREFIXES: list[tuple[str, str]] = sorted( - SPECKLE_TYPE_MAP.items(), key=lambda x: len(x[0]), reverse=True -) - # Pre-computed lowercase category map for substring matching _CATEGORY_MAP_LOWER: list[tuple[str, str]] = [ (k.lower(), v) for k, v in CATEGORY_MAP.items() @@ -255,10 +212,9 @@ def classify(obj, category_name: str = "") -> str: 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 + 2. category_name from traversal context (display string) + 3. obj.category field + 4. IfcBuildingElementProxy fallback """ cache_key = (id(obj), category_name) if cache_key in _classify_cache: @@ -275,16 +231,7 @@ def _classify_impl(obj, category_name: str) -> str: if bic and bic in BUILTIN_CATEGORY_MAP: return BUILTIN_CATEGORY_MAP[bic] - # 2. speckle_type — exact match first, then longest-prefix match - speckle_type = getattr(obj, "speckle_type", "") or "" - if speckle_type: - if speckle_type in SPECKLE_TYPE_MAP: - return SPECKLE_TYPE_MAP[speckle_type] - for prefix, ifc_class in _SPECKLE_PREFIXES: - if speckle_type.startswith(prefix): - return ifc_class - - # 3. category_name from traversal context — exact match first + # 2. category_name from traversal context — exact match first if category_name: if category_name in CATEGORY_MAP: return CATEGORY_MAP[category_name] @@ -293,7 +240,7 @@ def _classify_impl(obj, category_name: str) -> str: if key_lower in cat_lower: return ifc_class - # 4. obj.category field + # 3. obj.category field obj_category = getattr(obj, "category", None) if obj_category and isinstance(obj_category, str): if obj_category in CATEGORY_MAP: diff --git a/utils/properties.py b/utils/properties.py index 548aed3..ef0cede 100644 --- a/utils/properties.py +++ b/utils/properties.py @@ -644,7 +644,7 @@ def write_material_quantities(ifc, element, obj: Base): Source: properties."Material Quantities"..{area, volume, density, materialName, materialClass, materialCategory} - Each material produces one IfcElementQuantity named "Qto_" with: + Each material produces one IfcElementQuantity named "Qto_BaseQuantities" with: - GrossArea (IfcQuantityArea) - GrossVolume (IfcQuantityVolume) - Density (IfcPropertySingleValue — no standard IFC quantity type) @@ -711,7 +711,7 @@ def write_material_quantities(ifc, element, obj: Base): continue # Create IfcElementQuantity and link via IfcRelDefinesByProperties - qto_name = f"Qto_{mat_name}" + qto_name = f"Qto_{mat_name}BaseQuantities" try: qto = ifcopenshell.api.run( "pset.add_qto", ifc, @@ -723,6 +723,103 @@ def write_material_quantities(ifc, element, obj: Base): print(f" āš ļø {qto_name}: {e}") +# --------------------------------------------------------------------------- +# Qto_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", +} + +# 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_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: @@ -731,10 +828,12 @@ def write_properties(ifc, element, obj: Base, ifc_class: str = "", category_name 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_ — material quantities (area, volume, density) + 6. Qto_BaseQuantities — element-level quantities (area, volume, length) + 7. Qto_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)