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

This commit is contained in:
Thomas Krijnen
2026-08-10 03:31:02 +02:00
36 changed files with 1543 additions and 147 deletions
@@ -48,9 +48,9 @@ def test_add_segment_to_layout():
alignment = ifcopenshell.api.alignment.create(file, "")
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
assert (
len(referent_nest.RelatedObjects) == 1
len(stationing_nest.RelatedObjects) == 1
) # the alignment creates the stationing nest and it has one referent to defined the stationing for the alignment
horizontal_alignment = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
@@ -84,5 +84,8 @@ def test_add_segment_to_layout():
assert len(horizontal_alignment.IsNestedBy) == 1
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal_alignment)
assert len(segment_nest.RelatedObjects) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 1 # test this a second time to make sure that it is still true
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
assert len(stationing_nest.RelatedObjects) == 1 # test this a second time to make sure that it is still true
test_add_segment_to_layout()
@@ -48,8 +48,8 @@ def test_add_stationing_to_alignment():
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", start_station=2000.0)
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
referent = referent_nest.RelatedObjects[0]
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
referent = stationing_nest.RelatedObjects[0]
assert referent.PredefinedType == "STATION"
assert referent.Name == "2+000.000"
@@ -63,10 +63,10 @@ def test_add_stationing_to_alignment():
file, "4+000.000", alignment, distance_along=1000.0, station=4000.0, incoming_station=3000.0
)
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 2
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
assert len(stationing_nest.RelatedObjects) == 2
assert second_referent == referent_nest.RelatedObjects[1]
assert second_referent == stationing_nest.RelatedObjects[1]
assert second_referent.PredefinedType == "STATION"
assert second_referent.Name == "4+000.000"
@@ -45,11 +45,11 @@ def test_add_vertical_alignment():
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
assert len(layout_nest.RelatedObjects) == 1
assert layout_nest.RelatedObjects[0].is_a("IfcAlignmentHorizontal")
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
assert (
len(referent_nest.RelatedObjects) == 1
len(stationing_nest.RelatedObjects) == 1
) # the alignment creates the stationing nest and it has one referent to defined the stationing for the alignment
assert referent_nest.RelatedObjects[0].is_a("IfcReferent")
assert stationing_nest.RelatedObjects[0].is_a("IfcReferent")
curve = ifcopenshell.api.alignment.get_curve(alignment)
assert curve.is_a("IfcCompositeCurve")
@@ -60,8 +60,8 @@ def test_create_by_pi_method():
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
assert len(layout_nest.RelatedObjects) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 1
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
assert len(stationing_nest.RelatedObjects) == 1
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
horizontal_segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal_layout)
@@ -55,9 +55,9 @@ def test_horizontal_layout_by_pi_method():
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
assert len(alignment.IsNestedBy) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
assert referent_nest.RelatedObjects[0].is_a("IfcReferent")
assert stationing_nest.RelatedObjects[0].is_a("IfcReferent")
assert layout_nest.RelatedObjects[0].is_a("IfcAlignmentHorizontal")
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout_nest.RelatedObjects[0])
assert len(segment_nest.RelatedObjects) == 3 # segments in horizontal layout
@@ -102,19 +102,50 @@ def callback_alignment():
yield alignment
def _label(name):
return name.rsplit("(", 1)[1].rstrip(")")
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
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"]
for r in referent_nest.RelatedObjects:
assert [x in r.Name for x in expected]
h_nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
v_nest = ifcopenshell.api.alignment.update_key_point_referents(file, vertical)
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 [_label(r.Name) for r in h_nest.RelatedObjects] == expected_h
assert [_label(r.Name) for r in v_nest.RelatedObjects] == expected_v
@pytest.mark.skipif(not IFC4X3_AVAILABLE, reason="IFC4X3 not available")
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"]
for r in referent_nest.RelatedObjects:
assert [x in r.Name for x in expected]
h_nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
v_nest = ifcopenshell.api.alignment.update_key_point_referents(file, vertical)
expected_h = ["A", "Q", "Q", "Q", "Q", "Q", "Q", "Z"]
expected_v = ["a", "q", "q", "q", "q", "q", "q", "q", "q", "z"]
assert [_label(r.Name) for r in h_nest.RelatedObjects] == expected_h
assert [_label(r.Name) for r in v_nest.RelatedObjects] == expected_v
ifcopenshell.api.alignment.register_referent_name_callback(None, None, None) # reset global state
@@ -0,0 +1,307 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import pytest
import ifcopenshell.api.alignment
import ifcopenshell.api.context
import ifcopenshell.api.unit
import ifcopenshell.util.alignment
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 _real_segments(layout):
segments = ifcopenshell.api.alignment.get_layout_segments(layout)
return segments[:-1] if ifcopenshell.api.alignment.has_zero_length_segment(layout) else segments
def _label(tag):
return tag.rsplit("(", 1)[1].rstrip(")")
def test_wrong_layout_type_raises_type_error():
file = _new_file()
alignment = _build_alignment(file)
with pytest.raises(TypeError):
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, alignment)
def test_not_nested_under_alignment_raises_value_error():
file = _new_file_no_context()
horizontal = file.createIfcAlignmentHorizontal(GlobalId=ifcopenshell.guid.new())
with pytest.raises(ValueError):
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, horizontal)
def test_returns_none():
file = _new_file()
alignment = _build_alignment(file)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
result = ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, horizontal)
assert result is None
def test_no_referents_or_rel_nests_created():
file = _new_file()
alignment = _build_alignment(file)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
referents_before = len(file.by_type("IfcReferent"))
rel_nests_before = len(file.by_type("IfcRelNests"))
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, horizontal)
assert len(file.by_type("IfcReferent")) == referents_before
assert len(file.by_type("IfcRelNests")) == rel_nests_before
def test_no_real_segments_leaves_tags_none():
file = _new_file_no_context()
alignment = ifcopenshell.api.alignment.create(file, "A1", include_geometry=False)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
result = ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, horizontal)
assert result is None
segments = ifcopenshell.api.alignment.get_layout_segments(horizontal)
assert len(segments) == 1 # only the auto zero-length segment
assert segments[0].DesignParameters.StartTag is None
assert segments[0].DesignParameters.EndTag is None
def test_single_real_segment_produces_only_boundary_tags():
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)
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, horizontal, label_end_tag=True)
segments = _real_segments(horizontal)
assert len(segments) == 1
dp = segments[0].DesignParameters
assert _label(dp.StartTag) == "P.O.B."
assert _label(dp.EndTag) == "P.O.E."
def test_end_tag_not_labelled_by_default():
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)
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, horizontal)
segments = _real_segments(horizontal)
assert len(segments) == 1
dp = segments[0].DesignParameters
assert _label(dp.StartTag) == "P.O.B."
assert dp.EndTag is None
def test_horizontal_tag_labels_and_adjacency():
file = _new_file()
alignment = _build_alignment(file)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, horizontal, label_end_tag=True)
segments = _real_segments(horizontal)
assert len(segments) == 7
start_labels = [_label(s.DesignParameters.StartTag) for s in segments]
end_labels = [_label(s.DesignParameters.EndTag) for s in segments]
assert start_labels == ["P.O.B.", "P.C.", "P.T.", "P.C.", "P.T.", "P.C.", "P.T."]
assert end_labels == ["P.C.", "P.T.", "P.C.", "P.T.", "P.C.", "P.T.", "P.O.E."]
# every real segment has both tags set
assert all(s.DesignParameters.StartTag is not None for s in segments)
assert all(s.DesignParameters.EndTag is not None for s in segments)
# adjacent segments agree on the tag describing their shared transition point
for i in range(len(segments) - 1):
assert segments[i].DesignParameters.EndTag == segments[i + 1].DesignParameters.StartTag
def test_vertical_tag_labels_and_adjacency():
file = _new_file()
alignment = _build_alignment(file)
vertical = ifcopenshell.api.alignment.get_vertical_layout(alignment)
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, vertical, label_end_tag=True)
segments = _real_segments(vertical)
assert len(segments) == 9
start_labels = [_label(s.DesignParameters.StartTag) for s in segments]
end_labels = [_label(s.DesignParameters.EndTag) for s in segments]
assert start_labels == [
"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.",
]
assert end_labels == [
"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 all(s.DesignParameters.StartTag is not None for s in segments)
assert all(s.DesignParameters.EndTag is not None for s in segments)
for i in range(len(segments) - 1):
assert segments[i].DesignParameters.EndTag == segments[i + 1].DesignParameters.StartTag
def test_cant_layout_boundary_tags():
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)
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, cant, label_end_tag=True)
segments = _real_segments(cant)
assert _label(segments[0].DesignParameters.StartTag) == "C.P.O.B."
assert _label(segments[-1].DesignParameters.EndTag) == "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 _label(segments[0].DesignParameters.EndTag) == "xx"
assert _label(segments[-1].DesignParameters.StartTag) == "xx"
def test_exact_tag_format():
file = _new_file()
alignment = _build_alignment(file)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
ifcopenshell.api.alignment.update_alignment_parameter_segment_tags(file, horizontal)
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
segments = _real_segments(horizontal)
assert segments[0].DesignParameters.StartTag == (
f"{ifcopenshell.util.alignment.station_as_string(file, start_station)} (P.O.B.)"
)
test_wrong_layout_type_raises_type_error()
test_not_nested_under_alignment_raises_value_error()
test_returns_none()
test_no_referents_or_rel_nests_created()
test_no_real_segments_leaves_tags_none()
test_single_real_segment_produces_only_boundary_tags()
test_end_tag_not_labelled_by_default()
test_horizontal_tag_labels_and_adjacency()
test_vertical_tag_labels_and_adjacency()
test_cant_layout_boundary_tags()
test_exact_tag_format()
@@ -0,0 +1,402 @@
# 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 _label(name):
return name.rsplit("(", 1)[1].rstrip(")")
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 == alignment
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_creates_separate_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(nest1.RelatedObjects) == 8
assert len(nest2.RelatedObjects) == 8
segment_count_after = len(ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal).RelatedObjects)
assert segment_count_after == segment_count_before
def test_passing_previous_nest_back_in_accumulates():
file = _new_file()
alignment = _build_alignment(file)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
nest1 = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
nest2 = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal, rel_nests=nest1)
assert nest1.id() == nest2.id()
assert len(nest2.RelatedObjects) == 16
def test_provided_rel_nests_is_used_as_is():
file = _new_file()
alignment = _build_alignment(file)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
# rel_nests.RelatingObject must be the IfcAlignment that nests `layout`
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_provided_rel_nests_with_wrong_relating_object_raises_type_error():
file = _new_file()
alignment = _build_alignment(file)
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
rel_nests = file.createIfcRelNests(GlobalId=ifcopenshell.guid.new(), RelatingObject=horizontal, RelatedObjects=())
with pytest.raises(TypeError):
ifcopenshell.api.alignment.update_key_point_referents(file, horizontal, rel_nests=rel_nests)
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 [_label(r.Name) 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 [_label(r.Name) 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"{alignment.Name} {ifcopenshell.util.alignment.station_as_string(file, station)} (P.O.B.)"
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 = [_label(r.Name) 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 = [_label(r.Name) 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_creates_separate_nest()
test_passing_previous_nest_back_in_accumulates()
test_provided_rel_nests_is_used_as_is()
test_provided_rel_nests_with_wrong_relating_object_raises_type_error()
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()
@@ -73,8 +73,8 @@ def test_vertical_layout_by_pi_method():
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
assert len(layout_nest.RelatedObjects) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 1
stationing_nest = ifcopenshell.api.alignment.get_stationing_nest(file, alignment)
assert len(stationing_nest.RelatedObjects) == 1
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(vlayout)
assert len(segment_nest.RelatedObjects) == 3
@@ -200,6 +200,31 @@ class TestCalculateUnitScale(test.bootstrap.IFC4):
ifcopenshell.api.unit.assign_unit(self.file, units=[angle])
assert subject.calculate_unit_scale(self.file, "PLANEANGLEUNIT") == pi / 180 * 0.001
def test_prefix_is_raised_to_the_length_exponent_for_area_and_volume(self):
# A prefixed square/cubic metre is (prefix-metre) squared/cubed:
# DECI SQUARE_METRE = dm2 = 1e-2 m2, DECI CUBIC_METRE = dm3 (litre) = 1e-3 m3.
# https://github.com/IfcOpenShell/IfcOpenShell/issues/9278
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT")
area.Prefix = "DECI"
volume = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="VOLUMEUNIT")
volume.Prefix = "DECI"
ifcopenshell.api.unit.assign_unit(self.file, units=[area, volume])
assert subject.calculate_unit_scale(self.file, "AREAUNIT") == pytest.approx(0.1**2)
assert subject.calculate_unit_scale(self.file, "VOLUMEUNIT") == pytest.approx(0.1**3)
def test_prefix_stays_linear_for_units_that_are_not_a_pure_power_of_length(self):
# For derived and non-length SI units the prefix scales the unit itself:
# KILO PASCAL = 1e3 Pa, KILO GRAM = 1e3 g.
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
pressure = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="PRESSUREUNIT")
pressure.Prefix = "KILO"
mass = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="MASSUNIT")
mass.Prefix = "KILO"
ifcopenshell.api.unit.assign_unit(self.file, units=[pressure, mass])
assert subject.calculate_unit_scale(self.file, "PRESSUREUNIT") == pytest.approx(1000)
assert subject.calculate_unit_scale(self.file, "MASSUNIT") == pytest.approx(1000)
class TestFormatLength(test.bootstrap.IFC4):
def test_run(self):