From 52d894298e3fd24abb2ee4b8e2c8b4a70842f2f9 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Fri, 10 Jul 2026 07:55:51 -0700 Subject: [PATCH 1/4] Fixes double unit conversion when convert-back-units are used --- src/ifcwrap/IfcGeomWrapper.i | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 81c2a53795..404ecd7ded 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -977,12 +977,14 @@ struct ShapeRTTI : public boost::static_visitor if (item == nullptr) { throw IfcParse::IfcException("Failed to convert placement"); } + /* if (st.get().get()) { // we pass the settings to the Transformation object, but access the data just offloads to the // generic cartesian_base so there's no time to apply the settings to the translation part. item = ifcopenshell::geometry::taxonomy::matrix4::ptr(item->clone_()); item->components().col(3).head<3>() /= kernel.settings().get().get(); } + */ return new IfcGeom::Transformation(kernel.settings(), item); } else { if (!representation) { From 47a20f0c7c6973d42f225397b25671d008d87023 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:45:38 -0700 Subject: [PATCH 2/4] Locates positioning referent on the alignment curve, not the basis curve --- .../ifcopenshell/api/alignment/add_positioning_referent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_positioning_referent.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_positioning_referent.py index fa72e24ff4..aa1b751707 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_positioning_referent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_positioning_referent.py @@ -51,11 +51,11 @@ def add_positioning_referent( ifcopenshell.api.alignment.add_positioning_referent(model,name="Pier 1 Sta 1+00",alignment=alignment,distance_along=0.0,station=100.0,positioned_product=pier) """ - basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + curve = ifcopenshell.api.alignment.get_curve(alignment) object_placement = None representation = None - if basis_curve and basis_curve.is_a("IfcCompositeCurve") and 0 < len(basis_curve.Segments): + if curve and curve.is_a("IfcCompositeCurve") and 0 < len(curve.Segments): object_placement = file.createIfcLinearPlacement( RelativePlacement=file.createIfcAxis2PlacementLinear( Location=file.createIfcPointByDistanceExpression( @@ -63,7 +63,7 @@ def add_positioning_referent( OffsetLateral=None, OffsetVertical=None, OffsetLongitudinal=None, - BasisCurve=basis_curve, + BasisCurve=curve, ) ), ) From b5c1b81edef470a40ae8cecf45a458e8f3d1250a Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:46:11 -0700 Subject: [PATCH 3/4] Stationing referent can optionally be located relative to the basis_curve (default) or the alignment curve --- .../api/alignment/add_stationing_referent.py | 11 +- .../alignment/test_add_stationing_referent.py | 115 ++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/ifcopenshell-python/test/api/alignment/test_add_stationing_referent.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py index 58dcaa3765..621712e093 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py @@ -34,6 +34,7 @@ def add_stationing_referent( distance_along: float, station: float, incoming_station: Optional[float] = None, + on_basis_curve: Optional[bool] = None, ) -> entity_instance: """ Adds an IfcReferent to the alignment that defines the stationing system. @@ -43,6 +44,7 @@ def add_stationing_referent( :param distance_along: distance along the alignment basis curve :param station: station value :param incoming_station: station value of the incoming segment, only set to specify a station equation + :param on_basis_curve: whether the referent is positioned on the basis curve or the alignment curve, if None the function will default to the basis curve :return: referent Example: @@ -53,11 +55,14 @@ def add_stationing_referent( ifcopenshell.api.alignment.add_stationing_referent(model,name="1+00.0",alignment=alignment,distance_along=0.0,station=100.0) """ - basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + if on_basis_curve is None: + on_basis_curve = True + + curve = ifcopenshell.api.alignment.get_basis_curve(alignment) if on_basis_curve else ifcopenshell.api.alignment.get_curve(alignment) object_placement = None representation = None - if basis_curve and basis_curve.is_a("IfcCompositeCurve") and 0 < len(basis_curve.Segments): + if curve and curve.is_a("IfcCompositeCurve") and 0 < len(curve.Segments): object_placement = file.createIfcLinearPlacement( RelativePlacement=file.createIfcAxis2PlacementLinear( Location=file.createIfcPointByDistanceExpression( @@ -65,7 +70,7 @@ def add_stationing_referent( OffsetLateral=None, OffsetVertical=None, OffsetLongitudinal=None, - BasisCurve=basis_curve, + BasisCurve=curve, ) ), ) diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_stationing_referent.py b/src/ifcopenshell-python/test/api/alignment/test_add_stationing_referent.py new file mode 100644 index 0000000000..f2082102a9 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_stationing_referent.py @@ -0,0 +1,115 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + + +import ifcopenshell.api.alignment +import ifcopenshell.api.context +import ifcopenshell.api.unit +import ifcopenshell.util.element + + +def _create_test_file(): + file = ifcopenshell.file(schema="IFC4X3") + project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="Test") + length = ifcopenshell.api.unit.add_si_unit(file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(file, units=[length]) + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + return file + + +def _create_test_alignment_with_vertical(file): + # include_vertical=True so that get_curve() (IfcGradientCurve, on the "Axis" representation) + # and get_basis_curve() (IfcCompositeCurve, on the "FootPrint" representation) are different + # entities, letting the on_basis_curve option be observed. + alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", include_vertical=True, start_station=0.0) + assert ifcopenshell.api.alignment.get_basis_curve(alignment).is_a("IfcCompositeCurve") + assert ifcopenshell.api.alignment.get_curve(alignment).is_a("IfcGradientCurve") + assert ifcopenshell.api.alignment.get_basis_curve(alignment) != ifcopenshell.api.alignment.get_curve(alignment) + return alignment + + +def _assert_common_referent_asserts(referent, name, station): + assert referent.is_a("IfcReferent") + assert referent.PredefinedType == "STATION" + assert referent.Name == name + assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing") + assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing", prop="Station") == station + assert referent.ObjectPlacement != None + + +def test_add_stationing_referent_on_basis_curve_none_defaults_to_basis_curve(): + # on_basis_curve=None should behave the same as on_basis_curve=True + file = _create_test_file() + alignment = _create_test_alignment_with_vertical(file) + + referent = ifcopenshell.api.alignment.add_stationing_referent( + file, "1+00.000", alignment, distance_along=100.0, station=100.0, on_basis_curve=None + ) + + _assert_common_referent_asserts(referent, "1+00.000", 100.0) + + assert referent.ObjectPlacement.is_a("IfcLinearPlacement") + assert referent.ObjectPlacement.RelativePlacement.Location.BasisCurve == ifcopenshell.api.alignment.get_basis_curve( + alignment + ) + + +def test_add_stationing_referent_on_basis_curve_true(): + file = _create_test_file() + alignment = _create_test_alignment_with_vertical(file) + + referent = ifcopenshell.api.alignment.add_stationing_referent( + file, "1+00.000", alignment, distance_along=100.0, station=100.0, on_basis_curve=True + ) + + _assert_common_referent_asserts(referent, "1+00.000", 100.0) + + assert referent.ObjectPlacement.is_a("IfcLinearPlacement") + assert referent.ObjectPlacement.RelativePlacement.Location.BasisCurve == ifcopenshell.api.alignment.get_basis_curve( + alignment + ) + + +def test_add_stationing_referent_on_basis_curve_false(): + # with a vertical layout present, on_basis_curve=False positions the referent on the + # alignment curve (IfcGradientCurve) rather than on the basis curve (IfcCompositeCurve). + file = _create_test_file() + alignment = _create_test_alignment_with_vertical(file) + + referent = ifcopenshell.api.alignment.add_stationing_referent( + file, "1+00.000", alignment, distance_along=100.0, station=100.0, on_basis_curve=False + ) + + _assert_common_referent_asserts(referent, "1+00.000", 100.0) + + assert referent.ObjectPlacement.is_a("IfcLinearPlacement") + basis_curve = referent.ObjectPlacement.RelativePlacement.Location.BasisCurve + assert basis_curve == ifcopenshell.api.alignment.get_curve(alignment) + assert basis_curve != ifcopenshell.api.alignment.get_basis_curve(alignment) + + +test_add_stationing_referent_on_basis_curve_none_defaults_to_basis_curve() +test_add_stationing_referent_on_basis_curve_true() +test_add_stationing_referent_on_basis_curve_false() From ade03b171a5f9c02f1ace6eea9af98715dae993b Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:54:03 -0700 Subject: [PATCH 4/4] Fixes bug with fallback position introduced in 206cd6bb --- .../api/alignment/update_fallback_position.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/update_fallback_position.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_fallback_position.py index 85318d781f..431d19fc86 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/update_fallback_position.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_fallback_position.py @@ -36,12 +36,9 @@ def update_fallback_position(file: ifcopenshell.file, lp: entity_instance): p = ifcopenshell.util.placement.get_local_placement(lp) - - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) - - x = float(p[0, 3])*unit_scale - y = float(p[1, 3])*unit_scale - z = float(p[2, 3])*unit_scale + x = float(p[0, 3]) + y = float(p[1, 3]) + z = float(p[2, 3]) rx = float(p[0, 0]) ry = float(p[1, 0])