Alignment API update for station and positioning referents. Fixes bug with fallback position.

This commit is contained in:
Richard Brice
2026-07-09 14:10:33 -07:00
parent 9ae79b42dd
commit 206cd6bbe1
11 changed files with 366 additions and 45 deletions
@@ -0,0 +1,100 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# 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 <http://www.gnu.org/licenses/>.
import ifcopenshell.api.alignment
import ifcopenshell.api.context
import ifcopenshell.api.unit
import ifcopenshell.util.element
def test_add_positioning_referent():
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,
)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", start_station=2000.0)
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
segment = ifcopenshell.api.alignment.get_layout_segments(horizontal_layout)[0]
referent = ifcopenshell.api.alignment.add_positioning_referent(
file, "P.C.", alignment, distance_along=0.0, station=2000.0, positioned_product=segment
)
assert referent.is_a("IfcReferent")
assert referent.PredefinedType == "POSITION"
assert referent.Name == "P.C."
assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing")
assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing", prop="Station") == 2000.0
assert referent.ObjectPlacement != None
assert len(referent.Positions) == 1
rel_positions = referent.Positions[0]
assert rel_positions.is_a("IfcRelPositions")
assert rel_positions.RelatingPositioningElement == referent
assert rel_positions.RelatedProducts == (segment,)
def test_add_positioning_referent_creates_separate_referent_per_call():
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,
)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", start_station=2000.0)
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
segment = ifcopenshell.api.alignment.get_layout_segments(horizontal_layout)[0]
first_referent = ifcopenshell.api.alignment.add_positioning_referent(
file, "P.C.", alignment, distance_along=0.0, station=2000.0, positioned_product=segment
)
other_product = file.createIfcBuildingElementProxy(GlobalId=ifcopenshell.guid.new(), Name="Sign")
second_referent = ifcopenshell.api.alignment.add_positioning_referent(
file, "P.C.", alignment, distance_along=0.0, station=2000.0, positioned_product=other_product
)
# each call creates its own IfcReferent, each with its own IfcRelPositions to the product passed in
assert first_referent != second_referent
assert len(first_referent.Positions) == 1
assert first_referent.Positions[0].RelatedProducts == (segment,)
assert len(second_referent.Positions) == 1
assert second_referent.Positions[0].RelatedProducts == (other_product,)
test_add_positioning_referent()
test_add_positioning_referent_creates_separate_referent_per_call()
@@ -48,5 +48,26 @@ def test_add_stationing_to_alignment():
assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing", prop="Station") == 2000.0
assert referent.ObjectPlacement != None
# add a station equation at 1000 distance along. this is station 3+000 in coming and 4+000 outgoing.
# this is a gap equation.
second_referent = ifcopenshell.api.alignment.add_stationing_referent(
file, "4+000.000", alignment, distance_along=1000.0, station=4000.0, incoming_station=3000.0
)
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 2
assert second_referent == referent_nest.RelatedObjects[1]
assert second_referent.PredefinedType == "STATION"
assert second_referent.Name == "4+000.000"
assert ifcopenshell.util.element.get_pset(element=second_referent, name="Pset_Stationing")
assert ifcopenshell.util.element.get_pset(element=second_referent, name="Pset_Stationing", prop="Station") == 4000.0
assert (
ifcopenshell.util.element.get_pset(element=second_referent, name="Pset_Stationing", prop="IncomingStation")
== 3000.0
)
assert second_referent.ObjectPlacement != None
test_add_stationing_to_alignment()
@@ -53,4 +53,56 @@ def test_distance_along_from_station():
assert ifcopenshell.api.alignment.distance_along_from_station(file, alignment, 17525.36) == pytest.approx(7525.36)
def test_distance_along_from_station_with_station_equations():
# Reproduces the worked example from the IFC Alignment Geometry Implementation Guide, chapter 9.2.6:
# a gap equation (P3: incoming 14+00.00, outgoing 17+00.00) and an overlap equation
# (P4: incoming 19+00.00, outgoing 18+50.00).
file = ifcopenshell.file(schema="IFC4X3")
project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="Test")
length = ifcopenshell.api.unit.add_conversion_based_unit(file, name="foot")
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,
)
coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)]
radii = [(1000.0), (1250.0), (950.0)]
vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)]
lengths = [(1600.0), (1200.0), (2000.0), (800.0)]
alignment = ifcopenshell.api.alignment.create_by_pi_method(
file, "TestAlignment", coordinates, radii, vpoints, lengths, start_station=1000.0
)
ifcopenshell.api.alignment.add_stationing_referent(
file, "P3", alignment, distance_along=400.0, station=1700.0, incoming_station=1400.0
)
ifcopenshell.api.alignment.add_stationing_referent(
file, "P4", alignment, distance_along=600.0, station=1850.0, incoming_station=1900.0
)
distance_along_from_station = ifcopenshell.api.alignment.distance_along_from_station
# between P2 and P3: Sta. 13+00.00
assert distance_along_from_station(file, alignment, 1300.0) == pytest.approx(300.0)
# between P3 and P4: Sta. 18+00.00
assert distance_along_from_station(file, alignment, 1800.0) == pytest.approx(500.0)
# between P4 and P5: Sta. 19+25.00
assert distance_along_from_station(file, alignment, 1925.0) == pytest.approx(675.0)
# Sta. 15+00.00 falls inside the gap opened by the equation at P3 and has no corresponding distance along
assert distance_along_from_station(file, alignment, 1500.0) is None
# Sta. 18+75.00 falls inside the overlap zone at P4; the post-equation (outgoing) match is returned
assert distance_along_from_station(file, alignment, 1875.0) == pytest.approx(625.0)
test_distance_along_from_station()
test_distance_along_from_station_with_station_equations()