Merge remote-tracking branch 'origin/v0.8.0' into ifcviewer-wgpu

This commit is contained in:
Thomas Krijnen
2026-08-10 03:31:02 +02:00
36 changed files with 1543 additions and 147 deletions
@@ -25,7 +25,7 @@ are automatically created and maintained.
Alignments are created with stationing referents. Each layout segment is assigned a position referent that informs about
the start point of the segment. An example is the point of curvature of a horizontal circular curve. The referent is
nested to the segment representing the circular arc and is named with a indicator of the position and the station, e.g. "P.C. (145+98.32)"
nested to the segment representing the circular arc and is named with the alignment name and an indicator of the position and the station, e.g. "MyAlignment 145+98.32 (P.C.)"
This API does not determine alignment parameters based on rules, such as minimum curve radius as a function of design speed or sight distance.
@@ -79,7 +79,7 @@ from .get_layout_curve import get_layout_curve
from .get_layout_segments import get_layout_segments
from .get_mapped_segments import get_mapped_segments
from .get_parent_alignment import get_parent_alignment
from .get_referent_nest import get_referent_nest
from .get_stationing_nest import get_stationing_nest
from .get_vertical_layout import get_vertical_layout
from .has_zero_length_segment import has_zero_length_segment
from .layout_horizontal_alignment_by_pi_method import (
@@ -89,8 +89,10 @@ from .layout_vertical_alignment_by_pi_method import (
layout_vertical_alignment_by_pi_method,
)
from .name_segments import name_segments
from .update_alignment_parameter_segment_tags import update_alignment_parameter_segment_tags
from .update_end_point import update_end_point
from .update_fallback_position import update_fallback_position
from .update_key_point_referents import update_key_point_referents
from .util import *
__all__ = [
@@ -124,14 +126,16 @@ __all__ = [
"get_layout_curve",
"get_layout_segments",
"get_parent_alignment",
"get_referent_nest",
"get_stationing_nest",
"get_vertical_layout",
"has_zero_length_segment",
"layout_horizontal_alignment_by_pi_method",
"layout_vertical_alignment_by_pi_method",
"name_segments",
"register_referent_name_callback",
"update_alignment_parameter_segment_tags",
"update_end_point",
"update_fallback_position",
"update_key_point_referents",
"get_mapped_segments",
]
@@ -0,0 +1,30 @@
# 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
import ifcopenshell.util.alignment
def _get_key_point_tag(file: ifcopenshell.file, label: str, station: float) -> str:
"""
Builds the station-and-label text shared by update_alignment_parameter_segment_tags (used
directly as IfcAlignmentParameterSegment.StartTag/EndTag) and update_key_point_referents (used,
prefixed with the alignment name, as IfcReferent.Name): "<station> (<label>)", e.g.
"145+98.32 (P.O.B.)".
"""
return f"{ifcopenshell.util.alignment.station_as_string(file, station)} ({label})"
@@ -26,9 +26,10 @@ _cant_callback = None
def register_referent_name_callback(horizontal=None, vertical=None, cant=None):
"""
Referents are automatically created at the start of each horizontal, vertical, and cant segment.
The referents represent key points in the alignment layout such as Point of Curvature, Point of Tangent, and others.
Different juristicions use different naming systems for these key points.
Referents are created at the start of each horizontal, vertical, and cant segment by
ifcopenshell.api.alignment.update_key_point_referents. The referents represent key points in the
alignment layout such as Point of Curvature, Point of Tangent, and others. Different
juristicions use different naming systems for these key points.
The referent name callback functions provide a customizable method for naming these referents. If a callback is registered,
it is called when creating the referent name, otherwise the default naming is used.
@@ -39,8 +40,8 @@ def register_referent_name_callback(horizontal=None, vertical=None, cant=None):
The callback function returns a string that is used in the referent name for the referent at the start of `segment`.
The callback must accomodate the following cases:
* prev_segment = None and segment != None - this indicates the last segment so the "End of Alignment" name is returned
* prev_segment != None and segment == None - this indicates the first segment so the "Beginning of Alignment" name is returned
* prev_segment = None and segment != None - this indicates the first segment so the "Beginning of Alignment" name is returned
* prev_segment != None and segment == None - this indicates the last segment so the "End of Alignment" name is returned
* prev_segment != None and segment != None - this indicates an intermediate segment so a name representitive of the transition is returned
Setting any or all of the callbacks to None causes the default naming to be used.
@@ -0,0 +1,27 @@
# 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/>.
from typing import Callable
from ifcopenshell import entity_instance
def _sort_nest(nest: entity_instance, key: Callable) -> entity_instance:
"""Sorts the RelatedObjects of an IfcRelNests in place, by an arbitrary key function."""
nest.RelatedObjects = sorted(nest.RelatedObjects, key=key)
return nest
@@ -20,6 +20,7 @@ from typing import Optional
import ifcopenshell
import ifcopenshell.api.alignment
from ifcopenshell.api.alignment._sort_nest import _sort_nest
from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position
import ifcopenshell.api.pset
import ifcopenshell.guid
@@ -114,7 +115,7 @@ def add_stationing_referent(
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties=properties)
nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
if nest is None:
nest = file.createIfcRelNests(
GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=(referent,)
@@ -122,8 +123,6 @@ def add_stationing_referent(
else:
nest.RelatedObjects += (referent,)
nest.RelatedObjects = sorted(
nest.RelatedObjects, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station")
)
_sort_nest(nest, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station"))
return referent
@@ -64,22 +64,22 @@ def create_representation(
# if the alignment is created without geometry it's stationing referent isn't related to the alignment geometry.
# the stationing referent needs to be updated to have an IfcLinearPlacement that references the basis curve geometry
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
if (
referent_nest
and 0 < len(referent_nest.RelatedObjects)
and referent_nest.RelatedObjects[0].ObjectPlacement
and not referent_nest.RelatedObjects[0].ObjectPlacement.is_a("IfcLinearPlacement")
stationing_nest
and 0 < len(stationing_nest.RelatedObjects)
and stationing_nest.RelatedObjects[0].ObjectPlacement
and not stationing_nest.RelatedObjects[0].ObjectPlacement.is_a("IfcLinearPlacement")
):
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
if referent_nest.RelatedObjects[0].ObjectPlacement:
if referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.Location:
file.remove(referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.Location)
if referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.RefDirection:
file.remove(referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.RefDirection)
file.remove(referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement)
file.remove(referent_nest.RelatedObjects[0].ObjectPlacement)
if stationing_nest.RelatedObjects[0].ObjectPlacement:
if stationing_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.Location:
file.remove(stationing_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.Location)
if stationing_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.RefDirection:
file.remove(stationing_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.RefDirection)
file.remove(stationing_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement)
file.remove(stationing_nest.RelatedObjects[0].ObjectPlacement)
lp = file.createIfcLinearPlacement(
RelativePlacement=file.createIfcAxis2PlacementLinear(
@@ -93,4 +93,4 @@ def create_representation(
)
)
update_fallback_position(file, lp)
referent_nest.RelatedObjects[0].ObjectPlacement = lp
stationing_nest.RelatedObjects[0].ObjectPlacement = lp
@@ -67,8 +67,8 @@ def distance_along_from_station(file: ifcopenshell.file, alignment: entity_insta
print(dist_along) # 100.00
"""
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
if referent_nest is None:
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
if stationing_nest is None:
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
return station - start_station
@@ -77,7 +77,7 @@ def distance_along_from_station(file: ifcopenshell.file, alignment: entity_insta
_distance_along_of_referent(referent),
ifcopenshell.util.element.get_pset(referent, name="Pset_Stationing", prop="Station"),
)
for referent in referent_nest.RelatedObjects
for referent in stationing_nest.RelatedObjects
]
stations.sort(key=lambda entry: entry[0])
@@ -20,12 +20,18 @@ import ifcopenshell
from ifcopenshell import entity_instance
def get_referent_nest(file: ifcopenshell.file, alignment: entity_instance) -> entity_instance:
def get_stationing_nest(file: ifcopenshell.file, alignment: entity_instance) -> entity_instance:
"""
Searches for the IfcRelNest that contains IfcReferent.
Searches for the IfcRelNests that defines the alignment's stationing scheme.
The returned nest is nested to the IfcAlignment and its RelatedObjects contains only the
IfcReferent(s) (PredefinedType="STATION") that establish the alignment's starting station and
any station equations along it, as created by add_stationing_referent. It does not contain any
other kind of referent (e.g. key-point referents from update_key_point_referents live in their
own, separate IfcRelNests).
:param file:
:param alignment: The IfcAlignment which hosts IfcReferent
:param alignment: The IfcAlignment which hosts the stationing IfcReferent(s)
:return: Returns the IfcRelNests or None
"""
if not alignment.is_a("IfcAlignment"):
@@ -0,0 +1,108 @@
# 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
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._get_key_point_tag import _get_key_point_tag
from ifcopenshell.api.alignment._get_segment_start_point_label import (
_get_segment_start_point_label,
)
def update_alignment_parameter_segment_tags(
file: ifcopenshell.file, layout: entity_instance, label_end_tag: bool = False
) -> None:
"""
Sets IfcAlignmentParameterSegment.StartTag (and, optionally, EndTag) for every segment
transition in an alignment layout. Unlike update_key_point_referents, this does not create any
IfcReferent or IfcRelNests -- it only mutates the StartTag/EndTag string attributes already
present on each segment's DesignParameters.
Every real segment's StartTag is set to a computed tag describing the point where it begins,
using the same label-and-station format as update_key_point_referents' Name minus the alignment
name (via _get_key_point_tag), e.g. "145+98.32 (P.C.)". The first segment's StartTag comes from
the "Beginning of Alignment" boundary label.
EndTag is left untouched unless `label_end_tag` is True. When enabled, for each transition
between two consecutive segments, the outgoing segment's EndTag is set to the same tag as the
incoming segment's StartTag (they describe the same physical point), and the last segment's
EndTag is set from the "End of Alignment" boundary label.
Labels come from _get_segment_start_point_label -- if a callback has been registered via
register_referent_name_callback(), its output is used instead of the built-in labels, exactly as
in update_key_point_referents.
:param layout: IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
:param label_end_tag: if True, also sets EndTag on every real segment. If False (default),
EndTag is left untouched.
:return: None -- this function mutates segment.DesignParameters.StartTag/EndTag in place
Example:
.. code:: python
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(model, horizontal)
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
alignment = ifcopenshell.api.alignment.get_alignment(layout)
if alignment is None:
raise ValueError(f"{layout.is_a()} #{layout.id()} is not nested under an IfcAlignment.")
segments = list(ifcopenshell.api.alignment.get_layout_segments(layout))
if segments and ifcopenshell.api.alignment.has_zero_length_segment(layout):
segments = segments[:-1]
if not segments:
return
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
is_horizontal = layout.is_a("IfcAlignmentHorizontal")
distance_along = 0.0
prev_segment = None
for segment in segments:
dp = segment.DesignParameters
seg_distance_along = distance_along if is_horizontal else dp.StartDistAlong
label = _get_segment_start_point_label(prev_segment, segment)
station = start_station + seg_distance_along
tag = _get_key_point_tag(file, label, station)
dp.StartTag = tag
if prev_segment is not None and label_end_tag:
prev_segment.DesignParameters.EndTag = tag
if is_horizontal:
distance_along += dp.SegmentLength
else:
distance_along = dp.StartDistAlong + dp.HorizontalLength
prev_segment = segment
if label_end_tag:
label = _get_segment_start_point_label(prev_segment, None)
station = start_station + distance_along
prev_segment.DesignParameters.EndTag = _get_key_point_tag(file, label, station)
@@ -0,0 +1,217 @@
# 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/>.
from typing import Optional
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.api.pset
import ifcopenshell.guid
import ifcopenshell.util.element
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._get_key_point_tag import _get_key_point_tag
from ifcopenshell.api.alignment._get_segment_start_point_label import (
_get_segment_start_point_label,
)
from ifcopenshell.api.alignment._sort_nest import _sort_nest
from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position
def _remove_referent(file: ifcopenshell.file, referent: entity_instance) -> None:
"""Cleanly deletes a key-point IfcReferent: its Pset_Stationing, its ObjectPlacement (if
exclusively owned by it), and finally the referent itself."""
for inverse in list(file.get_inverse(referent)):
if inverse.is_a("IfcRelDefinesByProperties"):
ifcopenshell.api.pset.remove_pset(file, product=referent, pset=inverse.RelatingPropertyDefinition)
object_placement = referent.ObjectPlacement
if object_placement and file.get_total_inverses(object_placement) == 1:
referent.ObjectPlacement = None
ifcopenshell.util.element.remove_deep2(file, object_placement)
file.remove(referent) # also strips referent out of any IfcRelNests.RelatedObjects referencing it
def _create_key_point_referent(
file: ifcopenshell.file,
alignment: entity_instance,
curve: Optional[entity_instance],
label: str,
distance_along: float,
station: float,
) -> entity_instance:
if curve and curve.is_a("IfcCompositeCurve") and 0 < len(curve.Segments):
object_placement = file.createIfcLinearPlacement(
RelativePlacement=file.createIfcAxis2PlacementLinear(
Location=file.createIfcPointByDistanceExpression(
DistanceAlong=file.createIfcLengthMeasure(distance_along),
OffsetLateral=None,
OffsetVertical=None,
OffsetLongitudinal=None,
BasisCurve=curve,
)
),
)
update_fallback_position(file, object_placement)
else:
object_placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint(alignment.ObjectPlacement.RelativePlacement.Location.Coordinates)
),
)
name = f"{alignment.Name} {_get_key_point_tag(file, label, station)}"
referent = file.createIfcReferent(
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=name,
Description=None,
ObjectType=None,
ObjectPlacement=object_placement,
Representation=None,
PredefinedType="POSITION",
)
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": station})
return referent
def update_key_point_referents(
file: ifcopenshell.file,
layout: entity_instance,
rel_nests: Optional[entity_instance] = None,
clear: bool = False,
) -> entity_instance:
"""
Creates IfcReferent key-point markers for every segment transition in an alignment layout.
Labels are derived from _get_segment_start_point_label (e.g. "P.C.", "P.T.", "P.O.B.",
"P.V.C.", ...), and combined with the alignment name and station to build the Name, e.g.
"MyAlignment 145+98.32 (P.C.)". Different jurisdictions use
different naming systems for these key points -- register_referent_name_callback() lets a
caller override the default horizontal/vertical/cant labeling before calling this function; if
a callback is registered, its output is used here instead of the built-in labels. Referents are
nested to `rel_nests`, an IfcRelNests distinct from the layout's segment nest (found via
get_alignment_segment_nest) and from the alignment's stationing nest (found via
get_stationing_nest) -- key-point referents never belong in either of those.
:param layout: IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
:param rel_nests: an existing IfcRelNests to (re)populate; its RelatingObject must be the
IfcAlignment that nests `layout` (TypeError is raised otherwise). If omitted, a new
IfcRelNests is always created and related to that IfcAlignment -- there is no implicit
search for or reuse of a previously created nest. Callers who want to regenerate into an
existing nest must pass it back in explicitly via `rel_nests`.
:param clear: if True, deletes all IfcReferent currently in rel_nests.RelatedObjects (and their
Pset_Stationing) before regenerating. If False (default), new referents are appended to
whatever already exists -- no deduplication.
:return: the IfcRelNests, with RelatedObjects sorted ascending by Pset_Stationing.Station
Example:
.. code:: python
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
nest = ifcopenshell.api.alignment.update_key_point_referents(model, horizontal)
Example, with custom labels for a jurisdiction that doesn't use the built-in abbreviations:
.. code:: python
def my_horizontal_labels(prev_segment, segment):
if prev_segment is None:
return "Start"
if segment is None:
return "End"
return "Curve Point" # a name representative of the prev_segment -> segment transition
ifcopenshell.api.alignment.register_referent_name_callback(horizontal=my_horizontal_labels)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
nest = ifcopenshell.api.alignment.update_key_point_referents(model, horizontal)
# nest.RelatedObjects[0].Name ends with "(Start)" instead of the default "(P.O.B.)"
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
alignment = ifcopenshell.api.alignment.get_alignment(layout)
if alignment is None:
raise ValueError(f"{layout.is_a()} #{layout.id()} is not nested under an IfcAlignment.")
if rel_nests is not None:
if not rel_nests.RelatingObject.is_a("IfcAlignment"):
raise TypeError(
f"Expected rel_nests.RelatingObject to be IfcAlignment, instead received "
f"{rel_nests.RelatingObject.is_a()}"
)
else:
rel_nests = file.createIfcRelNests(
GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=()
)
if clear:
for referent in list(rel_nests.RelatedObjects):
_remove_referent(file, referent)
rel_nests.RelatedObjects = ()
segments = list(ifcopenshell.api.alignment.get_layout_segments(layout))
if segments and ifcopenshell.api.alignment.has_zero_length_segment(layout):
segments = segments[:-1]
if not segments:
_sort_nest(
rel_nests, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station")
)
return rel_nests
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
is_horizontal = layout.is_a("IfcAlignmentHorizontal")
new_referents = []
distance_along = 0.0
prev_segment = None
for segment in segments:
dp = segment.DesignParameters
seg_distance_along = distance_along if is_horizontal else dp.StartDistAlong
label = _get_segment_start_point_label(prev_segment, segment)
station = start_station + seg_distance_along
new_referents.append(_create_key_point_referent(file, alignment, curve, label, seg_distance_along, station))
if is_horizontal:
distance_along += dp.SegmentLength
else:
distance_along = dp.StartDistAlong + dp.HorizontalLength
prev_segment = segment
label = _get_segment_start_point_label(prev_segment, None)
station = start_station + distance_along
new_referents.append(_create_key_point_referent(file, alignment, curve, label, distance_along, station))
rel_nests.RelatedObjects = tuple(rel_nests.RelatedObjects) + tuple(new_referents)
_sort_nest(rel_nests, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station"))
return rel_nests