From f9c4b6a98f66006cbdaa64e55e4c8d853032bf62 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 8 Aug 2024 15:13:34 +1000 Subject: [PATCH] Improve QTO of segment lengths Previous method uses bounding box which could be wrong if the app (Revit) doesn't extrude in +Z and has a slope (e.g. most pipes). Also Revit tends to use rectangle profiles not for the cross section, but instead for the footprint which is crazy. --- src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json | 8 +-- src/ifc5d/ifc5d/qto.py | 68 +++++++++++++++++----- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json b/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json index 52b6e8e210..64b69884ec 100644 --- a/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json +++ b/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json @@ -97,7 +97,7 @@ "Qto_CableCarrierSegmentBaseQuantities": { "CrossSectionArea": null, "GrossWeight": null, - "Length": "net_get_max_xyz", + "Length": "net_get_segment_length", "OuterSurfaceArea": null } }, @@ -110,7 +110,7 @@ "Qto_CableSegmentBaseQuantities": { "CrossSectionArea": null, "GrossWeight": null, - "Length": "net_get_max_xyz", + "Length": "net_get_segment_length", "OuterSurfaceArea": null } }, @@ -236,7 +236,7 @@ "Qto_DuctSegmentBaseQuantities": { "GrossCrossSectionArea": null, "GrossWeight": null, - "Length": "net_get_max_xyz", + "Length": "net_get_segment_length", "NetCrossSectionArea": null, "OuterSurfaceArea": null } @@ -421,7 +421,7 @@ "Qto_PipeSegmentBaseQuantities": { "GrossCrossSectionArea": null, "GrossWeight": null, - "Length": "net_get_max_xyz", + "Length": "net_get_segment_length", "NetCrossSectionArea": null, "NetWeight": null, "OuterSurfaceArea": null diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 25c547a76f..adbe09cb20 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -21,9 +21,10 @@ import json import ifcopenshell import ifcopenshell.api import ifcopenshell.api.pset -import ifcopenshell.util.element import ifcopenshell.util.unit +import ifcopenshell.util.element import ifcopenshell.util.selector +import ifcopenshell.util.representation import multiprocessing from collections import namedtuple from typing import Any @@ -106,6 +107,7 @@ class IfcOpenShell: "Footprint Perimeter", "The perimeter if the object's faces were projected along the Z-axis and seen top down", ), + "get_segment_length": Function("IfcLengthMeasure", "Segment Length", "Intelligently guesses the length of flow segments"), # IfcAreaMeasure "get_area": Function("IfcAreaMeasure", "Area", "The total surface area of the element"), "get_footprint_area": Function( @@ -137,8 +139,9 @@ class IfcOpenShell: functions[f"gross_{k}"] = Function(v.measure, f"Gross {v.name}", v.description) functions[f"net_{k}"] = Function(v.measure, f"Net {v.name}", v.description) - @staticmethod + @classmethod def calculate( + cls, ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], qtos: dict, @@ -150,9 +153,10 @@ class IfcOpenShell: formula_functions = {} - gross_settings = ifcopenshell.geom.settings() - gross_settings.set("disable-opening-subtractions", True) - net_settings = ifcopenshell.geom.settings() + cls.gross_settings = ifcopenshell.geom.settings() + cls.gross_settings.set("disable-opening-subtractions", True) + cls.net_settings = ifcopenshell.geom.settings() + cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) gross_qtos = {} net_qtos = {} @@ -161,24 +165,27 @@ class IfcOpenShell: for quantity, formula in quantities.items(): if not formula: continue - if formula.startswith("gross_"): - formula = formula[6:] - gross_qtos.setdefault(name, {})[quantity] = formula + gross_or_net_qtos = gross_qtos if formula.startswith("gross_") else net_qtos + if formula.endswith("get_segment_length"): + gross_or_net_qtos.setdefault(name, {})[quantity] = formula.partition("_")[2] + elif formula.startswith("gross_"): + formula = formula.partition("_")[2] + gross_or_net_qtos.setdefault(name, {})[quantity] = formula formula_functions[formula] = getattr(ifcopenshell.util.shape, formula) elif formula.startswith("net_"): - formula = formula[4:] - net_qtos.setdefault(name, {})[quantity] = formula + formula = formula.partition("_")[2] + gross_or_net_qtos.setdefault(name, {})[quantity] = formula formula_functions[formula] = getattr(ifcopenshell.util.shape, formula) tasks = [] if gross_qtos: - tasks.append((IfcOpenShell.create_iterator(ifc_file, gross_settings, list(elements)), gross_qtos)) + tasks.append((IfcOpenShell.create_iterator(ifc_file, cls.gross_settings, list(elements)), gross_qtos)) if net_qtos: - tasks.append((IfcOpenShell.create_iterator(ifc_file, net_settings, list(elements)), net_qtos)) + tasks.append((IfcOpenShell.create_iterator(ifc_file, cls.net_settings, list(elements)), net_qtos)) - unit_converter = SI2ProjectUnitConverter(ifc_file) + cls.unit_converter = SI2ProjectUnitConverter(ifc_file) for iterator, qtos in tasks: if iterator.initialize(): @@ -189,9 +196,13 @@ class IfcOpenShell: for name, quantities in qtos.items(): results[element].setdefault(name, {}) for quantity, formula in quantities.items(): - results[element][name][quantity] = unit_converter.convert( - formula_functions[formula](shape.geometry), IfcOpenShell.raw_functions[formula].measure - ) + if formula == "get_segment_length": + results[element][name][quantity] = cls.get_segment_length(ifc_file, shape) + else: + results[element][name][quantity] = cls.unit_converter.convert( + formula_functions[formula](shape.geometry), + IfcOpenShell.raw_functions[formula].measure, + ) if not iterator.next(): break @@ -201,6 +212,31 @@ class IfcOpenShell: ) -> ifcopenshell.geom.iterator: return ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements) + @classmethod + def get_segment_length(cls, ifc_file: ifcopenshell.file, shape) -> float: + element = ifc_file.by_id(shape.id) + rep = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + if rep and len(rep.Items or []) == 1 and rep.Items[0].is_a("IfcExtrudedAreaSolid"): + item = rep.Items[0] + if item.SweptArea.is_a("IfcRectangleProfileDef"): + # Revit doesn't follow the +Z extrusion rule, so the rectangle isn't the cross section + x = item.SweptArea.XDim + y = item.SweptArea.YDim + z = item.Depth + return max([x, y, z]) + elif item.SweptArea.is_a("IfcCircleProfileDef"): + return item.Depth + elif item.SweptArea.is_a("IfcParameterizedProfileDef"): + return item.Depth + try: + area_shape = ifcopenshell.geom.create_shape(settings, item.SweptArea) + except: + return + x = ifcopenshell.util.shape.get_x(area_shape.geometry) / cls.unit_scale + y = ifcopenshell.util.shape.get_y(area_shape.geometry) / cls.unit_scale + z = item.Depth + return max([x, y, z]) + class Blender: """Calculates geometry based on currently loaded Blender objects."""