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
@@ -70,8 +70,10 @@ from .get_basis_curve import get_basis_curve
from .get_cant_layout import get_cant_layout
from .get_child_alignments import get_child_alignments
from .get_curve import get_curve
from .get_curve_segment import get_curve_segment
from .get_curve_segment_transition_code import get_curve_segment_transition_code
from .get_horizontal_layout import get_horizontal_layout
from .get_layout import get_layout
from .get_layout_curve import get_layout_curve
from .get_layout_segments import get_layout_segments
from .get_mapped_segments import get_mapped_segments
@@ -86,6 +88,7 @@ from .layout_vertical_alignment_by_pi_method import (
layout_vertical_alignment_by_pi_method,
)
from .name_segments import name_segments
from .update_end_point import update_end_point
from .update_fallback_position import update_fallback_position
from .util import *
@@ -112,8 +115,10 @@ __all__ = [
"get_cant_layout",
"get_child_alignments",
"get_curve",
"get_curve_segment",
"get_curve_segment_transition_code",
"get_horizontal_layout",
"get_layout",
"get_layout_curve",
"get_layout_segments",
"get_parent_alignment",
@@ -124,6 +129,7 @@ __all__ = [
"layout_vertical_alignment_by_pi_method",
"name_segments",
"register_referent_name_callback",
"update_end_point",
"update_fallback_position",
"get_mapped_segments",
]
@@ -16,6 +16,7 @@
# 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 Union
import numpy as np
import ifcopenshell
@@ -24,6 +25,9 @@ import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.util.unit
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._get_segment_endpoint import _get_segment_endpoint
from ifcopenshell.api.alignment._update_zero_length_segment_placement import _update_zero_length_segment_placement
from ifcopenshell.api.alignment._map_alignment_cant_segment import (
_map_alignment_cant_segment,
)
@@ -39,11 +43,26 @@ from ifcopenshell.api.alignment._update_curve_segment_transition_code import (
def _add_curve_segment_to_composite_curve(
file: ifcopenshell.file, curve_segment: entity_instance, composite_curve: entity_instance
):
file: ifcopenshell.file,
layout_segment: entity_instance,
curve_segment: entity_instance,
composite_curve: entity_instance,
) -> Union[np.array, None]:
"""
Adds a curve segment to a composite curve and returns the end point of the added segment.
:param file: The IFC file
:param layout_segment: The layout segment
:param curve_segment: The curve segment to be added
:param composite_curve: The composite curve to which the segment will be added
:return: The end point of the added segment or None if an error occurs
"""
if 0 < len(curve_segment.UsingCurves):
raise TypeError("IfcCurveSegment cannot belong to other curves")
prev_segment = None
zero_length_segment = None
settings = ifcopenshell.geom.settings()
if composite_curve.Segments == None or 0 == len(composite_curve.Segments):
# this is the first segment so just add it
@@ -56,22 +75,29 @@ def _add_curve_segment_to_composite_curve(
composite_curve.Segments += (curve_segment,)
assert len(curve_segment.UsingCurves) == 1
else:
# not the first segment, so get the zero_length segment (if it exists)
zero_length_segment = (
composite_curve.Segments[-1]
if ifcopenshell.api.alignment.has_zero_length_segment(composite_curve)
else None
)
prev_segment = None
# get the previous segment, which is either the on preceeding the zero length segment (if it exists) or
# the last curve segment if there is no zero length segment.
# This segment's transition code will need to be updated to match the new curve segment.
if zero_length_segment and 1 < len(composite_curve.Segments):
prev_segment = composite_curve.Segments[-2]
elif zero_length_segment == None:
prev_segment = composite_curve.Segments[-1]
curve_segment.Transition = "CONTINUOUS"
# IfcCompositeCurve is supposed to be comprised of continuous segments
curve_segment.Transition = "DISCONTINUOUS"
# get a list of all but the last segment (skips the zero length segment, if it exists)
segments = composite_curve.Segments[0:-1]
if zero_length_segment:
# if there is a zero length segment, need to append new curve_segment and the zero length segment to the array
# them update the composite curve segments with the new array
segments += (
curve_segment,
zero_length_segment,
@@ -79,31 +105,23 @@ def _add_curve_segment_to_composite_curve(
composite_curve.Segments = []
composite_curve.Segments += segments
else:
# if there is no zero length segment, we can just append the new curve segment to the existing array of segments
composite_curve.Segments += (curve_segment,)
if prev_segment:
_update_curve_segment_transition_code(prev_segment, curve_segment)
if prev_segment:
_update_curve_segment_transition_code(prev_segment, curve_segment)
if zero_length_segment:
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, curve_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
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])
end_point = _get_segment_endpoint(file, layout_segment)
if zero_length_segment:
_update_zero_length_segment_placement(file, zero_length_segment, end_point)
_update_curve_segment_transition_code(curve_segment, zero_length_segment)
# assume IfcAxis2Placement2D
zero_length_segment.Placement.Location.Coordinates = (x, y)
zero_length_segment.Placement.RefDirection.DirectionRatios = (dx, dy)
_update_curve_segment_transition_code(curve_segment, zero_length_segment)
return end_point
def _add_segment_to_curve(file: ifcopenshell.file, segment: entity_instance, curve: entity_instance) -> None:
def _add_segment_to_curve(
file: ifcopenshell.file, layout_segment: entity_instance, curve: entity_instance
) -> Union[np.array, None]:
"""
Creates an IfcCurveSegment from the IfcAlignmentSegment and adds it to the representation curve. The IfcCurveSegment is added
at the end of the curve, but before the manditory zero length segment. The IfcCurveSegment.Transition for the segment
@@ -114,16 +132,18 @@ def _add_segment_to_curve(file: ifcopenshell.file, segment: entity_instance, cur
:return: None
"""
expected_types = ["IfcAlignmentSegment"]
if not segment.is_a() in expected_types:
if not layout_segment.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{segment.is_a()}"
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{layout_segment.is_a()}"
)
if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment") and not curve.is_a("IfcCompositeCurve"):
if layout_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment") and not curve.is_a("IfcCompositeCurve"):
raise TypeError(f"Expected to see IfcCompositeCurve, instead received '{curve.is_a()}'.")
elif segment.DesignParameters.is_a("IfcAlignmentVerticalSegment") and not curve.is_a("IfcGradientCurve"):
elif layout_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment") and not curve.is_a("IfcGradientCurve"):
raise TypeError(f"Expected to see IfcGradientCurve, instead received '{curve.is_a()}'.")
elif segment.DesignParameters.is_a("IfcAlignmentCantSegment") and not curve.is_a("IfcSegmentedReferenceCurve"):
elif layout_segment.DesignParameters.is_a("IfcAlignmentCantSegment") and not curve.is_a(
"IfcSegmentedReferenceCurve"
):
raise TypeError(f"Expected to see IfcSegmentedReferenceCurve, instead received '{curve.is_a()}'.")
expected_type = "IfcCompositeCurve"
@@ -131,16 +151,18 @@ def _add_segment_to_curve(file: ifcopenshell.file, segment: entity_instance, cur
raise TypeError(f"Expected to see {expected_type}, instead received {curve.is_a()}.")
# map the IfcAlignmentSegment to an IfcCurveSegment (or two in the case of helmert curves)
if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
mapped_segments = _map_alignment_horizontal_segment(file, segment)
elif segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
mapped_segments = _map_alignment_vertical_segment(file, segment)
elif segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
cant_layout = segment.Nests[0].RelatingObject
mapped_segments = _map_alignment_cant_segment(file, segment, cant_layout.RailHeadDistance)
if layout_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
mapped_segments = _map_alignment_horizontal_segment(file, layout_segment)
elif layout_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
mapped_segments = _map_alignment_vertical_segment(file, layout_segment)
elif layout_segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
cant_layout = layout_segment.Nests[0].RelatingObject
mapped_segments = _map_alignment_cant_segment(file, layout_segment, cant_layout.RailHeadDistance)
else:
assert False
for mapped_segment in mapped_segments:
if mapped_segment:
_add_curve_segment_to_composite_curve(file, mapped_segment, curve)
end_point = _add_curve_segment_to_composite_curve(file, layout_segment, mapped_segment, curve)
return end_point
@@ -16,12 +16,14 @@
# 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
from typing import Union
import numpy as np
import ifcopenshell
import ifcopenshell.api.alignment
from ifcopenshell.api.alignment import _map_alignment_cant_segment
from ifcopenshell.api.alignment._update_zero_length_segment_placement import _update_zero_length_segment_placement
import ifcopenshell.api.nest
import ifcopenshell.api.pset
import ifcopenshell.geom
@@ -29,15 +31,29 @@ import ifcopenshell.util.alignment
import ifcopenshell.util.unit
from ifcopenshell import entity_instance, ifcopenshell_wrapper
from ifcopenshell.api.alignment._add_segment_to_curve import _add_segment_to_curve
from ifcopenshell.api.alignment._get_segment_endpoint import _get_segment_endpoint
from ifcopenshell.api.alignment._get_segment_start_point_label import (
_get_segment_start_point_label,
)
from ifcopenshell.api.alignment._map_alignment_cant_segment import (
_map_alignment_cant_segment,
)
from ifcopenshell.api.alignment._map_alignment_horizontal_segment import (
_map_alignment_horizontal_segment,
)
from ifcopenshell.api.alignment._map_alignment_vertical_segment import (
_map_alignment_vertical_segment,
)
def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, segment: entity_instance) -> None:
def _add_segment_to_layout(
file: ifcopenshell.file, layout: entity_instance, layout_segment: entity_instance
) -> Union[np.array, None]:
"""
Adds an IfcAlignmentSegment to a layout alignment (IfcAlignmentHorizontal/Vertical/Cant). This segment is added at the end
of the layout, before the manditory zero length segment. An IfcCurveSegment is created for the corresponding geometric representation.
of the layout, before the manditory zero length segment (if it exists).
If the layout has a corresponding geometric representation, an IfcCurveSegment is created for it and appended at the end
of the representation curve, before the zero length segment (if it exists).
:param layout: The layout alignment
:param segment: The segment to be appended
@@ -50,160 +66,31 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if not (segment.is_a("IfcAlignmentSegment")):
raise TypeError(f"Expected to see IfcAlignmentSegment, instead received {segment.is_a()}.")
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
if not (layout_segment.is_a("IfcAlignmentSegment")):
raise TypeError(f"Expected to see IfcAlignmentSegment, instead received {layout_segment.is_a()}.")
# add the new segment to the layout
ifcopenshell.api.nest.assign_object(file, related_objects=[segment], relating_object=layout)
ifcopenshell.api.nest.assign_object(file, related_objects=[layout_segment], relating_object=layout)
# segment is attached at the end, but this is after the zero length segment
# swap the last two segments
ifcopenshell.api.nest.reorder_nesting(file, segment, -1, -1)
ifcopenshell.api.nest.reorder_nesting(file, layout_segment, -1, -1)
# For cant segments, the end point depends on the next segment. The next segment is the
# zero-length segment and it hasn't been updated to match the end point.
# For this reason, we can't compute the end point from the IfcCurveSegment, but instead we
# compute it from the layout segment design parameters.
end_point = _get_segment_endpoint(file, layout_segment)
# update the position of the zero length layout segment to be at the end point of the newly added segment
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
zero_length_layout_segment = segment_nest.RelatedObjects[-1]
_update_zero_length_segment_placement(file, zero_length_layout_segment, end_point)
# if there is a curve defined, add a new IfcCurveSegment to it.
# _add_segment_to_curve maps the layout segment to the appropriate IfcCurveSegment type and adds it to the curve.
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
if curve:
# add the new segment to the geometric representation curve
_add_segment_to_curve(file, segment, curve)
_add_segment_to_curve(file, layout_segment, curve)
# gather information to:
# (1) add a referent at the start of this segment
# (2) update the name of the zero length segment's referent
# get the distance along the alignment to the start of the new segment
dist_along = 0.0
if layout.is_a("IfcAlignmentHorizontal"):
for nest in layout.IsNestedBy:
for seg in nest.RelatedObjects:
if seg.is_a("IfcAlignmentSegment"):
dist_along += seg.DesignParameters.SegmentLength
# the length of the current segment is in dist_along, so subtract it out
dist_along -= segment.DesignParameters.SegmentLength
else:
dist_along = segment.DesignParameters.StartDistAlong
# get the station of the start of the segment
alignment = ifcopenshell.api.alignment.get_alignment(layout)
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
station = start_station + dist_along
# update the zero length layout segment
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
zero_length_segment = segment_nest.RelatedObjects[-1]
mapped_segments = ifcopenshell.api.alignment.get_mapped_segments(segment)
mapped_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
# compute the end point matrix
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, mapped_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
# update the zero length segment semantic representation parameters
if zero_length_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
zero_length_segment.DesignParameters.StartPoint.Coordinates = (x, y)
zero_length_segment.DesignParameters.StartDirection = dy / dx
elif zero_length_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
y = float(end[1, 3]) / unit_scale
zero_length_segment.DesignParameters.StartHeight = y
dx = float(end[0, 0])
dy = float(end[1, 0])
zero_length_segment.DesignParameters.StartGradient = dy / dx
zero_length_segment.DesignParameters.EndGradient = zero_length_segment.DesignParameters.StartGradient
else:
z = float(end[2, 3]) / unit_scale
dx = float(end[0, 1])
dy = float(end[1, 1])
dz = float(end[2, 1])
ds = math.sqrt(dx * dx + dy * dy)
slope = dz / ds
railhead = layout.RailHeadDistance
zero_length_segment.DesignParameters.StartCantLeft = z + slope * railhead / 2.0
zero_length_segment.DesignParameters.StartCantRight = z - slope * railhead / 2.0
# updated the referent's name because the referent is now at a new station
start_dist_along = 0.0
if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
start_dist_along = dist_along + segment.DesignParameters.SegmentLength
else:
start_dist_along = segment.DesignParameters.StartDistAlong + segment.DesignParameters.HorizontalLength
zero_length_segment.DesignParameters.StartDistAlong = start_dist_along
end_referent = zero_length_segment.PositionedRelativeTo[0].RelatingPositioningElement
end_referent.Name = f"{_get_segment_start_point_label(zero_length_segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,start_station+start_dist_along)})"
# update the referent's geometric representation's location
end_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue = start_dist_along
settings = ifcopenshell.geom.settings()
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
curve_fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data)
curve_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, curve_fn)
p = curve_evaluator.evaluate(start_dist_along * unit_scale)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
y = float(p[1, 3]) / unit_scale
z = float(p[2, 3]) / unit_scale
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
end_referent.ObjectPlacement.CartesianPosition.Location.Coordinates = (x, y, z)
end_referent.ObjectPlacement.CartesianPosition.Axis.DirectionRatios = (ax, ay, az)
end_referent.ObjectPlacement.CartesianPosition.RefDirection.DirectionRatios = (rx, ry, rz)
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
end_referent_station = start_station + start_dist_along
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=end_referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": end_referent_station})
# create the start of segment referent
# get the previous segment. Working from the end of the basis curve, -1 is zero length segment
# -2 is the newly added segment, so -3 is the segment occuring just before the newly added segment
prev_segment = segment_nest.RelatedObjects[-3] if 2 < len(segment_nest.RelatedObjects) else None
name = f"{_get_segment_start_point_label(prev_segment,segment)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, distance_along=dist_along, station=station, name=name, positioned_product=segment
)
if len(curve.Segments) == 2 and layout.is_a("IfcAlignmentHorizontal"):
# this is the first real segment in the horizontal alignment
# update the location of the alignment's stationing referent
alignment = ifcopenshell.api.alignment.get_alignment(layout)
ref_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
stationing_referent = ref_nest.RelatedObjects[0]
p = curve_evaluator.evaluate(
stationing_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue
)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
y = float(p[1, 3]) / unit_scale
z = float(p[2, 3]) / unit_scale
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
stationing_referent.ObjectPlacement.CartesianPosition.Location.Coordinates = (x, y, z)
stationing_referent.ObjectPlacement.CartesianPosition.Axis.DirectionRatios = (ax, ay, az)
stationing_referent.ObjectPlacement.CartesianPosition.RefDirection.DirectionRatios = (rx, ry, rz)
return end_point
@@ -42,17 +42,8 @@ def _add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance) -
f"Expected layout type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if not ifcopenshell.api.alignment.add_zero_length_segment(file, layout, include_referent=False):
return # zero length segment not added, probably because it already exists
ifcopenshell.api.alignment.add_zero_length_segment(file, layout)
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
if curve:
ifcopenshell.api.alignment.add_zero_length_segment(file, curve)
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
segment = segment_nest.RelatedObjects[-1]
alignment = ifcopenshell.api.alignment.get_alignment(layout)
station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
name = f"{_get_segment_start_point_label(segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(file, alignment, 0.0, station, name, segment)
@@ -35,6 +35,8 @@ def _create_geometric_representation(file: ifcopenshell.file, alignment: entity_
4) Vertical only (this occurs when horizontal is reused from a parent alignment) -> IfcGradientCurve
5) Vertical + Cant (this occurs when horizontal is reused from a parent alignment) -> IfcSegmentedReferenceCurve
This method creates the geometric representation entity and assigns it to the alignment, but does not populate the geometry of the representation.
:param alignment: The alignment for which the representation is being created
:return: None
"""
@@ -43,13 +45,6 @@ def _create_geometric_representation(file: ifcopenshell.file, alignment: entity_
if not alignment.is_a(expected_type):
raise TypeError(f"Expected {expected_type} but got {alignment.is_a()}")
placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement2D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0))),
)
alignment.ObjectPlacement = placement
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
layouts = ifcopenshell.api.alignment.get_alignment_layouts(alignment)
@@ -126,7 +121,7 @@ def _create_geometric_representation(file: ifcopenshell.file, alignment: entity_
ifcopenshell.api.geometry.assign_representation(file, alignment, representation)
for child_alignment in children:
child_alignment.ObjectPlacement = placement
child_alignment.ObjectPlacement = alignment.ObjectPlacement
child_layouts = ifcopenshell.api.alignment.get_alignment_layouts(child_alignment)
if len(child_layouts) == 1:
assert child_layouts[0].is_a("IfcAlignmentVertical")
@@ -0,0 +1,88 @@
# 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 ifcopenshell.api.alignment
from ifcopenshell import entity_instance, ifcopenshell_wrapper
from ifcopenshell.api.alignment._map_alignment_segment import _map_alignment_segment
from typing import Union
import math
import numpy as np
def _get_segment_endpoint(file: ifcopenshell.file, segment: entity_instance) -> Union[np.array, None]:
"""
Computes the 4x4 matrix for a segment end point. The segment can be an IfcAlignmentSegment
or IfcCurveSegment
"""
expected_types = ["IfcAlignmentSegment", "IfcCurveSegment"]
if not segment.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {segment.is_a()}"
)
file.begin_transaction() # use a transaction so we can discard any temporary IFC entities created
curve_segment = segment
if segment.is_a("IfcAlignmentSegment"):
layout = ifcopenshell.api.alignment.get_layout(segment)
mapped_segments = _map_alignment_segment(file, layout, segment)
curve_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
# Inside of the IfcOpenShell C++ implementation where the IfcCurveSegment calculations occur,
# the composite curve owning the segment is evaluated to determine if a horizontal, vertical, or cant segment is being evaluated.
# This is necessary to determine how the end point of the curve segment is calculated.
# A temporary curve segment has been created and it needs to be associated with the correct composite curve for the end point to be calculated correctly.
# Inside the C++ implementation, if a composite curve isn't associated with the segment the segment is assumed to be horizontal. For this reason
# a temporary IfcCompositeCurve for horizontal segments doesn't need to be created.
if layout.is_a("IfcAlignmentVertical"):
gc = file.createIfcGradientCurve(Segments=[curve_segment])
elif layout.is_a("IfcAlignmentCant"):
# The evaluation of cant segments depend on the start conditions of the next segment. In the absense of a next segment the
# optional EndPoint is used. Since a tempoaryar IfcSegmentReferenceCurve is being used, there is not a next segment.
# For this reason the EndPoint must be created from the design parameters of the sementic segment definiton.
Dsl = segment.DesignParameters.StartCantLeft
Dsr = segment.DesignParameters.StartCantRight
Del = segment.DesignParameters.EndCantLeft if segment.DesignParameters.EndCantLeft != None else Dsl
Der = segment.DesignParameters.EndCantRight if segment.DesignParameters.EndCantRight != None else Dsr
cant = Der - Del
rh = layout.RailHeadDistance
Ay = cant / rh
Az = math.sqrt(rh**2 - cant**2) / rh
src = file.createIfcSegmentedReferenceCurve(
Segments=[curve_segment],
EndPoint=file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint((segment.DesignParameters.StartDistAlong, 0.5 * cant, 0.0)),
RefDirection=file.createIfcDirection((1.0, 0.0, 0.0)),
Axis=file.createIfcDirection((0.0, Ay, Az)),
),
)
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, curve_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
x = segment_fn.end()
e = segment_evaluator.evaluate(x)
end = np.array(e)
file.discard_transaction()
return end
@@ -24,10 +24,12 @@ from ifcopenshell import entity_instance
def _get_axis(file: ifcopenshell.file, Ds: float, rail_head_distance: float) -> entity_instance:
Dy = rail_head_distance
Dz = 2 * Ds
D = math.sqrt(Dy * Dy + Dz * Dz)
return file.createIfcDirection((0.0, Dz / D, Dy / D))
# solves the ratio right triangle legs to hypotenous
# Dh^2 = Dy^2 + Dz^2
Dh = rail_head_distance # hypotenous
Dy = 2 * Ds # horizontal leg
Dz = math.sqrt(Dh * Dh - Dy * Dy) # vertical leg
return file.createIfcDirection((0.0, Dy / Dh, Dz / Dh))
def _map_constant_cant(
@@ -54,7 +56,7 @@ def _map_constant_cant(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point,
Axis=_get_axis(file, Ds, rail_head_distance),
Axis=_get_axis(file, 0.5 * (Dsr - Dsl), rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction), 0.0)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
@@ -0,0 +1,49 @@
# 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.abc import Sequence
import ifcopenshell
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._map_alignment_cant_segment import (
_map_alignment_cant_segment,
)
from ifcopenshell.api.alignment._map_alignment_horizontal_segment import (
_map_alignment_horizontal_segment,
)
from ifcopenshell.api.alignment._map_alignment_vertical_segment import (
_map_alignment_vertical_segment,
)
def _map_alignment_segment(
file: ifcopenshell.file, layout: entity_instance, segment: entity_instance
) -> Sequence[entity_instance]:
"""
Maps an IfcAlignmentSegment to its corresponding IfcCurveSegment(s) in the geometric representation.
The mapping is done based on the layout type and segment type.
"""
if layout.is_a("IfcAlignmentHorizontal"):
mapped_segments = _map_alignment_horizontal_segment(file, segment)
elif layout.is_a("IfcAlignmentVertical"):
mapped_segments = _map_alignment_vertical_segment(file, segment)
else:
mapped_segments = _map_alignment_cant_segment(file, segment, layout.RailHeadDistance)
return mapped_segments
@@ -0,0 +1,71 @@
# 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 numpy as np
import ifcopenshell
import math
import ifcopenshell.api.alignment
import ifcopenshell.util.unit
from ifcopenshell import entity_instance
def _update_zero_length_segment_placement(
file: ifcopenshell.file, zero_length_segment: entity_instance, placement: np.array
) -> None:
"""
Updates the placement of a zero length segment (i.e. a segment with identical start and end point) based on a 4x4 placement matrix.
The zero_length_segment can be an IfcAlignmentSegment or IfcCurveSegment.
"""
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
x = float(placement[0, 3]) / unit_scale
y = float(placement[1, 3]) / unit_scale
z = float(placement[2, 3]) / unit_scale
Rdx = float(placement[0, 0])
Rdy = float(placement[1, 0])
Rdz = float(placement[2, 0])
Adx = float(placement[0, 2])
Ady = float(placement[1, 2])
Adz = float(placement[2, 2])
if zero_length_segment.is_a("IfcCurveSegment"):
if zero_length_segment.Placement.is_a("IfcAxis2Placement2D"):
zero_length_segment.Placement.Location.Coordinates = (x, y)
zero_length_segment.Placement.RefDirection.DirectionRatios = (Rdx, Rdy)
else:
zero_length_segment.Placement.Location.Coordinates = (x, y, z)
zero_length_segment.Placement.RefDirection.DirectionRatios = (Rdx, Rdy, Rdz)
zero_length_segment.Placement.Axis.DirectionRatios = (Adx, Ady, Adz)
elif zero_length_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
zero_length_segment.DesignParameters.StartPoint.Coordinates = (x, y)
zero_length_segment.DesignParameters.StartDirection = math.atan(Rdy / Rdx)
elif zero_length_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
zero_length_segment.DesignParameters.StartDistAlong = x
zero_length_segment.DesignParameters.StartHeight = y
zero_length_segment.DesignParameters.StartGradient = Rdy / Rdx
zero_length_segment.DesignParameters.EndGradient = zero_length_segment.DesignParameters.StartGradient
else:
slope = Ady / math.sqrt(Ady**2 + Adz**2)
layout = ifcopenshell.api.alignment.get_layout(zero_length_segment)
railhead = layout.RailHeadDistance
zero_length_segment.DesignParameters.StartDistAlong = x
zero_length_segment.DesignParameters.StartCantLeft = y - slope * railhead / 2.0
zero_length_segment.DesignParameters.StartCantRight = y + slope * railhead / 2.0
zero_length_segment.DesignParameters.EndCantLeft = zero_length_segment.DesignParameters.StartCantLeft
zero_length_segment.DesignParameters.EndCantRight = zero_length_segment.DesignParameters.StartCantRight
@@ -20,6 +20,7 @@ import numpy as np
import ifcopenshell
import ifcopenshell.api.alignment
from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position
import ifcopenshell.api.pset
import ifcopenshell.geom
import ifcopenshell.guid
@@ -58,7 +59,7 @@ def add_stationing_referent(
object_placement = None
representation = None
if basis_curve:
if basis_curve and basis_curve.is_a("IfcCompositeCurve") and 0 < len(basis_curve.Segments):
object_placement = file.createIfcLinearPlacement(
RelativePlacement=file.createIfcAxis2PlacementLinear(
Location=file.createIfcPointByDistanceExpression(
@@ -71,54 +72,13 @@ def add_stationing_referent(
),
)
is_valid_curve = True
if basis_curve.is_a("IfcCompositeCurve") and len(basis_curve.Segments) == 0:
is_valid_curve = False
if basis_curve.is_a("IfcPolyline") and len(basis_curve.Points) < 2:
is_valid_curve = False
elif basis_curve.is_a("IfcIndexedPolyCurve") and len(basis_curve.Points.CoordList) < 2:
is_valid_curve = False
if is_valid_curve:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
settings = ifcopenshell.geom.settings()
fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data)
if basis_curve.is_a("IfcPolyline") or basis_curve.is_a("IfcIndexedPolyCurve"):
fn = ifcopenshell_wrapper.convert_loop_to_function_item(fn)
evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, fn)
p = evaluator.evaluate(distance_along * unit_scale)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
y = float(p[1, 3]) / unit_scale
z = float(p[2, 3]) / unit_scale
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
else:
x = 0.0
y = 0.0
z = 0.0
rx = 1.0
ry = 0.0
rz = 0.0
ax = 0.0
ay = 0.0
az = 1.0
object_placement.CartesianPosition = file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint((x, y, z)),
Axis=file.createIfcDirection((ax, ay, az)),
RefDirection=file.createIfcDirection((rx, ry, rz)),
update_fallback_position(file, object_placement)
else:
object_placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint(alignment.ObjectPlacement.RelativePlacement.Location.Coordinates)
),
)
# this commented out code is what you would do to add a geometric representation of the referent
@@ -144,7 +104,12 @@ def add_stationing_referent(
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": station})
nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
nest.RelatedObjects += (referent,)
if nest is None:
nest = file.createIfcRelNests(
GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=(referent,)
)
else:
nest.RelatedObjects += (referent,)
nest.RelatedObjects = sorted(
nest.RelatedObjects, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station")
@@ -18,14 +18,12 @@
import math
import numpy as np
import ifcopenshell
import ifcopenshell.api.alignment
from ifcopenshell.api.alignment._get_segment_endpoint import _get_segment_endpoint
from ifcopenshell.api.alignment._update_zero_length_segment_placement import _update_zero_length_segment_placement
import ifcopenshell.api.nest
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as wrapper
import ifcopenshell.util.alignment
import ifcopenshell.util.unit
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._get_segment_start_point_label import (
@@ -42,14 +40,13 @@ from ifcopenshell.api.alignment._update_curve_segment_transition_code import (
)
def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, include_referent: bool = True) -> bool:
def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance) -> bool:
"""
Adds a zero length segment to the end of a layout.
If the layout already has a zero length segment, nothing is changed.
:param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant, IfcCompositeCurve, IfcGradientCurve, IfcSegmentedReferenceCurve
:param include_referent: If True, an IfcReferent representing the ending point of the layout is included for IfcLinearElement layouts (i.e. business logic)
:return: True if segment is added
"""
@@ -74,28 +71,6 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in
return False
if layout.is_a("IfcCompositeCurve") or layout.is_a("IfcGradientCurve") or layout.is_a("IfcSegmentedReferenceCurve"):
x = 0.0
y = 0.0
dx = 1.0
dy = 0.0
segment_start = 0.0
last_segment = None
if layout.Segments and 0 < len(layout.Segments):
# If there are segments, get the last segment and compute the end point and tangent direction
# because this becomes of placement of the zero length segment
last_segment = layout.Segments[-1]
settings = ifcopenshell.geom.settings()
fn = wrapper.map_shape(settings, last_segment.wrapped_data)
eval = wrapper.function_item_evaluator(settings, fn)
e = np.array(eval.evaluate(fn.end()))
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
e[:3, 3] /= unit_scale
x = float(e[0, 3])
y = float(e[1, 3])
dx = float(e[0, 0])
dy = float(e[1, 0])
parent_curve = file.createIfcLine(
Pnt=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))),
Dir=file.createIfcVector(
@@ -103,22 +78,36 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in
Magnitude=1.0,
),
)
if layout.is_a("IfcSegmentedReferenceCurve"):
placement = file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
RefDirection=file.createIfcDirection((1.0, 0.0, 0.0)),
Axis=file.createIfcDirection((0.0, 0.0, 1.0)),
)
else:
placement = file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((0.0, 0.0)),
RefDirection=file.createIfcDirection((1.0, 0.0)),
)
zero_length_curve_segment = file.createIfcCurveSegment(
Transition="DISCONTINUOUS",
Placement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((x, y)),
RefDirection=file.createIfcDirection((dx, dy)),
),
Placement=placement,
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(0.0),
ParentCurve=parent_curve,
)
layout.Segments += (zero_length_curve_segment,)
if last_segment:
if layout.Segments and 0 < len(layout.Segments):
# If there are segments, get the last segment and compute the end point and tangent direction
# because this becomes of placement of the zero length segment
last_segment = layout.Segments[-1]
end_point = _get_segment_endpoint(file, last_segment)
_update_zero_length_segment_placement(file, zero_length_curve_segment, end_point)
_update_curve_segment_transition_code(last_segment, zero_length_curve_segment)
layout.Segments += (zero_length_curve_segment,)
# add zero length segments to base curves
if layout.is_a("IfcSegmentedReferenceCurve"):
ifcopenshell.api.alignment.add_zero_length_segment(file, layout.BaseCurve)
@@ -139,22 +128,14 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in
break
if last_segment:
file.begin_transaction() # use a transaction so we can discard any temporary IFC entities created
e = _get_segment_endpoint(file, last_segment)
settings = ifcopenshell.geom.settings()
mapped_segments = _map_alignment_horizontal_segment(file, last_segment)
geometry_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
fn = wrapper.map_shape(settings, geometry_segment.wrapped_data)
eval = wrapper.function_item_evaluator(settings, fn)
e = np.array(eval.evaluate(fn.end()))
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
x = float(e[0, 3]) / unit_scale
y = float(e[1, 3]) / unit_scale
dx = float(e[0, 0])
dy = float(e[1, 0])
file.discard_transaction()
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
design_parameters = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint((x, y)),
@@ -178,22 +159,14 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in
break
if last_segment:
file.begin_transaction()
last_segment_dist_along = (
last_segment.DesignParameters.StartDistAlong + last_segment.DesignParameters.HorizontalLength
)
last_segment_end_gradient = last_segment.DesignParameters.EndGradient
settings = ifcopenshell.geom.settings()
mapped_segments = _map_alignment_vertical_segment(file, last_segment)
geometry_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
fn = wrapper.map_shape(settings, geometry_segment.wrapped_data)
eval = wrapper.function_item_evaluator(settings, fn)
e = np.array(eval.evaluate(fn.end()))
e = _get_segment_endpoint(file, last_segment)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
last_segment_height = float(e[1, 3]) / unit_scale
file.discard_transaction()
design_parameters = file.createIfcAlignmentVerticalSegment(
StartDistAlong=last_segment_dist_along,
HorizontalLength=0.0,
@@ -240,13 +213,4 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in
ifcopenshell.api.nest.assign_object(file, related_objects=[zero_length_curve_segment], relating_object=layout)
if include_referent:
alignment = ifcopenshell.api.alignment.get_alignment(layout)
station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
name = f"{_get_segment_start_point_label(zero_length_curve_segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, 0.0, station, name, zero_length_curve_segment
)
referent.Description = f"Positions zero length segment {zero_length_curve_segment.id()}"
return True
@@ -63,6 +63,12 @@ def create(
alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(),
Name=name,
ObjectPlacement=file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0))
),
),
)
alignment_layouts = []
@@ -80,10 +86,10 @@ def create(
if include_geometry:
_create_geometric_representation(file, alignment)
name = ifcopenshell.util.alignment.station_as_string(file, start_station)
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, 0.0, start_station, name, alignment
)
referent_name = ifcopenshell.util.alignment.station_as_string(file, start_station)
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, 0.0, start_station, referent_name, alignment
)
for layout in alignment_layouts:
_add_zero_length_segment(file, layout)
@@ -53,35 +53,8 @@ def create_layout_segment(
# create the segment and add it to the layout.
segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters)
_add_segment_to_layout(file, layout, segment) # adds to layout and geometric representation
end = _add_segment_to_layout(
file, layout, segment
) # adds to layout and geometric representation (if present, also updates zero length segment position)
# compute the 4x4 matrix at the end of the segment so this information can be
# returned and used when defining the next segment
alignment = ifcopenshell.api.alignment.get_alignment(layout)
curve = ifcopenshell.api.alignment.get_curve(alignment)
if curve:
if layout.is_a("IfcAlignmentHorizontal"):
if curve.is_a("IfcGradientCurve"):
curve = curve.BaseCurve
elif curve.is_a("IfcSegmentedReferenceCurve"):
curve = (
curve.BaseCurve.BaseCurve
) # layout is horizontal and curve is segmented ref ... we want the curve's base curve
elif layout.is_a("IfcAlignmentVertical"):
if curve.is_a("IfcSegmentedReferenceCurve"):
curve = curve.BaseCurve
# the new segment is two from the end... the end segment is zero length
curve_segment = curve.Segments[-2]
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, curve_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
return end
else:
return None
return end
@@ -23,6 +23,7 @@ from ifcopenshell.api.alignment._add_segment_to_curve import _add_segment_to_cur
from ifcopenshell.api.alignment._create_geometric_representation import (
_create_geometric_representation,
)
from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position
def create_representation(
@@ -34,8 +35,13 @@ def create_representation(
This function is intended to be used when a model has only the semantic definition of an alignment
and you want to add the geometric representation.
If the alignments are complete, it is recommended that add_zero_length_segment is called after this method to ensure
the proper structure of the semantic and geometric definitions of the alignment
If the alignments are complete, it is recommended that add_zero_length_segment is called before this method to ensure
the proper structure of the semantic and geometric definitions of the alignment.
It is presumed that the alignment does not have any geometric representation. However, if the alignment has stationing defined,
the referent defining the stationing is not related to the alignment geometry (it can't be because the geometry doesn't exist yet).
When the geometric representation is created, the referent is updated to have an IfcLinearPlacement that references the basis curve geometry.
This function assumes the referent defines the stationing at the start of the alignment, and therefore sets the IfcLinearPlacement.RelativePlacement.Location.DistanceAlong to 0.0.
:param alignment: The alignment to create the representation.
"""
@@ -51,6 +57,40 @@ def create_representation(
layouts = ifcopenshell.api.alignment.get_alignment_layouts(alignment)
for layout in layouts:
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
layout_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
for segment in layout_nest.RelatedObjects:
_add_segment_to_curve(file, segment, curve)
# if the alignment is created without geometry it's stationing referent isn't related to the alignment geometry.
# the stationing referent needs to be updated to have an IfcLinearPlacement that references the basis curve geometry
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
if (
referent_nest
and 0 < len(referent_nest.RelatedObjects)
and referent_nest.RelatedObjects[0].ObjectPlacement
and not referent_nest.RelatedObjects[0].ObjectPlacement.is_a("IfcLinearPlacement")
):
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
if referent_nest.RelatedObjects[0].ObjectPlacement:
if referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.Location:
file.remove(referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.Location)
if referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.RefDirection:
file.remove(referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement.RefDirection)
file.remove(referent_nest.RelatedObjects[0].ObjectPlacement.RelativePlacement)
file.remove(referent_nest.RelatedObjects[0].ObjectPlacement)
lp = file.createIfcLinearPlacement(
RelativePlacement=file.createIfcAxis2PlacementLinear(
Location=file.createIfcPointByDistanceExpression(
DistanceAlong=file.createIfcLengthMeasure(0.0),
OffsetLateral=None,
OffsetVertical=None,
OffsetLongitudinal=None,
BasisCurve=basis_curve,
)
)
)
update_fallback_position(file, lp)
referent_nest.RelatedObjects[0].ObjectPlacement = lp
@@ -0,0 +1,51 @@
# 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.abc import Sequence
from ifcopenshell import entity_instance
import ifcopenshell.api.alignment
from ifcopenshell.api.alignment.get_mapped_segments import _get_curve_segment_count
def get_curve_segment(layout: entity_instance, segment: entity_instance) -> entity_instance:
"""
Returns the IfcCurveSegment associated with the given alignment segment. If the curve segment does not exist, None is returned.
Example:
.. code:: python
horizontal = model.by_type("IfcAlignmentHorizontal")[0]
curve_segment = ifcopenshell.api.alignment.get_curve_segment(horizontal, alignment_segment)
"""
index = 0
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
for related_object in segment_nest.RelatedObjects:
if related_object == segment:
break
n = _get_curve_segment_count(related_object)
index += n
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
if curve and index < len(curve.Segments):
return curve.Segments[index]
else:
return None
@@ -0,0 +1,34 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from ifcopenshell import entity_instance
def get_layout(segment: entity_instance) -> entity_instance:
"""
Retrieves the layout to which an alignment segment belongs.
"""
if not segment.is_a("IfcAlignmentSegment"):
raise TypeError(f"Expected entity type to be IfcAlignmentSegment, instead received {segment.is_a()}")
layout = None
nests = segment.Nests
if nests:
layout = nests[0].RelatingObject
return layout
@@ -22,11 +22,11 @@ from ifcopenshell import entity_instance
def get_referent_nest(file: ifcopenshell.file, alignment: entity_instance) -> entity_instance:
"""
Searches for the IfcRelNest that contains IfcReferent. If one is not found, a empty IfcRelNests is created.
Searches for the IfcRelNest that contains IfcReferent.
:param file:
:param alignment: The IfcAlignment which hosts IfcReferent
:return: Returns the IfcRelNests.
:return: Returns the IfcRelNests or None
"""
if not alignment.is_a("IfcAlignment"):
raise TypeError(f"Expected IfcAlignment, instead received {alignment.is_a()}")
@@ -36,5 +36,4 @@ def get_referent_nest(file: ifcopenshell.file, alignment: entity_instance) -> en
if related_object.is_a("IfcReferent"):
return nest
nest = file.createIfcRelNests(GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=[])
return nest
return None
@@ -0,0 +1,90 @@
# 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 numpy as np
import ifcopenshell
import ifcopenshell.util.placement
from ifcopenshell import entity_instance
def update_end_point(file: ifcopenshell.file, curve: entity_instance):
"""
Updates the IfcGradientCurve.EndPoint and IfcSegmentedReferenceCurve.EndPoint.
If the curve does not have a zero length segment, one is added. The EndPoint is then updated to match the placement of the zero length segment.
:param curve: The gradient curve or segmented reference curve
:return: None
"""
expected_types = ["IfcGradientCurve", "IfcSegmentedReferenceCurve"]
if not curve.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{curve.is_a()}"
)
if not ifcopenshell.api.alignment.has_zero_length_segment(curve):
ifcopenshell.api.alignment.add_zero_length_segment(file, curve)
zero_length_segment = curve.Segments[-1]
if not curve.EndPoint:
if curve.is_a("IfcGradientCurve"):
curve.EndPoint = file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((0.0, 0.0)),
RefDirection=file.createIfcDirection((1.0, 0.0)),
)
else:
curve.EndPoint = file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
RefDirection=file.createIfcDirection((1.0, 0.0, 0.0)),
Axis=file.createIfcDirection((0.0, 0.0, 1.0)),
)
p = np.array(ifcopenshell.util.placement.get_axis2placement(zero_length_segment.Placement))
x = float(p[0, 3])
y = float(p[1, 3])
z = float(p[2, 3])
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
if curve.is_a("IfcGradientCurve"):
curve.EndPoint.Location.Coordinates = (x, y)
if not curve.EndPoint.RefDirection:
curve.EndPoint.RefDirection = file.createIfcDirection((1.0, 0.0))
curve.EndPoint.RefDirection.DirectionRatios = (rx, ry)
else:
curve.EndPoint.Location.Coordinates = (x, y, z)
if not curve.EndPoint.RefDirection:
curve.EndPoint.RefDirection = file.createIfcDirection((1.0, 0.0, 0.0))
if not curve.EndPoint.Axis:
curve.EndPoint.Axis = file.createIfcDirection((0.0, 0.0, 1.0))
curve.EndPoint.RefDirection.DirectionRatios = (rx, ry, rz)
curve.EndPoint.Axis.DirectionRatios = (ax, ay, az)
@@ -34,7 +34,7 @@ def update_fallback_position(file: ifcopenshell.file, lp: entity_instance):
"""
if not lp.CartesianPosition:
lp.CartesianPosition = file.createIfcAxis2Placement3D(Location=file.createIfcCartesianPoint((0.0, 0.0)))
lp.CartesianPosition = file.createIfcAxis2Placement3D(Location=file.createIfcCartesianPoint((0.0, 0.0, 0.0)))
p = np.array(ifcopenshell.util.placement.get_axis2placement(lp.RelativePlacement))
@@ -60,7 +60,7 @@ def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray:
segment_type = segment.is_a().upper()
if not segment_type in supported_segment_types:
raise NotImplementedError(f"Expected entity type 'IFCCURVESEGMENT', got '{segment_type}")
if dist_along > segment.SegmentLength:
if dist_along > abs(segment.SegmentLength.wrappedValue):
raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).")
s = ifcopenshell.geom.settings()
@@ -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