From 90554f43ce8249d1c4bd7569f0c9aedc69443425 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sat, 11 Jul 2026 15:34:41 +0300 Subject: [PATCH] Derive pipe segment length through boolean-clipped bodies (#6761) get_segment_length only handled a body whose single representation item is a bare IfcExtrudedAreaSolid. A miter bend or otherwise clipped pipe segment wraps that extrusion in an IfcBooleanClippingResult (an IfcBooleanResult), so the guard failed, the function returned None, and the Qto length (and thus the whole base-quantity set) came out empty while plain segments worked. Unwrap any IfcBooleanResult to its FirstOperand until the underlying extrusion is reached, then apply the existing depth logic. The length is still taken from the sweep Depth, matching a plain segment. Non-extrusion items after unwrapping still return None as before. Verified on the reported sample: the two booleaned IfcPipeSegment miter bends go from an empty Qto_PipeSegmentBaseQuantities to Length 5.05; a bare-extrusion segment is unchanged. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 --- src/ifc5d/ifc5d/qto.py | 43 ++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 955c6fd7af..86bc825beb 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -398,27 +398,34 @@ class IfcOpenShell(QtoCalculator): or ``None`` if element doesn't have a representation or it's not supported. """ 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"): + if rep and len(rep.Items or []) == 1: 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 + # Miter bends and other clipped segments wrap the swept solid in a + # boolean result (e.g. IfcBooleanClippingResult with a half space). + # Unwrap it to reach the underlying extrusion so the segment length + # is still derived from the sweep depth, matching plain segments. + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand + if item.is_a("IfcExtrudedAreaSolid"): + 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("IfcParameterizedProfileDef"): + return item.Depth + settings = ifcopenshell.geom.settings() + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + try: + area_shape = ifcopenshell.geom.create_shape(settings, item.SweptArea) + except RuntimeError: + return + assert isinstance(area_shape, W.Triangulation) + x = ifcopenshell.util.shape.get_x(area_shape) / cls.unit_scale + y = ifcopenshell.util.shape.get_y(area_shape) / cls.unit_scale z = item.Depth return max([x, y, z]) - elif item.SweptArea.is_a("IfcParameterizedProfileDef"): - return item.Depth - settings = ifcopenshell.geom.settings() - settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - try: - area_shape = ifcopenshell.geom.create_shape(settings, item.SweptArea) - except RuntimeError: - return - assert isinstance(area_shape, W.Triangulation) - x = ifcopenshell.util.shape.get_x(area_shape) / cls.unit_scale - y = ifcopenshell.util.shape.get_y(area_shape) / cls.unit_scale - z = item.Depth - return max([x, y, z]) @classmethod def get_weight(