mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
add update_key_point_referents to label key alignment points
This commit is contained in:
@@ -91,6 +91,7 @@ from .layout_vertical_alignment_by_pi_method import (
|
|||||||
from .name_segments import name_segments
|
from .name_segments import name_segments
|
||||||
from .update_end_point import update_end_point
|
from .update_end_point import update_end_point
|
||||||
from .update_fallback_position import update_fallback_position
|
from .update_fallback_position import update_fallback_position
|
||||||
|
from .update_key_point_referents import update_key_point_referents
|
||||||
from .util import *
|
from .util import *
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -133,5 +134,6 @@ __all__ = [
|
|||||||
"register_referent_name_callback",
|
"register_referent_name_callback",
|
||||||
"update_end_point",
|
"update_end_point",
|
||||||
"update_fallback_position",
|
"update_fallback_position",
|
||||||
|
"update_key_point_referents",
|
||||||
"get_mapped_segments",
|
"get_mapped_segments",
|
||||||
]
|
]
|
||||||
|
|||||||
+6
-5
@@ -26,9 +26,10 @@ _cant_callback = None
|
|||||||
|
|
||||||
def register_referent_name_callback(horizontal=None, vertical=None, cant=None):
|
def register_referent_name_callback(horizontal=None, vertical=None, cant=None):
|
||||||
"""
|
"""
|
||||||
Referents are automatically created at the start of each horizontal, vertical, and cant segment.
|
Referents are created at the start of each horizontal, vertical, and cant segment by
|
||||||
The referents represent key points in the alignment layout such as Point of Curvature, Point of Tangent, and others.
|
ifcopenshell.api.alignment.update_key_point_referents. The referents represent key points in the
|
||||||
Different juristicions use different naming systems for these key points.
|
alignment layout such as Point of Curvature, Point of Tangent, and others. Different
|
||||||
|
juristicions use different naming systems for these key points.
|
||||||
|
|
||||||
The referent name callback functions provide a customizable method for naming these referents. If a callback is registered,
|
The referent name callback functions provide a customizable method for naming these referents. If a callback is registered,
|
||||||
it is called when creating the referent name, otherwise the default naming is used.
|
it is called when creating the referent name, otherwise the default naming is used.
|
||||||
@@ -39,8 +40,8 @@ def register_referent_name_callback(horizontal=None, vertical=None, cant=None):
|
|||||||
|
|
||||||
The callback function returns a string that is used in the referent name for the referent at the start of `segment`.
|
The callback function returns a string that is used in the referent name for the referent at the start of `segment`.
|
||||||
The callback must accomodate the following cases:
|
The callback must accomodate the following cases:
|
||||||
* prev_segment = None and segment != None - this indicates the last segment so the "End of Alignment" name is returned
|
* prev_segment = None and segment != None - this indicates the first segment so the "Beginning of Alignment" name is returned
|
||||||
* prev_segment != None and segment == None - this indicates the first segment so the "Beginning of Alignment" name is returned
|
* prev_segment != None and segment == None - this indicates the last segment so the "End of Alignment" name is returned
|
||||||
* prev_segment != None and segment != None - this indicates an intermediate segment so a name representitive of the transition is returned
|
* prev_segment != None and segment != None - this indicates an intermediate segment so a name representitive of the transition is returned
|
||||||
|
|
||||||
Setting any or all of the callbacks to None causes the default naming to be used.
|
Setting any or all of the callbacks to None causes the default naming to be used.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# IfcOpenShell - IFC toolkit and geometry engine
|
||||||
|
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
|
||||||
|
#
|
||||||
|
# This file is part of IfcOpenShell.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from ifcopenshell import entity_instance
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_nest(nest: entity_instance, key: Callable) -> entity_instance:
|
||||||
|
"""Sorts the RelatedObjects of an IfcRelNests in place, by an arbitrary key function."""
|
||||||
|
nest.RelatedObjects = sorted(nest.RelatedObjects, key=key)
|
||||||
|
return nest
|
||||||
@@ -20,6 +20,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api.alignment
|
import ifcopenshell.api.alignment
|
||||||
|
from ifcopenshell.api.alignment._sort_nest import _sort_nest
|
||||||
from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position
|
from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position
|
||||||
import ifcopenshell.api.pset
|
import ifcopenshell.api.pset
|
||||||
import ifcopenshell.guid
|
import ifcopenshell.guid
|
||||||
@@ -122,8 +123,6 @@ def add_stationing_referent(
|
|||||||
else:
|
else:
|
||||||
nest.RelatedObjects += (referent,)
|
nest.RelatedObjects += (referent,)
|
||||||
|
|
||||||
nest.RelatedObjects = sorted(
|
_sort_nest(nest, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station"))
|
||||||
nest.RelatedObjects, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station")
|
|
||||||
)
|
|
||||||
|
|
||||||
return referent
|
return referent
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# IfcOpenShell - IFC toolkit and geometry engine
|
||||||
|
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
|
||||||
|
#
|
||||||
|
# This file is part of IfcOpenShell.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import ifcopenshell
|
||||||
|
import ifcopenshell.api.alignment
|
||||||
|
import ifcopenshell.api.pset
|
||||||
|
import ifcopenshell.guid
|
||||||
|
import ifcopenshell.util.alignment
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
from ifcopenshell import entity_instance
|
||||||
|
from ifcopenshell.api.alignment._get_segment_start_point_label import (
|
||||||
|
_get_segment_start_point_label,
|
||||||
|
)
|
||||||
|
from ifcopenshell.api.alignment._sort_nest import _sort_nest
|
||||||
|
from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position
|
||||||
|
|
||||||
|
|
||||||
|
def _get_key_point_referent_nest(layout: entity_instance) -> Optional[entity_instance]:
|
||||||
|
"""
|
||||||
|
Searches layout.IsNestedBy for the IfcRelNests whose RelatedObjects are IfcReferent.
|
||||||
|
|
||||||
|
This is distinct from both get_stationing_nest (scoped to the parent IfcAlignment, and
|
||||||
|
specifically the STATION/station-equation nest) and get_alignment_segment_nest (the *segment*
|
||||||
|
nest that also lives on layout.IsNestedBy, holding IfcAlignmentSegment, never IfcReferent).
|
||||||
|
"""
|
||||||
|
for nest in layout.IsNestedBy:
|
||||||
|
for related_object in nest.RelatedObjects:
|
||||||
|
if related_object.is_a("IfcReferent"):
|
||||||
|
return nest
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_referent(file: ifcopenshell.file, referent: entity_instance) -> None:
|
||||||
|
"""Cleanly deletes a key-point IfcReferent: its Pset_Stationing, its ObjectPlacement (if
|
||||||
|
exclusively owned by it), and finally the referent itself."""
|
||||||
|
for inverse in list(file.get_inverse(referent)):
|
||||||
|
if inverse.is_a("IfcRelDefinesByProperties"):
|
||||||
|
ifcopenshell.api.pset.remove_pset(file, product=referent, pset=inverse.RelatingPropertyDefinition)
|
||||||
|
|
||||||
|
object_placement = referent.ObjectPlacement
|
||||||
|
if object_placement and file.get_total_inverses(object_placement) == 1:
|
||||||
|
referent.ObjectPlacement = None
|
||||||
|
ifcopenshell.util.element.remove_deep2(file, object_placement)
|
||||||
|
|
||||||
|
file.remove(referent) # also strips referent out of any IfcRelNests.RelatedObjects referencing it
|
||||||
|
|
||||||
|
|
||||||
|
def _create_key_point_referent(
|
||||||
|
file: ifcopenshell.file,
|
||||||
|
alignment: entity_instance,
|
||||||
|
curve: Optional[entity_instance],
|
||||||
|
label: str,
|
||||||
|
distance_along: float,
|
||||||
|
station: float,
|
||||||
|
) -> entity_instance:
|
||||||
|
if curve and curve.is_a("IfcCompositeCurve") and 0 < len(curve.Segments):
|
||||||
|
object_placement = file.createIfcLinearPlacement(
|
||||||
|
RelativePlacement=file.createIfcAxis2PlacementLinear(
|
||||||
|
Location=file.createIfcPointByDistanceExpression(
|
||||||
|
DistanceAlong=file.createIfcLengthMeasure(distance_along),
|
||||||
|
OffsetLateral=None,
|
||||||
|
OffsetVertical=None,
|
||||||
|
OffsetLongitudinal=None,
|
||||||
|
BasisCurve=curve,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
update_fallback_position(file, object_placement)
|
||||||
|
else:
|
||||||
|
object_placement = file.createIfcLocalPlacement(
|
||||||
|
PlacementRelTo=None,
|
||||||
|
RelativePlacement=file.createIfcAxis2Placement2D(
|
||||||
|
Location=file.createIfcCartesianPoint(alignment.ObjectPlacement.RelativePlacement.Location.Coordinates)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = f"{label} ({ifcopenshell.util.alignment.station_as_string(file, station)})"
|
||||||
|
|
||||||
|
referent = file.createIfcReferent(
|
||||||
|
GlobalId=ifcopenshell.guid.new(),
|
||||||
|
OwnerHistory=None,
|
||||||
|
Name=name,
|
||||||
|
Description=None,
|
||||||
|
ObjectType=None,
|
||||||
|
ObjectPlacement=object_placement,
|
||||||
|
Representation=None,
|
||||||
|
PredefinedType="POSITION",
|
||||||
|
)
|
||||||
|
|
||||||
|
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=referent, name="Pset_Stationing")
|
||||||
|
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": station})
|
||||||
|
|
||||||
|
return referent
|
||||||
|
|
||||||
|
|
||||||
|
def update_key_point_referents(
|
||||||
|
file: ifcopenshell.file,
|
||||||
|
layout: entity_instance,
|
||||||
|
rel_nests: Optional[entity_instance] = None,
|
||||||
|
clear: bool = False,
|
||||||
|
) -> entity_instance:
|
||||||
|
"""
|
||||||
|
Creates IfcReferent key-point markers for every segment transition in an alignment layout.
|
||||||
|
|
||||||
|
Labels are derived from _get_segment_start_point_label (e.g. "P.C.", "P.T.", "P.O.B.",
|
||||||
|
"P.V.C.", ...), with the station appended, e.g. "P.C. (145+98.32)". Different jurisdictions use
|
||||||
|
different naming systems for these key points -- register_referent_name_callback() lets a
|
||||||
|
caller override the default horizontal/vertical/cant labeling before calling this function; if
|
||||||
|
a callback is registered, its output is used here instead of the built-in labels. Referents are
|
||||||
|
nested to `rel_nests`, an IfcRelNests distinct from the layout's segment nest (found via
|
||||||
|
get_alignment_segment_nest) and from the alignment's stationing nest (found via
|
||||||
|
get_stationing_nest) -- key-point referents never belong in either of those.
|
||||||
|
|
||||||
|
:param layout: IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
|
||||||
|
:param rel_nests: an existing IfcRelNests to (re)populate. May live anywhere (e.g. the parent
|
||||||
|
IfcAlignment, the layout, or elsewhere) -- the caller decides. If omitted, an existing
|
||||||
|
referent-nest already on `layout` is reused, or a new one is created and related to `layout`.
|
||||||
|
:param clear: if True, deletes all IfcReferent currently in rel_nests.RelatedObjects (and their
|
||||||
|
Pset_Stationing) before regenerating. If False (default), new referents are appended to
|
||||||
|
whatever already exists -- no deduplication.
|
||||||
|
:return: the IfcRelNests, with RelatedObjects sorted ascending by Pset_Stationing.Station
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(model, horizontal)
|
||||||
|
|
||||||
|
Example, with custom labels for a jurisdiction that doesn't use the built-in abbreviations:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
def my_horizontal_labels(prev_segment, segment):
|
||||||
|
if prev_segment is None:
|
||||||
|
return "Start"
|
||||||
|
if segment is None:
|
||||||
|
return "End"
|
||||||
|
return "Curve Point" # a name representative of the prev_segment -> segment transition
|
||||||
|
|
||||||
|
ifcopenshell.api.alignment.register_referent_name_callback(horizontal=my_horizontal_labels)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(model, horizontal)
|
||||||
|
# nest.RelatedObjects[0].Name starts with "Start (" instead of the default "P.O.B. ("
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
|
||||||
|
if not layout.is_a() in expected_types:
|
||||||
|
raise TypeError(
|
||||||
|
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if rel_nests is None:
|
||||||
|
rel_nests = _get_key_point_referent_nest(layout)
|
||||||
|
if rel_nests is None:
|
||||||
|
rel_nests = file.createIfcRelNests(
|
||||||
|
GlobalId=ifcopenshell.guid.new(), RelatingObject=layout, RelatedObjects=()
|
||||||
|
)
|
||||||
|
|
||||||
|
if clear:
|
||||||
|
for referent in list(rel_nests.RelatedObjects):
|
||||||
|
_remove_referent(file, referent)
|
||||||
|
rel_nests.RelatedObjects = ()
|
||||||
|
|
||||||
|
segments = list(ifcopenshell.api.alignment.get_layout_segments(layout))
|
||||||
|
if segments and ifcopenshell.api.alignment.has_zero_length_segment(layout):
|
||||||
|
segments = segments[:-1]
|
||||||
|
|
||||||
|
if not segments:
|
||||||
|
_sort_nest(
|
||||||
|
rel_nests, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station")
|
||||||
|
)
|
||||||
|
return rel_nests
|
||||||
|
|
||||||
|
alignment = ifcopenshell.api.alignment.get_alignment(layout)
|
||||||
|
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
|
||||||
|
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
|
||||||
|
is_horizontal = layout.is_a("IfcAlignmentHorizontal")
|
||||||
|
|
||||||
|
new_referents = []
|
||||||
|
distance_along = 0.0
|
||||||
|
prev_segment = None
|
||||||
|
for segment in segments:
|
||||||
|
dp = segment.DesignParameters
|
||||||
|
seg_distance_along = distance_along if is_horizontal else dp.StartDistAlong
|
||||||
|
|
||||||
|
label = _get_segment_start_point_label(prev_segment, segment)
|
||||||
|
station = start_station + seg_distance_along
|
||||||
|
new_referents.append(_create_key_point_referent(file, alignment, curve, label, seg_distance_along, station))
|
||||||
|
|
||||||
|
if is_horizontal:
|
||||||
|
distance_along += dp.SegmentLength
|
||||||
|
else:
|
||||||
|
distance_along = dp.StartDistAlong + dp.HorizontalLength
|
||||||
|
|
||||||
|
prev_segment = segment
|
||||||
|
|
||||||
|
label = _get_segment_start_point_label(prev_segment, None)
|
||||||
|
station = start_station + distance_along
|
||||||
|
new_referents.append(_create_key_point_referent(file, alignment, curve, label, distance_along, station))
|
||||||
|
|
||||||
|
rel_nests.RelatedObjects = tuple(rel_nests.RelatedObjects) + tuple(new_referents)
|
||||||
|
_sort_nest(rel_nests, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station"))
|
||||||
|
|
||||||
|
return rel_nests
|
||||||
@@ -97,16 +97,32 @@ def callback_alignment():
|
|||||||
|
|
||||||
|
|
||||||
def test_with_default_names(default_names_alignment):
|
def test_with_default_names(default_names_alignment):
|
||||||
referent_nest = ifcopenshell.api.alignment.get_referent_nest(None, default_names_alignment)
|
file = default_names_alignment.file
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(default_names_alignment)
|
||||||
|
vertical = ifcopenshell.api.alignment.get_vertical_layout(default_names_alignment)
|
||||||
|
|
||||||
expected = ["P.O.B", "P.C.", "P.T.", "P.O.E.", "V.P.O.B.", "P.V.C.", "P.V.T.", "V.P.O.E"]
|
h_nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
for r in referent_nest.RelatedObjects:
|
v_nest = ifcopenshell.api.alignment.update_key_point_referents(file, vertical)
|
||||||
assert [x in r.Name for x in expected]
|
|
||||||
|
expected_h = ["P.O.B.", "P.C.", "P.T.", "P.C.", "P.T.", "P.C.", "P.T.", "P.O.E."]
|
||||||
|
expected_v = ["V.P.O.B.", "P.V.C.", "P.V.T.", "P.V.C.", "P.V.T.", "P.V.C.", "P.V.T.", "P.V.C.", "P.V.T.", "V.P.O.E."]
|
||||||
|
|
||||||
|
assert [r.Name.split(" (")[0] for r in h_nest.RelatedObjects] == expected_h
|
||||||
|
assert [r.Name.split(" (")[0] for r in v_nest.RelatedObjects] == expected_v
|
||||||
|
|
||||||
|
|
||||||
def test_with_callbacks(callback_alignment):
|
def test_with_callbacks(callback_alignment):
|
||||||
referent_nest = ifcopenshell.api.alignment.get_referent_nest(None, callback_alignment)
|
file = callback_alignment.file
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(callback_alignment)
|
||||||
|
vertical = ifcopenshell.api.alignment.get_vertical_layout(callback_alignment)
|
||||||
|
|
||||||
expected = ["A", "Q", "Z", "a", "q", "z"]
|
h_nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
for r in referent_nest.RelatedObjects:
|
v_nest = ifcopenshell.api.alignment.update_key_point_referents(file, vertical)
|
||||||
assert [x in r.Name for x in expected]
|
|
||||||
|
expected_h = ["A", "Q", "Q", "Q", "Q", "Q", "Q", "Z"]
|
||||||
|
expected_v = ["a", "q", "q", "q", "q", "q", "q", "q", "q", "z"]
|
||||||
|
|
||||||
|
assert [r.Name.split(" (")[0] for r in h_nest.RelatedObjects] == expected_h
|
||||||
|
assert [r.Name.split(" (")[0] for r in v_nest.RelatedObjects] == expected_v
|
||||||
|
|
||||||
|
ifcopenshell.api.alignment.register_referent_name_callback(None, None, None) # reset global state
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
# 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 collections import Counter
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import ifcopenshell.api.alignment
|
||||||
|
import ifcopenshell.api.context
|
||||||
|
import ifcopenshell.api.unit
|
||||||
|
import ifcopenshell.util.alignment
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
|
||||||
|
COORDINATES = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)]
|
||||||
|
RADII = [1000.0, 1250.0, 950.0]
|
||||||
|
VPOINTS = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)]
|
||||||
|
LENGTHS = [1600.0, 1200.0, 2000.0, 800.0]
|
||||||
|
|
||||||
|
|
||||||
|
def _new_file():
|
||||||
|
file = ifcopenshell.file(schema="IFC4X3")
|
||||||
|
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")
|
||||||
|
ifcopenshell.api.context.add_context(
|
||||||
|
file,
|
||||||
|
context_type="Model",
|
||||||
|
context_identifier="Axis",
|
||||||
|
target_view="MODEL_VIEW",
|
||||||
|
parent=geometric_representation_context,
|
||||||
|
)
|
||||||
|
return file
|
||||||
|
|
||||||
|
|
||||||
|
def _new_file_no_context():
|
||||||
|
file = ifcopenshell.file(schema="IFC4X3")
|
||||||
|
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])
|
||||||
|
return file
|
||||||
|
|
||||||
|
|
||||||
|
def _build_alignment(file, start_station=0.0):
|
||||||
|
return ifcopenshell.api.alignment.create_by_pi_method(
|
||||||
|
file, "TestAlignment", COORDINATES, RADII, VPOINTS, LENGTHS, start_station
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _pset_station(referent):
|
||||||
|
return ifcopenshell.util.element.get_pset(referent, name="Pset_Stationing", prop="Station")
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrong_layout_type_raises_type_error():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
ifcopenshell.api.alignment.update_key_point_referents(file, alignment)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_rel_nests_created_when_none_provided():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
|
||||||
|
assert nest.is_a("IfcRelNests")
|
||||||
|
assert nest.RelatingObject == horizontal
|
||||||
|
assert nest.id() != segment_nest.id()
|
||||||
|
assert len(nest.RelatedObjects) == 8
|
||||||
|
assert all(r.is_a("IfcReferent") for r in nest.RelatedObjects)
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_call_without_rel_nests_reuses_existing_nest():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
segment_count_before = len(ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal).RelatedObjects)
|
||||||
|
|
||||||
|
nest1 = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
nest2 = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
|
||||||
|
assert nest1.id() == nest2.id()
|
||||||
|
assert len(nest2.RelatedObjects) == 16
|
||||||
|
segment_count_after = len(ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal).RelatedObjects)
|
||||||
|
assert segment_count_after == segment_count_before
|
||||||
|
|
||||||
|
|
||||||
|
def test_provided_rel_nests_is_used_as_is():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
|
||||||
|
# the nest may live anywhere the caller chooses, e.g. hung off the parent IfcAlignment
|
||||||
|
rel_nests = file.createIfcRelNests(GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=())
|
||||||
|
|
||||||
|
result = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal, rel_nests=rel_nests)
|
||||||
|
|
||||||
|
assert result.id() == rel_nests.id()
|
||||||
|
assert result.RelatingObject == alignment
|
||||||
|
assert len(result.RelatedObjects) == 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_true_removes_old_referents_and_psets():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
old_referent_ids = [r.id() for r in nest.RelatedObjects]
|
||||||
|
old_pset_ids = [r.IsDefinedBy[0].RelatingPropertyDefinition.id() for r in nest.RelatedObjects]
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal, rel_nests=nest, clear=True)
|
||||||
|
|
||||||
|
assert len(nest.RelatedObjects) == 8
|
||||||
|
for old_id in old_referent_ids + old_pset_ids:
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
file.by_id(old_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_false_appends_without_dedup():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
ifcopenshell.api.alignment.update_key_point_referents(file, horizontal, rel_nests=nest, clear=False)
|
||||||
|
|
||||||
|
assert len(nest.RelatedObjects) == 16
|
||||||
|
counts = Counter(r.Name for r in nest.RelatedObjects)
|
||||||
|
assert len(counts) == 8
|
||||||
|
assert all(count == 2 for count in counts.values())
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_horizontal_labels_and_order():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
|
||||||
|
expected = ["P.O.B.", "P.C.", "P.T.", "P.C.", "P.T.", "P.C.", "P.T.", "P.O.E."]
|
||||||
|
assert [r.Name.split(" (")[0] for r in nest.RelatedObjects] == expected
|
||||||
|
|
||||||
|
stations = [_pset_station(r) for r in nest.RelatedObjects]
|
||||||
|
assert stations == sorted(stations)
|
||||||
|
assert stations[0] == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_vertical_labels_and_order():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
vertical = ifcopenshell.api.alignment.get_vertical_layout(alignment)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, vertical)
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
"V.P.O.B.",
|
||||||
|
"P.V.C.",
|
||||||
|
"P.V.T.",
|
||||||
|
"P.V.C.",
|
||||||
|
"P.V.T.",
|
||||||
|
"P.V.C.",
|
||||||
|
"P.V.T.",
|
||||||
|
"P.V.C.",
|
||||||
|
"P.V.T.",
|
||||||
|
"V.P.O.E.",
|
||||||
|
]
|
||||||
|
assert [r.Name.split(" (")[0] for r in nest.RelatedObjects] == expected
|
||||||
|
|
||||||
|
segments = ifcopenshell.api.alignment.get_layout_segments(vertical)
|
||||||
|
real_segments = segments[:-1] if ifcopenshell.api.alignment.has_zero_length_segment(vertical) else segments
|
||||||
|
# spot check the interior referents' stations against the segments' StartDistAlong directly
|
||||||
|
for referent, segment in zip(nest.RelatedObjects[1:-1], real_segments[1:]):
|
||||||
|
assert _pset_station(referent) == pytest.approx(segment.DesignParameters.StartDistAlong)
|
||||||
|
|
||||||
|
|
||||||
|
def test_name_format():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
referent = nest.RelatedObjects[0]
|
||||||
|
station = _pset_station(referent)
|
||||||
|
assert referent.Name == f"P.O.B. ({ifcopenshell.util.alignment.station_as_string(file, station)})"
|
||||||
|
|
||||||
|
|
||||||
|
def test_geometric_placement_when_layout_has_representation():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
curve = ifcopenshell.api.alignment.get_layout_curve(horizontal)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
|
||||||
|
for referent in nest.RelatedObjects:
|
||||||
|
assert referent.ObjectPlacement.is_a("IfcLinearPlacement")
|
||||||
|
location = referent.ObjectPlacement.RelativePlacement.Location
|
||||||
|
assert location.is_a("IfcPointByDistanceExpression")
|
||||||
|
assert location.BasisCurve == curve
|
||||||
|
assert referent.ObjectPlacement.CartesianPosition is not None
|
||||||
|
|
||||||
|
first, last = nest.RelatedObjects[0], nest.RelatedObjects[-1]
|
||||||
|
assert first.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue == pytest.approx(0.0)
|
||||||
|
assert last.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue == pytest.approx(
|
||||||
|
_pset_station(last)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fallback_placement_when_layout_has_no_geometry():
|
||||||
|
file = _new_file_no_context()
|
||||||
|
alignment = ifcopenshell.api.alignment.create(file, "A1", include_geometry=False)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
ifcopenshell.api.alignment.layout_horizontal_alignment_by_pi_method(file, horizontal, COORDINATES, RADII)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
|
||||||
|
expected_coordinates = alignment.ObjectPlacement.RelativePlacement.Location.Coordinates
|
||||||
|
for referent in nest.RelatedObjects:
|
||||||
|
assert referent.ObjectPlacement.is_a("IfcLocalPlacement")
|
||||||
|
assert referent.ObjectPlacement.RelativePlacement.Location.Coordinates == expected_coordinates
|
||||||
|
|
||||||
|
|
||||||
|
def test_cant_layout_boundary_labels():
|
||||||
|
file = _new_file_no_context()
|
||||||
|
alignment = ifcopenshell.api.alignment.create(file, "A1", include_cant=True, include_geometry=False)
|
||||||
|
cant = ifcopenshell.api.alignment.get_cant_layout(alignment)
|
||||||
|
|
||||||
|
dp1 = file.createIfcAlignmentCantSegment(
|
||||||
|
StartDistAlong=0.0,
|
||||||
|
HorizontalLength=100.0,
|
||||||
|
StartCantLeft=0.0,
|
||||||
|
EndCantLeft=0.0,
|
||||||
|
StartCantRight=0.0,
|
||||||
|
EndCantRight=0.0,
|
||||||
|
PredefinedType="CONSTANTCANT",
|
||||||
|
)
|
||||||
|
ifcopenshell.api.alignment.create_layout_segment(file, cant, dp1)
|
||||||
|
|
||||||
|
dp2 = file.createIfcAlignmentCantSegment(
|
||||||
|
StartDistAlong=100.0,
|
||||||
|
HorizontalLength=50.0,
|
||||||
|
StartCantLeft=0.0,
|
||||||
|
EndCantLeft=0.0,
|
||||||
|
StartCantRight=0.0,
|
||||||
|
EndCantRight=0.0,
|
||||||
|
PredefinedType="CONSTANTCANT",
|
||||||
|
)
|
||||||
|
ifcopenshell.api.alignment.create_layout_segment(file, cant, dp2)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, cant)
|
||||||
|
|
||||||
|
labels = [r.Name.split(" (")[0] for r in nest.RelatedObjects]
|
||||||
|
assert labels[0] == "C.P.O.B."
|
||||||
|
assert labels[-1] == "C.P.O.E."
|
||||||
|
# CONSTANTCANT -> CONSTANTCANT is currently an unfilled "xx" placeholder in the cant lookup
|
||||||
|
# table (_get_segment_start_point_label.py) -- out of scope to fill in here.
|
||||||
|
assert labels[1] == "xx"
|
||||||
|
|
||||||
|
stations = [_pset_station(r) for r in nest.RelatedObjects]
|
||||||
|
assert stations == [0.0, 100.0, 150.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_real_segments_produces_no_referents():
|
||||||
|
file = _new_file_no_context()
|
||||||
|
alignment = ifcopenshell.api.alignment.create(file, "A1", include_geometry=False)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
assert nest.RelatedObjects == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_real_segment_produces_only_boundary_labels():
|
||||||
|
file = _new_file_no_context()
|
||||||
|
alignment = ifcopenshell.api.alignment.create(file, "A1", include_geometry=False)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
|
||||||
|
design_parameters = file.createIfcAlignmentHorizontalSegment(
|
||||||
|
StartTag=None,
|
||||||
|
EndTag=None,
|
||||||
|
StartPoint=file.createIfcCartesianPoint((0.0, 0.0)),
|
||||||
|
StartDirection=0.0,
|
||||||
|
StartRadiusOfCurvature=0.0,
|
||||||
|
EndRadiusOfCurvature=0.0,
|
||||||
|
SegmentLength=100.0,
|
||||||
|
GravityCenterLineHeight=None,
|
||||||
|
PredefinedType="LINE",
|
||||||
|
)
|
||||||
|
ifcopenshell.api.alignment.create_layout_segment(file, horizontal, design_parameters)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
labels = [r.Name.split(" (")[0] for r in nest.RelatedObjects]
|
||||||
|
assert labels == ["P.O.B.", "P.O.E."]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
|
||||||
|
ifcopenshell.api.alignment.add_vertical_layout(file, alignment) # forces the child-alignment split
|
||||||
|
|
||||||
|
child_alignment = alignment.IsDecomposedBy[0].RelatedObjects[-1]
|
||||||
|
child_vertical = ifcopenshell.api.alignment.get_vertical_layout(child_alignment)
|
||||||
|
|
||||||
|
dp1 = file.createIfcAlignmentVerticalSegment(
|
||||||
|
StartDistAlong=0.0,
|
||||||
|
HorizontalLength=500.0,
|
||||||
|
StartHeight=10.0,
|
||||||
|
StartGradient=0.01,
|
||||||
|
EndGradient=0.01,
|
||||||
|
PredefinedType="CONSTANTGRADIENT",
|
||||||
|
)
|
||||||
|
ifcopenshell.api.alignment.create_layout_segment(file, child_vertical, dp1)
|
||||||
|
|
||||||
|
dp2 = file.createIfcAlignmentVerticalSegment(
|
||||||
|
StartDistAlong=500.0,
|
||||||
|
HorizontalLength=300.0,
|
||||||
|
StartHeight=15.0,
|
||||||
|
StartGradient=0.01,
|
||||||
|
EndGradient=0.01,
|
||||||
|
PredefinedType="CONSTANTGRADIENT",
|
||||||
|
)
|
||||||
|
ifcopenshell.api.alignment.create_layout_segment(file, child_vertical, dp2)
|
||||||
|
|
||||||
|
nest = ifcopenshell.api.alignment.update_key_point_referents(file, child_vertical)
|
||||||
|
stations = [_pset_station(r) for r in nest.RelatedObjects]
|
||||||
|
assert stations == pytest.approx([100.0, 600.0, 900.0])
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_ifc_rel_nests():
|
||||||
|
file = _new_file()
|
||||||
|
alignment = _build_alignment(file)
|
||||||
|
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
|
||||||
|
result = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||||
|
assert result.is_a("IfcRelNests")
|
||||||
|
|
||||||
|
|
||||||
|
test_wrong_layout_type_raises_type_error()
|
||||||
|
test_default_rel_nests_created_when_none_provided()
|
||||||
|
test_second_call_without_rel_nests_reuses_existing_nest()
|
||||||
|
test_provided_rel_nests_is_used_as_is()
|
||||||
|
test_clear_true_removes_old_referents_and_psets()
|
||||||
|
test_clear_false_appends_without_dedup()
|
||||||
|
test_default_horizontal_labels_and_order()
|
||||||
|
test_default_vertical_labels_and_order()
|
||||||
|
test_name_format()
|
||||||
|
test_geometric_placement_when_layout_has_representation()
|
||||||
|
test_fallback_placement_when_layout_has_no_geometry()
|
||||||
|
test_cant_layout_boundary_labels()
|
||||||
|
test_no_real_segments_produces_no_referents()
|
||||||
|
test_single_real_segment_produces_only_boundary_labels()
|
||||||
|
test_start_station_composes_for_child_alignment()
|
||||||
|
test_returns_ifc_rel_nests()
|
||||||
Reference in New Issue
Block a user