From 846f6e36e5fbeec43765e60fb20dcfd4680c1437 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Tue, 21 Jul 2026 17:26:53 +0300 Subject: [PATCH] ifcopenshell.api.geometry: author door swing arcs as IfcCircle, not IfcEllipse The door swing arc in the Plan/Body PLAN_VIEW representation was built with slightly unequal semi-axes (one radius was shortened by the panel depth), so every door authored an IfcEllipse where the swing path is actually a circle. shape_builder.create_ellipse_curve now emits IfcCircle whenever the two requested radii are equal, and the door radius calculation was corrected so both radii match the true swing radius (panel width), so the curve is now a true circle. This was AI-assisted (Claude), reviewed and tested before committing. --- .../api/geometry/add_door_representation.py | 5 ++-- .../ifcopenshell/util/shape_builder.py | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py index 6174d9d2d7..96b70bb5d8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py @@ -454,11 +454,12 @@ class Usecase: door_items.append(second_swing_line) else: trim_points_mask = (0, 1) + # Swing path is a true circle: both radii equal panel_size[np_Y]. semicircle = builder.create_ellipse_curve( - panel_size[np_Y] - panel_size[np_X], + panel_size[np_Y], panel_size[np_Y], trim_points_mask=trim_points_mask, - position=(panel_size[np_X], 0), + position=(0, 0), ) door_items.append(semicircle) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 1a133e041a..88c2d95aba 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -688,7 +688,12 @@ class ShapeBuilder: ) -> ifcopenshell.entity_instance: """Create an IfcEllipse, optionally trimmed to an arc. - If neither ``trim_points`` nor ``trim_points_mask`` is provided, a full IfcEllipse is returned. + If the two semi-axes are equal, the curve is geometrically a circle and an + IfcCircle is created instead of an IfcEllipse, so that downstream consumers + that special-case circles (e.g. DXF arc/circle export) recognise it as such. + + If neither ``trim_points`` nor ``trim_points_mask`` is provided, a full IfcEllipse + (or IfcCircle) is returned. Trimming points must be given in counter-clockwise order. For example, to get the arc above the Y-axis use mask ``(0, 2)``; below the Y-axis use ``(2, 0)``. @@ -702,16 +707,19 @@ class ShapeBuilder: :param ref_x_direction: Direction of the local X axis. :param trim_points_mask: Pair of cardinal-point indices (0–3) used when ``trim_points`` is empty. See :meth:`get_trim_points_from_mask` for index definitions. - :return: IfcEllipse (untrimmed) or IfcTrimmedCurve (trimmed). + :return: IfcEllipse/IfcCircle (untrimmed) or IfcTrimmedCurve (trimmed). """ ifc_position = self.create_axis2_placement_2d(position, ref_x_direction) - ifc_ellipse = self.file.createIfcEllipse( - Position=ifc_position, SemiAxis1=x_axis_radius, SemiAxis2=y_axis_radius - ) + if x_axis_radius == y_axis_radius: + ifc_curve = self.file.createIfcCircle(Position=ifc_position, Radius=x_axis_radius) + else: + ifc_curve = self.file.createIfcEllipse( + Position=ifc_position, SemiAxis1=x_axis_radius, SemiAxis2=y_axis_radius + ) if not trim_points: if not trim_points_mask: - return ifc_ellipse + return ifc_curve trim_points = self.get_trim_points_from_mask( x_axis_radius, y_axis_radius, trim_points_mask, position_offset=position ) @@ -719,10 +727,10 @@ class ShapeBuilder: trim1 = [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(trim_points[0]))] trim2 = [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(trim_points[1]))] - trim_ellipse = self.file.createIfcTrimmedCurve( - BasisCurve=ifc_ellipse, Trim1=trim1, Trim2=trim2, SenseAgreement=True, MasterRepresentation="CARTESIAN" + trim_curve = self.file.createIfcTrimmedCurve( + BasisCurve=ifc_curve, Trim1=trim1, Trim2=trim2, SenseAgreement=True, MasterRepresentation="CARTESIAN" ) - return trim_ellipse + return trim_curve def profile( self,