Reverts from automatically adding stationing to alignments because of missing initial geometry. Adds support for stationing with decreasing values.

This commit is contained in:
Richard Brice
2026-08-31 15:34:21 -07:00
parent 971cccc186
commit b5670c4fc5
21 changed files with 394 additions and 131 deletions
@@ -51,7 +51,7 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
self.file = tool.Ifc.get()
start = time.time()
alignment = ifcopenshell.api.alignment.create_from_csv(self.file, self.filepath)
alignment = ifcopenshell.api.alignment.create_from_csv(self.file, self.filepath, start_station=0.0)
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
sites = self.file.by_type("IfcSite")
@@ -23,9 +23,11 @@ This API is defined in terms of the semantic definition of an alignment. The cor
is created and maintained automatically. The manditory zero length segment for the semantic and geometric definitions
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 the alignment name and an indicator of the position and the station, e.g. "MyAlignment 145+98.32 (P.C.)"
Stationing is defined explicitly by calling add_stationing_referent() after the layout segments (and therefore the
basis curve geometry) exist - create() does not add one. update_key_point_referents() assigns each layout segment 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 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.
@@ -35,7 +37,7 @@ Presently, this API supports:
1. Creating alignments, both horizontal and vertical, using the PI method. Alignment definition can be read from a CSV file.
2. Creating alignments segment by segment.
3. Automatic creation of geometric definitions (IfcCompositeCurve, IfcGradientCurve, IfcSegmentedReferenceCurve)
4. Automatic definition of stationing
4. Explicit definition of stationing, including station equations and reverse (decreasing) stationing
5. Automatic definition of alignment transition point referents
6. Utility functions for printing business logical and geometric representations, as well as minimal geometry evaluations
@@ -0,0 +1,37 @@
# 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 ifcopenshell import entity_instance
def _referent_distance_along(referent: entity_instance) -> float:
"""The distance along the basis curve at which a referent is placed.
Read from IfcLinearPlacement.RelativePlacement.Location.DistanceAlong. An
IfcLocalPlacement fallback (semantic-only alignment, or a placement that could not
yet be expressed relative to a basis curve) carries no DistanceAlong; it is only ever
used for the starting referent, at distance 0.0 (the global origin).
Referents nested under an alignment are ordered by increasing DistanceAlong (IFC CT
4.1.4.4.3), which - for a reverse-stationed alignment - means by decreasing Station.
Sort on this, never on Pset_Stationing.Station.
"""
placement = referent.ObjectPlacement
if placement and placement.is_a("IfcLinearPlacement"):
return placement.RelativePlacement.Location.DistanceAlong.wrappedValue
return 0.0
@@ -22,8 +22,8 @@ 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._referent_distance_along import _referent_distance_along
from ifcopenshell.api.alignment._sort_nest import _sort_nest
from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position
@@ -35,16 +35,28 @@ def add_stationing_referent(
distance_along: float,
station: float,
incoming_station: Optional[float] = None,
has_increasing_station: Optional[bool] = None,
on_basis_curve: Optional[bool] = None,
) -> entity_instance:
"""
Adds an IfcReferent to the alignment that defines the stationing system.
Call this once with ``distance_along=0.0`` to define the starting station, and again
for each station equation. If the alignment has no geometry yet, the referent is
placed with an IfcLocalPlacement at the global origin; once the basis curve has real
segments it is placed with an IfcLinearPlacement at ``distance_along`` on that curve.
create_representation() restates an origin-placed starting referent onto the curve
when geometry is added later.
:param name: name to assign to IfcReferent.Name, typically a stringized version of the station value
:param alignment: the alignment to receive the 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 has_increasing_station: sets Pset_Stationing.HasIncreasingStation, which records the direction of
stationing for the referents nested after this one. Leave None (the default) or pass True for the
common case where station values increase with distance along; pass False on the starting referent
of a reverse-stationed alignment, where station values decrease as distance along increases.
: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
@@ -82,10 +94,13 @@ def add_stationing_referent(
update_fallback_position(file, object_placement)
else:
# No resolvable basis curve yet: place the referent at the global origin. Once
# geometry exists, create_representation() restates the starting referent onto the
# curve at DistanceAlong 0.0.
object_placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint(alignment.ObjectPlacement.RelativePlacement.Location.Coordinates)
Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0))
),
)
@@ -111,6 +126,8 @@ def add_stationing_referent(
properties = {"Station": station}
if incoming_station is not None:
properties["IncomingStation"] = incoming_station
if has_increasing_station is not None:
properties["HasIncreasingStation"] = has_increasing_station
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties=properties)
@@ -123,6 +140,8 @@ def add_stationing_referent(
else:
nest.RelatedObjects += (referent,)
_sort_nest(nest, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station"))
# Referents are ordered by increasing DistanceAlong (IFC CT 4.1.4.4.3), which for a
# reverse-stationed alignment is decreasing Station - so sort on DistanceAlong, not Station.
_sort_nest(nest, key=_referent_distance_along)
return referent
@@ -20,7 +20,6 @@ import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
import ifcopenshell.util.alignment
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._add_zero_length_segment import _add_zero_length_segment
from ifcopenshell.api.alignment._create_geometric_representation import (
@@ -34,7 +33,6 @@ def create(
include_vertical: bool = False,
include_cant: bool = False,
include_geometry: bool = True,
start_station: float = 0.0,
) -> entity_instance:
"""
Creates a new alignment with a horizontal layout. Optionally, vertical and cant layouts can be created as well.
@@ -49,19 +47,15 @@ def create(
The horizontal geometry in the Viennese Bend transition curves depends on the Viennese Bend cant parameters. create_layout_segment() automatically creates
the geometric representation from the semantic definition. The horizontal segment geometric representation will fail if the cant segment is not defined.
If geometric representations are created, the alignment stationing referent is also created using the start_station value. IfcReferent.ObjectPlacement
is required for linear positiion elements and IfcLinearPlacement is defined relative to alignment curve geometry.
This referent's Name follows the same "<alignment name> <station>" convention update_key_point_referents() uses
for its own key-point referents (e.g. "MyAlignment 49+00.00"), so that every referent nested under an alignment
is identifiable by name alone, without needing to inspect its Pset_Stationing or placement to know which
alignment it belongs to.
This function does not define the alignment's stationing. Call add_stationing_referent() once the layout
segments (and therefore the basis curve geometry) exist to place the starting-station IfcReferent, and again
for any station equations. Until stationing is defined, get_alignment_start_station() reports 0.0.
:param file:
:param name: name assigned to IfcAlignment.Name
:param include_vertical: If True, IfcAlignmentVertical is created. IfcGradientCurve is created if include_geometry is True
:param include_cant: If True, IfcAlignmentCant is created. IfcSegmentedReferenceCurve is created if include_geometry is True
:param include_geometry: If True, the geometric representations are added
:param start_station: station value at the start of the alignment.
:return: Returns an IfcAlignment
"""
alignment = file.createIfcAlignment(
@@ -90,9 +84,6 @@ def create(
if include_geometry:
_create_geometric_representation(file, alignment)
referent_name = f"{name} {ifcopenshell.util.alignment.station_as_string(file, start_station)}"
referent = ifcopenshell.api.alignment.add_stationing_referent(file, referent_name, alignment, 0.0, start_station)
for layout in alignment_layouts:
_add_zero_length_segment(file, layout)
@@ -18,6 +18,7 @@
import math
from collections.abc import Sequence
from typing import Optional
import ifcopenshell
import ifcopenshell.api.aggregate
@@ -123,22 +124,23 @@ def create_as_polyline(
file: ifcopenshell.file,
name: str,
points: Sequence[entity_instance],
start_station: float = 0.0,
start_station: Optional[float] = None,
) -> entity_instance:
"""
Creates a new IfcAlignment with an IfcPolyline representation.
The IfcAlignment is aggreated to IfcProject
The stationing referent created from start_station has Name "<alignment name> <station>"
(e.g. "MyAlignment 49+00.00"), the same convention update_key_point_referents() and
create() use for their own referents, so every referent nested under an alignment is
identifiable by name alone.
If start_station is given, a STATION IfcReferent named "<alignment name> <station>"
(e.g. "MyAlignment 49+00.00") is added at distance along 0.0 - the same naming
convention update_key_point_referents() uses for its own referents, so every referent
nested under an alignment is identifiable by name alone. If None (the default), no
stationing referent is created.
:param file:
:param name: name assigned to IfcAlignment.Name
:param points: sequence of points defining the polyline
:param start_station: station value at the start of the alignment
:param start_station: station value at the start of the alignment, or None for no stationing referent
:return: Returns an IfcAlignment
"""
alignment = file.createIfcAlignment(
@@ -149,8 +151,9 @@ def create_as_polyline(
_create_polyline_representation(file, alignment, points)
# define stationing
referent_name = f"{alignment.Name} {ifcopenshell.util.alignment.station_as_string(file, start_station)}"
referent = ifcopenshell.api.alignment.add_stationing_referent(file, referent_name, alignment, 0.0, start_station)
if start_station is not None:
referent_name = f"{alignment.Name} {ifcopenshell.util.alignment.station_as_string(file, start_station)}"
ifcopenshell.api.alignment.add_stationing_referent(file, referent_name, alignment, 0.0, start_station)
# IFC 4.1.4.1.1 Alignment Aggregation To Project
project = file.by_type("IfcProject")[0]
@@ -17,9 +17,11 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from collections.abc import Sequence
from typing import Optional
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.util.alignment
from ifcopenshell import entity_instance
@@ -30,7 +32,7 @@ def create_by_pi_method(
radii: Sequence[float],
vpoints: Sequence[Sequence[float]] = None,
lengths: Sequence[float] = None,
start_station: float = 0.0,
start_station: Optional[float] = None,
) -> entity_instance:
"""
Create an alignment using the PI layout method for both horizontal and vertical alignments.
@@ -41,16 +43,21 @@ def create_by_pi_method(
:param radii: radii values to use for transition
:param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end.
:param lengths: parabolic vertical curve horizontal length values to use for transition
:param start_station: if given, the starting station value. A STATION IfcReferent named
"<name> <station string>" is added at distance along 0.0 once the geometry exists. If None
(the default), no stationing referent is created and get_alignment_start_station() reports 0.0.
:return: Returns an IfcAlignment
"""
include_vertical = True if vpoints and lengths else False
alignment = ifcopenshell.api.alignment.create(
file, name, include_vertical=include_vertical, start_station=start_station
)
alignment = ifcopenshell.api.alignment.create(file, name, include_vertical=include_vertical)
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
ifcopenshell.api.alignment.layout_horizontal_alignment_by_pi_method(file, horizontal_layout, hpoints, radii)
if include_vertical:
vertical_layout = ifcopenshell.api.alignment.get_vertical_layout(alignment)
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(file, vertical_layout, vpoints, lengths)
if start_station is not None:
referent_name = f"{name} {ifcopenshell.util.alignment.station_as_string(file, start_station)}"
ifcopenshell.api.alignment.add_stationing_referent(file, referent_name, alignment, 0.0, start_station)
return alignment
@@ -17,13 +17,17 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import csv
from typing import Optional
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.util.alignment
from ifcopenshell import entity_instance
def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
def create_from_csv(
file: ifcopenshell.file, filepath: str, start_station: Optional[float] = None
) -> entity_instance:
"""
Creates an alignment from PI data stored in a CSV file.
@@ -55,6 +59,8 @@ def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
The CSV file contains one horizontal alignment, zero, one, or more vertical alignments
:param filepath: path the to CSV file
:param start_station: if given, the starting station value; a STATION IfcReferent is added at
distance along 0.0 once the geometry exists. If None (the default), no stationing referent is created.
:return: IfcAlignment
"""
alignment = None
@@ -97,4 +103,9 @@ def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
)
assert alignment is not None
if start_station is not None:
referent_name = f"{alignment.Name} {ifcopenshell.util.alignment.station_as_string(file, start_station)}"
ifcopenshell.api.alignment.add_stationing_referent(file, referent_name, alignment, 0.0, start_station)
return alignment
@@ -22,16 +22,7 @@ import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.util.element
from ifcopenshell import entity_instance
def _distance_along_of_referent(referent: entity_instance) -> float:
placement = referent.ObjectPlacement
if placement.is_a("IfcLinearPlacement"):
return placement.RelativePlacement.Location.DistanceAlong.wrappedValue
# IfcLocalPlacement fallback (e.g. semantic-only alignment, or the placement could not yet
# be expressed relative to a basis curve) carries no DistanceAlong; it is only ever used for
# the starting referent, at distance 0.0.
return 0.0
from ifcopenshell.api.alignment._referent_distance_along import _referent_distance_along
def distance_along_from_station(file: ifcopenshell.file, alignment: entity_instance, station: float) -> Optional[float]:
@@ -41,18 +32,24 @@ def distance_along_from_station(file: ifcopenshell.file, alignment: entity_insta
If the alignment does not have stationing defined with an IfcReferent, the start of the alignment is assumed
to be at station 0.0. That is, the station is the distance along.
Station equations (where Pset_Stationing.IncomingStation is set on a referent) are taken into account.
Station equations (where Pset_Stationing.IncomingStation is set on a referent) and reverse
(decreasing) stationing (where Pset_Stationing.HasIncreasingStation is False) are taken into account.
For each STATION referent nested to the alignment, DistanceAlong (D) and the outgoing station (S, i.e.
Pset_Stationing.Station) are read off, sorted by DistanceAlong. The requested station is located within
the segment defined by the last referent whose outgoing station is less than or equal to it, and the
distance along is computed as D + (station - S) for that referent.
Pset_Stationing.Station) are read off and sorted by DistanceAlong. A direction sign is tracked while
walking the sorted referents: it starts at +1 and is set to +1 or -1 at any referent that carries an
explicit Pset_Stationing.HasIncreasingStation, so each referent's region has a sign sigma of +1
(increasing) or -1 (decreasing). HasIncreasingStation may flip any number of times along the alignment -
unrealistic, but valid IFC, and handled. The governing referent is the last one, by DistanceAlong, for
which sigma * (station - S) >= 0, and the distance along is D + sigma * (station - S).
If the station falls within a gap introduced by a forward (gap) station equation - that is, it was skipped
over by the equation - there is no distance along that corresponds to it, and None is returned.
If the station falls within a gap introduced by a station equation - that is, it was skipped over by the
equation - there is no distance along that corresponds to it, and None is returned.
Note that an overlap (backward) station equation causes a range of stations to correspond to two distinct
distances along the alignment, one on either side of the equation. This implementation returns the distance
along in the segment following the equation (i.e. the outgoing side).
Note that an overlap station equation - or a HasIncreasingStation direction reversal, which creates an
equivalent overlap zone - causes a range of stations to correspond to two (or more) distinct distances
along the alignment. This implementation returns the most downstream one (largest DistanceAlong), i.e.
the match in the region following the last equation / reversal.
:param alignment: the alignment
:param station: station value
@@ -72,31 +69,42 @@ def distance_along_from_station(file: ifcopenshell.file, alignment: entity_insta
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
return station - start_station
stations = [
referents = [
(
_distance_along_of_referent(referent),
_referent_distance_along(referent),
ifcopenshell.util.element.get_pset(referent, name="Pset_Stationing", prop="Station"),
ifcopenshell.util.element.get_pset(referent, name="Pset_Stationing", prop="HasIncreasingStation"),
)
for referent in stationing_nest.RelatedObjects
]
stations.sort(key=lambda entry: entry[0])
referents.sort(key=lambda entry: entry[0])
# Assign each referent's region a direction sign: +1 increasing, -1 decreasing. The sign
# starts increasing and flips at any referent carrying an explicit HasIncreasingStation.
sigma = 1.0
stations = []
for distance_along, outgoing_station, has_increasing_station in referents:
if has_increasing_station is not None:
sigma = 1.0 if has_increasing_station else -1.0
stations.append((distance_along, outgoing_station, sigma))
index = None
for i, (distance_along, outgoing_station) in enumerate(stations):
if outgoing_station <= station:
for i, (distance_along, outgoing_station, region_sign) in enumerate(stations):
if region_sign * (station - outgoing_station) >= 0.0:
index = i
if index is None:
# station precedes the alignment's starting station; extrapolate from the first referent
distance_along, outgoing_station = stations[0]
return distance_along + (station - outgoing_station)
distance_along, outgoing_station, region_sign = stations[0]
return distance_along + region_sign * (station - outgoing_station)
distance_along, outgoing_station = stations[index]
distance_along, outgoing_station, region_sign = stations[index]
advance = region_sign * (station - outgoing_station)
if index + 1 < len(stations):
next_distance_along, _ = stations[index + 1]
if station - outgoing_station > next_distance_along - distance_along:
# the station was skipped over by a forward (gap) station equation
next_distance_along, _, _ = stations[index + 1]
if advance > next_distance_along - distance_along:
# the station was skipped over by a gap station equation
return None
return distance_along + (station - outgoing_station)
return distance_along + advance
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
def test_add_positioning_referent():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -37,7 +37,7 @@ def test_add_positioning_referent():
parent=geometric_representation_context,
)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", start_station=2000.0)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment")
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
segment = ifcopenshell.api.alignment.get_layout_segments(horizontal_layout)[0]
@@ -61,7 +61,7 @@ def test_add_positioning_referent():
def test_add_positioning_referent_creates_separate_referent_per_call():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -74,7 +74,7 @@ def test_add_positioning_referent_creates_separate_referent_per_call():
parent=geometric_representation_context,
)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", start_station=2000.0)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment")
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
segment = ifcopenshell.api.alignment.get_layout_segments(horizontal_layout)[0]
@@ -25,7 +25,7 @@ import ifcopenshell.api.unit
from ifcopenshell.api.alignment._add_segment_to_layout import _add_segment_to_layout
try:
ifcopenshell.file(schema="IFC4X3")
ifcopenshell.file(schema="IFC4X3_ADD2")
IFC4X3_AVAILABLE = True
except RuntimeError:
IFC4X3_AVAILABLE = False
@@ -33,7 +33,7 @@ except RuntimeError:
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
def test_add_segment_to_layout():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -47,11 +47,12 @@ def test_add_segment_to_layout():
)
alignment = ifcopenshell.api.alignment.create(file, "")
ifcopenshell.api.alignment.add_stationing_referent(file, "0+00.00", alignment, distance_along=0.0, station=0.0)
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
assert (
len(stationing_nest.RelatedObjects) == 1
) # the alignment creates the stationing nest and it has one referent to defined the stationing for the alignment
) # the stationing nest has one referent that defines the stationing for the alignment
horizontal_alignment = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
def _create_test_file():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -43,7 +43,7 @@ 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)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", include_vertical=True)
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)
@@ -110,6 +110,44 @@ def test_add_stationing_referent_on_basis_curve_false():
assert basis_curve != ifcopenshell.api.alignment.get_basis_curve(alignment)
def test_add_stationing_referent_without_geometry_placed_at_global_origin():
# An alignment with no resolvable basis curve gets an IfcLocalPlacement at (0, 0),
# not an IfcLinearPlacement, and does not require the alignment to have an ObjectPlacement.
file = _create_test_file()
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", include_geometry=False)
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, "1+00.000", alignment, distance_along=0.0, station=100.0
)
_assert_common_referent_asserts(referent, "1+00.000", 100.0)
assert referent.ObjectPlacement.is_a("IfcLocalPlacement")
assert referent.ObjectPlacement.RelativePlacement.Location.Coordinates == (0.0, 0.0)
def test_add_stationing_referent_has_increasing_station():
file = _create_test_file()
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", include_geometry=False)
# default: HasIncreasingStation is not written
default_referent = ifcopenshell.api.alignment.add_stationing_referent(
file, "1+00.000", alignment, distance_along=0.0, station=100.0
)
assert (
ifcopenshell.util.element.get_pset(default_referent, name="Pset_Stationing", prop="HasIncreasingStation") is None
)
# explicit False (reverse stationing) is written
reverse_referent = ifcopenshell.api.alignment.add_stationing_referent(
file, "R", alignment, distance_along=0.0, station=100.0, has_increasing_station=False
)
assert (
ifcopenshell.util.element.get_pset(reverse_referent, name="Pset_Stationing", prop="HasIncreasingStation") is False
)
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()
test_add_stationing_referent_without_geometry_placed_at_global_origin()
test_add_stationing_referent_has_increasing_station()
@@ -25,7 +25,7 @@ import ifcopenshell.api.unit
import ifcopenshell.util.element
try:
ifcopenshell.file(schema="IFC4X3")
ifcopenshell.file(schema="IFC4X3_ADD2")
IFC4X3_AVAILABLE = True
except RuntimeError:
IFC4X3_AVAILABLE = False
@@ -33,7 +33,7 @@ except RuntimeError:
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
def test_add_stationing_to_alignment():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -46,7 +46,10 @@ def test_add_stationing_to_alignment():
parent=geometric_representation_context,
)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", start_station=2000.0)
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment")
ifcopenshell.api.alignment.add_stationing_referent(
file, "TestAlignment 2+000.000", alignment, distance_along=0.0, station=2000.0
)
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
referent = stationing_nest.RelatedObjects[0]
@@ -24,7 +24,7 @@ import ifcopenshell.api.context
import ifcopenshell.api.unit
try:
ifcopenshell.file(schema="IFC4X3")
ifcopenshell.file(schema="IFC4X3_ADD2")
IFC4X3_AVAILABLE = True
except RuntimeError:
IFC4X3_AVAILABLE = False
@@ -32,13 +32,14 @@ except RuntimeError:
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
def test_add_vertical_alignment():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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")
alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False)
ifcopenshell.api.alignment.add_stationing_referent(file, "0+00.00", alignment, distance_along=0.0, station=0.0)
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
assert len(alignment.IsNestedBy) == 2 # nests for layout and referents
@@ -22,40 +22,6 @@ import pytest
import ifcopenshell.api.alignment
import ifcopenshell.api.context
import ifcopenshell.api.unit
import ifcopenshell.util.element
def test_create_stationing_referent_name_includes_alignment_name():
"""create() creates an initial stationing IfcReferent from start_station
(see add_stationing_referent()). Its Name must include the alignment's
own name, the same "<alignment name> <station>" convention
update_key_point_referents() uses for its own referents -- otherwise
this referent is indistinguishable by name alone from the same-named
referent of any OTHER alignment in the same file, unlike every other
referent in the model."""
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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")
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=4900.0)
referents = [
r
for r in ifcopenshell.util.element.get_components(alignment)
if r.is_a("IfcReferent")
and ifcopenshell.util.element.get_pset(r, name="Pset_Stationing", prop="Station") == 4900.0
]
assert len(referents) == 1
assert referents[0].Name == "TestAlignment 49+00.00"
try:
@@ -67,7 +33,7 @@ except RuntimeError:
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
def test_create():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -88,6 +54,12 @@ def test_create():
ali = ifcopenshell.api.alignment.create(file, "A1", include_vertical[i], include_cant[i])
assert ali != None
# create() does not define stationing - the alignment has no referent nest at all
assert ifcopenshell.api.alignment.get_stationing_nest(file, ali) is None
assert not any(
related.is_a("IfcReferent") for nest in ali.IsNestedBy for related in nest.RelatedObjects
)
# verify the geometric representation was created
curve = ifcopenshell.api.alignment.get_curve(ali)
assert curve.is_a() == expected_curve_type[i]
@@ -24,7 +24,7 @@ import ifcopenshell.api.context
import ifcopenshell.api.unit
try:
ifcopenshell.file(schema="IFC4X3")
ifcopenshell.file(schema="IFC4X3_ADD2")
IFC4X3_AVAILABLE = True
except RuntimeError:
IFC4X3_AVAILABLE = False
@@ -32,7 +32,7 @@ except RuntimeError:
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
def test_create_by_pi_method():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -51,7 +51,7 @@ def test_create_by_pi_method():
lengths = [(1600.0), (1200.0), (2000.0), (800.0)]
alignment = ifcopenshell.api.alignment.create_by_pi_method(
file, "TestAlignment", coordinates, radii, vpoints, lengths
file, "TestAlignment", coordinates, radii, vpoints, lengths, start_station=10000.0
)
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
@@ -57,7 +57,7 @@ def test_create_representation():
(12799.99998062693, 89.99999997234107, 0.9999875002340269, -0.004999937569813611),
]
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
file.header.file_description.description = ["ViewDefinition [Alignment-basedView]"]
project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="FHWA Alignment")
@@ -77,9 +77,15 @@ def test_create_representation():
site = file.createIfcSite(GlobalId=ifcopenshell.guid.new(), Name="Site")
ifcopenshell.api.aggregate.assign_object(file, relating_object=project, products=[site])
alignment = ifcopenshell.api.alignment.create(
file, "E-Line", include_vertical=True, start_station=10000.0, include_geometry=False
alignment = ifcopenshell.api.alignment.create(file, "E-Line", include_vertical=True, include_geometry=False)
# stationing is defined before the geometry exists, so the referent is placed at the global
# origin; create_representation() must restate it onto the basis curve at DistanceAlong 0.0
ifcopenshell.api.alignment.add_stationing_referent(
file, "E-Line 100+00.00", alignment, distance_along=0.0, station=10000.0
)
start_referent = ifcopenshell.api.alignment.get_stationing_nest(file, alignment).RelatedObjects[0]
assert start_referent.ObjectPlacement.is_a("IfcLocalPlacement")
# alignment is referenced into spatial structure of site per CT 4.1.5.1
ifcopenshell.api.spatial.reference_structure(file, products=[alignment], relating_structure=site)
@@ -420,6 +426,10 @@ def test_create_representation():
ifcopenshell.api.alignment.create_representation(file, alignment)
# the starting referent is now placed relative to the basis curve at DistanceAlong 0.0
assert start_referent.ObjectPlacement.is_a("IfcLinearPlacement")
assert start_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue == 0.0
curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
assert curve.is_a("IfcCompositeCurve")
for s in curve.Segments:
@@ -23,7 +23,7 @@ import ifcopenshell.api.context
import ifcopenshell.api.unit
try:
ifcopenshell.file(schema="IFC4X3")
ifcopenshell.file(schema="IFC4X3_ADD2")
IFC4X3_AVAILABLE = True
except RuntimeError:
IFC4X3_AVAILABLE = False
@@ -31,7 +31,7 @@ except RuntimeError:
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
def test_distance_along_from_station():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -64,7 +64,7 @@ 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")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -111,5 +111,159 @@ def test_distance_along_from_station_with_station_equations():
assert distance_along_from_station(file, alignment, 1875.0) == pytest.approx(625.0)
def test_distance_along_from_station_reverse_stationing_with_gap_equation():
# A reverse-stationed alignment: HasIncreasingStation=False on the starting referent, station
# labels decreasing as distance along increases, with one gap equation.
# R1: D 0.0, Station 20+00.00, HasIncreasingStation=False
# R2: D 400.0, Station 15+50.00, IncomingStation 16+00.00 (gap of 50)
# R3: D 800.0, Station 11+50.00
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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")
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)]
alignment = ifcopenshell.api.alignment.create_by_pi_method(file, "TestAlignment", coordinates, radii)
ifcopenshell.api.alignment.add_stationing_referent(
file, "R1", alignment, distance_along=0.0, station=2000.0, has_increasing_station=False
)
ifcopenshell.api.alignment.add_stationing_referent(
file, "R2", alignment, distance_along=400.0, station=1550.0, incoming_station=1600.0
)
ifcopenshell.api.alignment.add_stationing_referent(file, "R3", alignment, distance_along=800.0, station=1150.0)
distance_along_from_station = ifcopenshell.api.alignment.distance_along_from_station
# Sta. 18+00.00 -> governed by R1
assert distance_along_from_station(file, alignment, 1800.0) == pytest.approx(200.0)
# Sta. 13+00.00 -> governed by R2; naive subtraction from the start would overstate by the 50 ft gap
assert distance_along_from_station(file, alignment, 1300.0) == pytest.approx(650.0)
# Sta. 15+75.00 falls inside the range the gap equation at R2 skipped -> no distance along
assert distance_along_from_station(file, alignment, 1575.0) is None
def _long_alignment(file):
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")
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)]
return ifcopenshell.api.alignment.create_by_pi_method(file, "TestAlignment", coordinates, radii)
def test_distance_along_from_station_direction_switch_increasing_then_decreasing():
# A peak: station labels rise to R2 then fall. HasIncreasingStation=False on R2 governs the
# region after it. Data is self-consistent (no equations): each region is 500 units long with
# a 500 station-label swing.
# R1 D 0.0 S 1000.0
# R2 D 500.0 S 1500.0 HasIncreasingStation=False
# R3 D 1000.0 S 1000.0
file = ifcopenshell.file(schema="IFC4X3_ADD2")
alignment = _long_alignment(file)
ifcopenshell.api.alignment.add_stationing_referent(file, "R1", alignment, distance_along=0.0, station=1000.0)
ifcopenshell.api.alignment.add_stationing_referent(
file, "R2", alignment, distance_along=500.0, station=1500.0, has_increasing_station=False
)
ifcopenshell.api.alignment.add_stationing_referent(file, "R3", alignment, distance_along=1000.0, station=1000.0)
dafs = ifcopenshell.api.alignment.distance_along_from_station
# Sta 12+00 appears twice (rising at D 200, falling at D 800); the downstream match is returned
assert dafs(file, alignment, 1200.0) == pytest.approx(800.0)
# the peak label sits at the single point D 500
assert dafs(file, alignment, 1500.0) == pytest.approx(500.0)
# the end label
assert dafs(file, alignment, 1000.0) == pytest.approx(1000.0)
# Sta 16+00 is above the peak - it exists nowhere on the alignment
assert dafs(file, alignment, 1600.0) is None
def test_distance_along_from_station_direction_switch_decreasing_then_increasing():
# A valley: labels fall to R2 then rise. R1 starts the alignment decreasing
# (HasIncreasingStation=False); R2 switches it back to increasing.
# R1 D 0.0 S 2000.0 HasIncreasingStation=False
# R2 D 500.0 S 1500.0 HasIncreasingStation=True
# R3 D 1000.0 S 2000.0
file = ifcopenshell.file(schema="IFC4X3_ADD2")
alignment = _long_alignment(file)
ifcopenshell.api.alignment.add_stationing_referent(
file, "R1", alignment, distance_along=0.0, station=2000.0, has_increasing_station=False
)
ifcopenshell.api.alignment.add_stationing_referent(
file, "R2", alignment, distance_along=500.0, station=1500.0, has_increasing_station=True
)
ifcopenshell.api.alignment.add_stationing_referent(file, "R3", alignment, distance_along=1000.0, station=2000.0)
dafs = ifcopenshell.api.alignment.distance_along_from_station
# Sta 18+00 appears twice (falling at D 200, rising at D 800); the downstream match is returned
assert dafs(file, alignment, 1800.0) == pytest.approx(800.0)
# the valley label sits at the single point D 500
assert dafs(file, alignment, 1500.0) == pytest.approx(500.0)
# Sta 14+00 is below the valley - it exists nowhere on the alignment
assert dafs(file, alignment, 1400.0) is None
def test_distance_along_from_station_multiple_direction_switches():
# Not realistic, but valid IFC: stationing direction flips at every referent.
# R1 D 0.0 S 1000.0 increasing [0, 300] 1000 -> 1300
# R2 D 300.0 S 1300.0 HasIncreasingStation=False decreasing [300, 600] 1300 -> 1000
# R3 D 600.0 S 1000.0 HasIncreasingStation=True increasing [600, 900] 1000 -> 1300
# R4 D 900.0 S 1300.0 HasIncreasingStation=False decreasing [900, 1200] 1300 -> 1000
# R5 D 1200.0 S 1000.0
file = ifcopenshell.file(schema="IFC4X3_ADD2")
alignment = _long_alignment(file)
ifcopenshell.api.alignment.add_stationing_referent(file, "R1", alignment, distance_along=0.0, station=1000.0)
ifcopenshell.api.alignment.add_stationing_referent(
file, "R2", alignment, distance_along=300.0, station=1300.0, has_increasing_station=False
)
ifcopenshell.api.alignment.add_stationing_referent(
file, "R3", alignment, distance_along=600.0, station=1000.0, has_increasing_station=True
)
ifcopenshell.api.alignment.add_stationing_referent(
file, "R4", alignment, distance_along=900.0, station=1300.0, has_increasing_station=False
)
ifcopenshell.api.alignment.add_stationing_referent(file, "R5", alignment, distance_along=1200.0, station=1000.0)
dafs = ifcopenshell.api.alignment.distance_along_from_station
# referents nest in DistanceAlong order regardless of the direction flips
nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
assert [r.Name for r in nest.RelatedObjects] == ["R1", "R2", "R3", "R4", "R5"]
# Sta 11+00 appears in every one of the four regions; the last (most downstream) match wins
assert dafs(file, alignment, 1100.0) == pytest.approx(1100.0)
# the shared min label resolves to the very end of the alignment
assert dafs(file, alignment, 1000.0) == pytest.approx(1200.0)
# Sta 13+50 is above every peak - nowhere on the alignment
assert dafs(file, alignment, 1350.0) is None
# a label exactly at a peak
assert dafs(file, alignment, 1300.0) == pytest.approx(900.0)
test_distance_along_from_station()
test_distance_along_from_station_with_station_equations()
test_distance_along_from_station_reverse_stationing_with_gap_equation()
test_distance_along_from_station_direction_switch_increasing_then_decreasing()
test_distance_along_from_station_direction_switch_decreasing_then_increasing()
test_distance_along_from_station_multiple_direction_switches()
@@ -24,7 +24,7 @@ import ifcopenshell.api.context
import ifcopenshell.api.unit
try:
ifcopenshell.file(schema="IFC4X3")
ifcopenshell.file(schema="IFC4X3_ADD2")
IFC4X3_AVAILABLE = True
except RuntimeError:
IFC4X3_AVAILABLE = False
@@ -35,7 +35,7 @@ except RuntimeError:
# compound curve (no tangent between curves)
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
def test_horizontal_layout_by_pi_method():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -51,7 +51,9 @@ def test_horizontal_layout_by_pi_method():
coordinates = [(838.760, 224.745), (965.926, 258.819), (1226.296, 258.819), (1350.817, 291.415)]
radii = [(1000.0), (1000.0)]
alignment = ifcopenshell.api.alignment.create_by_pi_method(file, "TestAlignment", coordinates, radii)
alignment = ifcopenshell.api.alignment.create_by_pi_method(
file, "TestAlignment", coordinates, radii, start_station=10000.0
)
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
assert len(alignment.IsNestedBy) == 2
@@ -33,7 +33,7 @@ LENGTHS = [1600.0, 1200.0, 2000.0, 800.0]
def _new_file():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -49,7 +49,7 @@ def _new_file():
def _new_file_no_context():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -341,7 +341,8 @@ def test_single_real_segment_produces_only_boundary_labels():
def test_start_station_composes_for_child_alignment():
file = _new_file()
alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False, start_station=100.0)
alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False)
ifcopenshell.api.alignment.add_stationing_referent(file, "A1 1+00.00", alignment, distance_along=0.0, station=100.0)
ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
ifcopenshell.api.alignment.add_vertical_layout(file, alignment) # forces the child-alignment split
@@ -379,7 +380,8 @@ def test_rel_nests_from_ancestor_used_for_naming_and_nesting():
to an ancestor alignment's own rel_nests -- e.g. the same one already holding that
ancestor's horizontal key points -- rather than the child's generic "Child of X" name."""
file = _new_file()
alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False, start_station=100.0)
alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False)
ifcopenshell.api.alignment.add_stationing_referent(file, "A1 1+00.00", alignment, distance_along=0.0, station=100.0)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
horizontal_nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
horizontal_count = len(horizontal_nest.RelatedObjects)
@@ -24,7 +24,7 @@ import ifcopenshell.api.context
import ifcopenshell.api.unit
try:
ifcopenshell.file(schema="IFC4X3")
ifcopenshell.file(schema="IFC4X3_ADD2")
IFC4X3_AVAILABLE = True
except RuntimeError:
IFC4X3_AVAILABLE = False
@@ -35,7 +35,7 @@ except RuntimeError:
# compound vertical curve (no gradient between curves)
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
def test_vertical_layout_by_pi_method():
file = ifcopenshell.file(schema="IFC4X3")
file = ifcopenshell.file(schema="IFC4X3_ADD2")
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])
@@ -61,6 +61,8 @@ def test_vertical_layout_by_pi_method():
ifcopenshell.api.alignment.create_layout_segment(file, hlayout, segment1)
ifcopenshell.api.alignment.add_stationing_referent(file, "0+00.00", alignment, distance_along=0.0, station=0.0)
vpoints = [(0.0, 110.0), (400.0, 100.0), (800.0, 115.0), (1300.0, 125.0), (1800.0, 105.0)]
lengths = [(800.0), (0.0), (1000.0)]
vlayout = ifcopenshell.api.alignment.get_vertical_layout(alignment)