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.
This commit is contained in:
Dion Moult
2024-08-08 15:13:34 +10:00
parent b01e93161e
commit f9c4b6a98f
2 changed files with 56 additions and 20 deletions
+4 -4
View File
@@ -97,7 +97,7 @@
"Qto_CableCarrierSegmentBaseQuantities": { "Qto_CableCarrierSegmentBaseQuantities": {
"CrossSectionArea": null, "CrossSectionArea": null,
"GrossWeight": null, "GrossWeight": null,
"Length": "net_get_max_xyz", "Length": "net_get_segment_length",
"OuterSurfaceArea": null "OuterSurfaceArea": null
} }
}, },
@@ -110,7 +110,7 @@
"Qto_CableSegmentBaseQuantities": { "Qto_CableSegmentBaseQuantities": {
"CrossSectionArea": null, "CrossSectionArea": null,
"GrossWeight": null, "GrossWeight": null,
"Length": "net_get_max_xyz", "Length": "net_get_segment_length",
"OuterSurfaceArea": null "OuterSurfaceArea": null
} }
}, },
@@ -236,7 +236,7 @@
"Qto_DuctSegmentBaseQuantities": { "Qto_DuctSegmentBaseQuantities": {
"GrossCrossSectionArea": null, "GrossCrossSectionArea": null,
"GrossWeight": null, "GrossWeight": null,
"Length": "net_get_max_xyz", "Length": "net_get_segment_length",
"NetCrossSectionArea": null, "NetCrossSectionArea": null,
"OuterSurfaceArea": null "OuterSurfaceArea": null
} }
@@ -421,7 +421,7 @@
"Qto_PipeSegmentBaseQuantities": { "Qto_PipeSegmentBaseQuantities": {
"GrossCrossSectionArea": null, "GrossCrossSectionArea": null,
"GrossWeight": null, "GrossWeight": null,
"Length": "net_get_max_xyz", "Length": "net_get_segment_length",
"NetCrossSectionArea": null, "NetCrossSectionArea": null,
"NetWeight": null, "NetWeight": null,
"OuterSurfaceArea": null "OuterSurfaceArea": null
+52 -16
View File
@@ -21,9 +21,10 @@ import json
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.api.pset import ifcopenshell.api.pset
import ifcopenshell.util.element
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.selector import ifcopenshell.util.selector
import ifcopenshell.util.representation
import multiprocessing import multiprocessing
from collections import namedtuple from collections import namedtuple
from typing import Any from typing import Any
@@ -106,6 +107,7 @@ class IfcOpenShell:
"Footprint Perimeter", "Footprint Perimeter",
"The perimeter if the object's faces were projected along the Z-axis and seen top down", "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 # IfcAreaMeasure
"get_area": Function("IfcAreaMeasure", "Area", "The total surface area of the element"), "get_area": Function("IfcAreaMeasure", "Area", "The total surface area of the element"),
"get_footprint_area": Function( "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"gross_{k}"] = Function(v.measure, f"Gross {v.name}", v.description)
functions[f"net_{k}"] = Function(v.measure, f"Net {v.name}", v.description) functions[f"net_{k}"] = Function(v.measure, f"Net {v.name}", v.description)
@staticmethod @classmethod
def calculate( def calculate(
cls,
ifc_file: ifcopenshell.file, ifc_file: ifcopenshell.file,
elements: set[ifcopenshell.entity_instance], elements: set[ifcopenshell.entity_instance],
qtos: dict, qtos: dict,
@@ -150,9 +153,10 @@ class IfcOpenShell:
formula_functions = {} formula_functions = {}
gross_settings = ifcopenshell.geom.settings() cls.gross_settings = ifcopenshell.geom.settings()
gross_settings.set("disable-opening-subtractions", True) cls.gross_settings.set("disable-opening-subtractions", True)
net_settings = ifcopenshell.geom.settings() cls.net_settings = ifcopenshell.geom.settings()
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
gross_qtos = {} gross_qtos = {}
net_qtos = {} net_qtos = {}
@@ -161,24 +165,27 @@ class IfcOpenShell:
for quantity, formula in quantities.items(): for quantity, formula in quantities.items():
if not formula: if not formula:
continue continue
if formula.startswith("gross_"): gross_or_net_qtos = gross_qtos if formula.startswith("gross_") else net_qtos
formula = formula[6:] if formula.endswith("get_segment_length"):
gross_qtos.setdefault(name, {})[quantity] = formula 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) formula_functions[formula] = getattr(ifcopenshell.util.shape, formula)
elif formula.startswith("net_"): elif formula.startswith("net_"):
formula = formula[4:] formula = formula.partition("_")[2]
net_qtos.setdefault(name, {})[quantity] = formula gross_or_net_qtos.setdefault(name, {})[quantity] = formula
formula_functions[formula] = getattr(ifcopenshell.util.shape, formula) formula_functions[formula] = getattr(ifcopenshell.util.shape, formula)
tasks = [] tasks = []
if gross_qtos: 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: 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: for iterator, qtos in tasks:
if iterator.initialize(): if iterator.initialize():
@@ -189,9 +196,13 @@ class IfcOpenShell:
for name, quantities in qtos.items(): for name, quantities in qtos.items():
results[element].setdefault(name, {}) results[element].setdefault(name, {})
for quantity, formula in quantities.items(): for quantity, formula in quantities.items():
results[element][name][quantity] = unit_converter.convert( if formula == "get_segment_length":
formula_functions[formula](shape.geometry), IfcOpenShell.raw_functions[formula].measure 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(): if not iterator.next():
break break
@@ -201,6 +212,31 @@ class IfcOpenShell:
) -> ifcopenshell.geom.iterator: ) -> ifcopenshell.geom.iterator:
return ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements) 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: class Blender:
"""Calculates geometry based on currently loaded Blender objects.""" """Calculates geometry based on currently loaded Blender objects."""