mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 14:23:53 +00:00
Reverts from automatically adding stationing to alignments because of missing initial geometry. Adds support for stationing with decreasing values.
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user