Updates alignment api. Fixes bugs authoring semantic-only alignment

This commit is contained in:
Richard Brice
2026-05-25 10:31:57 -07:00
parent 42ed398169
commit 45ea5eb07a
27 changed files with 1085 additions and 373 deletions
@@ -38,6 +38,12 @@ def test_add_segment_to_layout():
)
alignment = ifcopenshell.api.alignment.create(file, "")
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert (
len(referent_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)
design_parameters = file.create_entity(
@@ -70,7 +76,7 @@ def test_add_segment_to_layout():
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) == 3
assert len(referent_nest.RelatedObjects) == 1 # test this a second time to make sure that it is still true
test_add_segment_to_layout()
@@ -37,7 +37,9 @@ def test_add_vertical_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)
assert len(referent_nest.RelatedObjects) == 2
assert (
len(referent_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")
curve = ifcopenshell.api.alignment.get_curve(alignment)
@@ -62,7 +64,7 @@ def test_add_vertical_alignment():
for child_alignment in alignment.IsDecomposedBy[0].RelatedObjects:
assert child_alignment.is_a("IfcAlignment")
assert len(child_alignment.IsNestedBy) == 2
assert len(child_alignment.IsNestedBy) == 1
child_layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(child_alignment)
assert len(child_layout_nest.RelatedObjects) == 1 # The IfcAlignmentVertical
assert child_layout_nest.RelatedObjects[0].is_a("IfcAlignmentVertical")
@@ -52,7 +52,7 @@ def test_create_by_pi_method():
assert len(layout_nest.RelatedObjects) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 19
assert len(referent_nest.RelatedObjects) == 1
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
horizontal_segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal_layout)
@@ -73,9 +73,16 @@ def _test_horizontal() -> ifcopenshell.file:
assert y == 0.0
assert z == 0.0
# check the start point of the zero length segment
assert horizontal_alignment.IsNestedBy[0].RelatedObjects[1].DesignParameters.SegmentLength == 0.0
assert horizontal_alignment.IsNestedBy[0].RelatedObjects[1].DesignParameters.StartPoint.Coordinates[0] == x
assert horizontal_alignment.IsNestedBy[0].RelatedObjects[1].DesignParameters.StartPoint.Coordinates[1] == y
curve = ifcopenshell.api.alignment.get_curve(ali)
assert curve.is_a("IfcCompositeCurve")
assert len(curve.Segments) == 2
assert curve.Segments[0].Transition == "CONTSAMEGRADIENTSAMECURVATURE"
assert curve.Segments[1].Transition == "DISCONTINUOUS"
design_parameters = file.create_entity(
type="IfcAlignmentHorizontalSegment",
@@ -101,9 +108,16 @@ def _test_horizontal() -> ifcopenshell.file:
assert y == 50.0 * math.sin(math.pi / 6)
assert z == 0.0
# check the start point of the zero length segment
assert horizontal_alignment.IsNestedBy[0].RelatedObjects[2].DesignParameters.SegmentLength == 0.0
assert horizontal_alignment.IsNestedBy[0].RelatedObjects[2].DesignParameters.StartPoint.Coordinates[0] == x
assert horizontal_alignment.IsNestedBy[0].RelatedObjects[2].DesignParameters.StartPoint.Coordinates[1] == y
curve = ifcopenshell.api.alignment.get_curve(ali)
assert curve.is_a("IfcCompositeCurve")
assert len(curve.Segments) == 3
assert curve.Segments[1].Transition == "CONTSAMEGRADIENTSAMECURVATURE"
assert curve.Segments[2].Transition == "DISCONTINUOUS"
return file
@@ -50,7 +50,14 @@ def test_create_no_geometry():
PredefinedType="LINE",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, horizontal_alignment, design_parameters)
assert end == None
x = end[0, 3]
y = end[1, 3]
z = end[2, 3]
assert x == 100.0
assert y == 0.0
assert z == 0.0
design_parameters = file.createIfcAlignmentVerticalSegment(
StartDistAlong=0.0,
@@ -61,7 +68,14 @@ def test_create_no_geometry():
PredefinedType="CONSTANTGRADIENT",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vertical_alignment, design_parameters)
assert end == None
x = end[0, 3]
y = end[1, 3]
z = end[2, 3]
assert x == 50.0
assert y == 20.0 + 50.0 * 1.0 / 100.0
assert z == 0.0
test_create_no_geometry()
@@ -0,0 +1,443 @@
# 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 math
import pytest
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.api.unit
import numpy as np
def test_create_representation():
# expected values for horizontal segment ends points (X,Y,dx,dy)
h_expected = [
(500.0, 2500.0, math.cos(math.radians(327.0613)), math.sin(math.radians(327.0613))),
(2142.2378194934668, 1436.0145490066361, 0.8392527899703555, -0.5437414408769801),
(3660.446048592728, 2050.735651565721, 0.22453168741127044, 0.9744667882222808),
(4084.1161141648777, 3889.4623490042068, 0.22453168741127047, 0.9744667882222809),
(5469.395455576321, 4847.565492667097, 0.9910142023415828, -0.13375668490687387),
(7019.971720182908, 4638.284999653966, 0.9910142023415827, -0.13375668490687387),
(7790.932377201981, 4006.729563689594, 0.32621900658961334, -0.9452942186111613),
(8479.999918938518, 2009.9986857258034, 0.32621900658961345, -0.9452942186111613),
]
# expected values for vertical segment ends points (X,Y,dx,dy)
v_expected = [
(0.0, 100.0, 0.999846910161925, 0.01749732092783369),
(1200.0, 121.0, 0.999846910161925, 0.01749732092783369),
(2799.99999384661, 127.00000006153391, 0.9999500037507449, -0.009999499931751348),
(4399.99999384661, 111.00000023075212, 0.999950003750745, -0.009999499931751352),
(5599.9999883553455, 117.00000018438367, 0.999800059982751, 0.019996001062400855),
(6399.999988355345, 133.0000000745584, 0.999800059982751, 0.019996001062400855),
(8399.99998428796, 133.00000001862446, 0.999800059981633, -0.019996001118301257),
(9399.99998428796, 113.00000009997211, 0.999800059981633, -0.019996001118301257),
(10199.99998062693, 103.00000015081635, 0.9999875002340269, -0.004999937569813611),
(12799.99998062693, 89.99999997234107, 0.9999875002340269, -0.004999937569813611),
]
file = ifcopenshell.file(schema="IFC4X3_ADD2")
file.header.file_description.description = ["ViewDefinition [Alignment-basedView]"]
project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="FHWA Alignment")
# ifcopenshell.api.unit.assign_unit(file)
# length = ifcopenshell.api.unit.add_si_unit(file,unit_type="LENGTHUNIT")
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")
axis_model_representation_subcontext = ifcopenshell.api.context.add_context(
file,
context_type="Model",
context_identifier="Axis",
target_view="MODEL_VIEW",
parent=geometric_representation_context,
)
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 is referenced into spatial structure of site per CT 4.1.5.1
ifcopenshell.api.spatial.reference_structure(file, products=[alignment], relating_structure=site)
layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
segment1 = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint(Coordinates=((500.0, 2500.0))),
StartDirection=math.radians(327.0613),
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=1956.785654,
PredefinedType="LINE",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment1)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
dir = math.atan2(dy, dx)
assert (
pytest.approx(h_expected[1][0]) == x
and pytest.approx(h_expected[1][1]) == y
and pytest.approx(h_expected[1][2]) == dx
and pytest.approx(h_expected[1][3]) == dy
)
segment2 = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint((x, y)),
StartDirection=dir,
StartRadiusOfCurvature=1000.0,
EndRadiusOfCurvature=1000.0,
SegmentLength=1919.222667,
PredefinedType="CIRCULARARC",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment2)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
dir = math.atan2(dy, dx)
assert (
pytest.approx(h_expected[2][0]) == x
and pytest.approx(h_expected[2][1]) == y
and pytest.approx(h_expected[2][2]) == dx
and pytest.approx(h_expected[2][3]) == dy
)
segment3 = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint((x, y)),
StartDirection=dir,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=1886.905454,
PredefinedType="LINE",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment3)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
dir = math.atan2(dy, dx)
assert (
pytest.approx(h_expected[3][0]) == x
and pytest.approx(h_expected[3][1]) == y
and pytest.approx(h_expected[3][2]) == dx
and pytest.approx(h_expected[3][3]) == dy
)
segment4 = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint((x, y)),
StartDirection=dir,
StartRadiusOfCurvature=-1250.0,
EndRadiusOfCurvature=-1250.0,
SegmentLength=1848.115835,
PredefinedType="CIRCULARARC",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment4)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
dir = math.atan2(dy, dx)
assert (
pytest.approx(h_expected[4][0]) == x
and pytest.approx(h_expected[4][1]) == y
and pytest.approx(h_expected[4][2]) == dx
and pytest.approx(h_expected[4][3]) == dy
)
segment5 = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint((x, y)),
StartDirection=dir,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=1564.635765,
PredefinedType="LINE",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment5)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
dir = math.atan2(dy, dx)
assert (
pytest.approx(h_expected[5][0]) == x
and pytest.approx(h_expected[5][1]) == y
and pytest.approx(h_expected[5][2]) == dx
and pytest.approx(h_expected[5][3]) == dy
)
segment6 = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint((x, y)),
StartDirection=dir,
StartRadiusOfCurvature=-950.0,
EndRadiusOfCurvature=-950.0,
SegmentLength=1049.119737,
PredefinedType="CIRCULARARC",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment6)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
dir = math.atan2(dy, dx)
assert (
pytest.approx(h_expected[6][0]) == x
and pytest.approx(h_expected[6][1]) == y
and pytest.approx(h_expected[6][2]) == dx
and pytest.approx(h_expected[6][3]) == dy
)
segment7 = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint((x, y)),
StartDirection=dir,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=2112.285084,
PredefinedType="LINE",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment7)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(h_expected[7][0]) == x
and pytest.approx(h_expected[7][1]) == y
and pytest.approx(h_expected[7][2]) == dx
and pytest.approx(h_expected[7][3]) == dy
)
vlayout = ifcopenshell.api.alignment.get_vertical_layout(alignment)
segment1 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=0.0,
HorizontalLength=1200.0,
StartHeight=100.0,
StartGradient=1.75 / 100.0,
EndGradient=1.75 / 100.0,
PredefinedType="CONSTANTGRADIENT",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment1)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[1][0]) == x
and pytest.approx(v_expected[1][1]) == y
and pytest.approx(v_expected[1][2]) == dx
and pytest.approx(v_expected[1][3]) == dy
)
segment2 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=x,
HorizontalLength=1600.0,
StartHeight=y,
StartGradient=dy / dx,
EndGradient=-1.0 / 100.0,
PredefinedType="PARABOLICARC",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment2)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[2][0]) == x
and pytest.approx(v_expected[2][1]) == y
and pytest.approx(v_expected[2][2]) == dx
and pytest.approx(v_expected[2][3]) == dy
)
segment3 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=x,
HorizontalLength=1600.0,
StartHeight=y,
StartGradient=dy / dx,
EndGradient=-1.0 / 100.0,
PredefinedType="CONSTANTGRADIENT",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment3)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[3][0]) == x
and pytest.approx(v_expected[3][1]) == y
and pytest.approx(v_expected[3][2]) == dx
and pytest.approx(v_expected[3][3]) == dy
)
segment4 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=x,
HorizontalLength=1200.0,
StartHeight=y,
StartGradient=dy / dx,
EndGradient=2.0 / 100.0,
PredefinedType="PARABOLICARC",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment4)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[4][0]) == x
and pytest.approx(v_expected[4][1]) == y
and pytest.approx(v_expected[4][2]) == dx
and pytest.approx(v_expected[4][3]) == dy
)
segment5 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=x,
HorizontalLength=800.0,
StartHeight=y,
StartGradient=dy / dx,
EndGradient=2.0 / 100.0,
PredefinedType="CONSTANTGRADIENT",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment5)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[5][0]) == x
and pytest.approx(v_expected[5][1]) == y
and pytest.approx(v_expected[5][2]) == dx
and pytest.approx(v_expected[5][3]) == dy
)
segment6 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=x,
HorizontalLength=2000.0,
StartHeight=y,
StartGradient=dy / dx,
EndGradient=-2.0 / 100.0,
PredefinedType="PARABOLICARC",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment6)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[6][0]) == x
and pytest.approx(v_expected[6][1]) == y
and pytest.approx(v_expected[6][2]) == dx
and pytest.approx(v_expected[6][3]) == dy
)
segment7 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=x,
HorizontalLength=1000.0,
StartHeight=y,
StartGradient=dy / dx,
EndGradient=-2.0 / 100.0,
PredefinedType="CONSTANTGRADIENT",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment7)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[7][0]) == x
and pytest.approx(v_expected[7][1]) == y
and pytest.approx(v_expected[7][2]) == dx
and pytest.approx(v_expected[7][3]) == dy
)
segment8 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=x,
HorizontalLength=800.0,
StartHeight=y,
StartGradient=dy / dx,
EndGradient=-0.5 / 100.0,
PredefinedType="PARABOLICARC",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment8)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[8][0]) == x
and pytest.approx(v_expected[8][1]) == y
and pytest.approx(v_expected[8][2]) == dx
and pytest.approx(v_expected[8][3]) == dy
)
segment9 = file.createIfcAlignmentVerticalSegment(
StartDistAlong=x,
HorizontalLength=2600.0,
StartHeight=y,
StartGradient=dy / dx,
EndGradient=-0.5 / 100.0,
PredefinedType="CONSTANTGRADIENT",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment9)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
assert (
pytest.approx(v_expected[9][0]) == x
and pytest.approx(v_expected[9][1]) == y
and pytest.approx(v_expected[9][2]) == dx
and pytest.approx(v_expected[9][3]) == dy
)
ifcopenshell.api.alignment.create_representation(file, alignment)
curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
assert curve.is_a("IfcCompositeCurve")
for s in curve.Segments:
assert len(s.UsingCurves) == 1
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
assert curve.is_a("IfcCompositeCurve")
for index, s in enumerate(curve.Segments):
assert len(s.UsingCurves) == 1
assert s.Placement.Location.Coordinates[0] == pytest.approx(h_expected[index][0])
assert s.Placement.Location.Coordinates[1] == pytest.approx(h_expected[index][1])
assert s.Placement.RefDirection.DirectionRatios[0] == pytest.approx(h_expected[index][2])
assert s.Placement.RefDirection.DirectionRatios[1] == pytest.approx(h_expected[index][3])
curve = ifcopenshell.api.alignment.get_layout_curve(vlayout)
assert curve.is_a("IfcGradientCurve")
for index, s in enumerate(curve.Segments):
assert len(s.UsingCurves) == 1
assert s.Placement.Location.Coordinates[0] == pytest.approx(v_expected[index][0])
assert s.Placement.Location.Coordinates[1] == pytest.approx(v_expected[index][1])
assert s.Placement.RefDirection.DirectionRatios[0] == pytest.approx(v_expected[index][2])
assert s.Placement.RefDirection.DirectionRatios[1] == pytest.approx(v_expected[index][3])
test_create_representation()
@@ -65,7 +65,7 @@ def test_vertical_layout_by_pi_method():
assert len(layout_nest.RelatedObjects) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 6
assert len(referent_nest.RelatedObjects) == 1
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(vlayout)
assert len(segment_nest.RelatedObjects) == 3