Alignment api update (#6817)

* Some basic COGO survey points functions

* Update alignment api

Includes defining alignment segment by segment, automatic geometry definitions, and automatric stationing and referents

* Fixes bonsai import alignment from csv

* Adds DMS angle conversion functions to COGO api

* Updated per @civilx64 review comments

* Fixes problem with segment representations

* Fixes problem with segment transition codes

* Allows for compound vertical and horizontal curves

* Implements callbacks for referent naming

* Renames angle_from_bearing to bearing2dd for consistency with ifcopenshell.util.geolocation.dms2dd. Removes angle_from_dms because it duplicates dms2dd

* Documents register_referent_name_callback

* Fixes all sorts of problems with Cant/SegRefCurve implementation

* refactor referent unit tests to use a fixture for test setup

* lint with black

---------

Co-authored-by: Scott Lecher <civilx64@gmail.com>
This commit is contained in:
Richard Brice
2025-07-08 10:09:14 -07:00
committed by GitHub
parent ad8d02bfed
commit 40f92d15c3
76 changed files with 3645 additions and 1839 deletions
@@ -17,7 +17,15 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""
Manages alignment layout (business logical) and alignment geometry (geometric representations).
Manages alignment layout (semantic definition) and alignment geometry (geometric definition).
This API is defined in terms of the semantic definition of an alignment. The corresponding geometric definition
is created and maintained automatically. The manditory zero length segment for the semantic and geometric definitions
are automatically created and maintained.
Alignments are created with stationing referents. Each layout segment is assigned a position referent that informs about
the start point of the segment. An example is the point of curvature of a horizontal circular curve. The referent is
nested to the segment representing the circular arc and is named with a indicator of the position and the station, e.g. "P.C. (145+98.32)"
This API does not determine alignment parameters based on rules, such as minimum curve radius as a function of design speed or sight distance.
@@ -25,14 +33,13 @@ This API is under development and subject to code breaking changes in the future
Presently, this API supports:
1. Creating alignments, both horizontal and vertical, using the PI method. Alignment definition can be read from a CSV file.
2. Adding business logic and geometric segments to the end of an alignment
3. Adding and removing the zero length segment at the end of alignments
4. Creating geometric representations from a business logical definition
5. Mapping individual business logical segments to geometric segments (complete for horizontal, missing clothoid for vertical, not implemented for cant)
6. Using curve geometry to determine IfcCurveSegment.Transition transition code.
7. Utility functions for printing business logical and geometric representations, as well as minimal geometry evaluations
2. Creating alignments segment by segment.
3. Automatic creation of geometric definitions (IfcCompositeCurve, IfcGradientCurve, IfcSegmentedReferenceCurve)
4. Automatic definition of stationing
5. Automatic definition of alignment transition point referents
6. Utility functions for printing business logical and geometric representations, as well as minimal geometry evaluations
Future versions of this API will support:
Future versions of this API may support:
1. Defining alignments using the PI method, including transition spirals
2. Updating horizontal curve definitions by revising transition spiral parameters and circular curve radii
3. Updating vertical curve definitions by revising horizontal length of curves
@@ -40,61 +47,59 @@ Future versions of this API will support:
5. Adding a segment at any location along a curve
"""
from .add_segment_to_curve import add_segment_to_curve
from .add_segment_to_layout import add_segment_to_layout
from .add_stationing_to_alignment import add_stationing_to_alignment
from .add_vertical_alignment_by_pi_method import add_vertical_alignment_by_pi_method
from .add_vertical_alignment import add_vertical_alignment
from .add_zero_length_segment import add_zero_length_segment
from .create_alignment_by_pi_method import create_alignment_by_pi_method
from .create_alignment_from_csv import create_alignment_from_csv
from .create_horizontal_alignment_by_pi_method import create_horizontal_alignment_by_pi_method
from .create_geometric_representation import create_geometric_representation
from .create_vertical_alignment_by_pi_method import create_vertical_alignment_by_pi_method
from .add_stationing_referent import add_stationing_referent
from .add_vertical_layout import add_vertical_layout
from .create_layout_segment import create_layout_segment
from .create_alignment import create
from .create_by_pi_method import create_by_pi_method
from .create_from_csv import create_from_csv
from .create_segment_representations import create_segment_representations
from .distance_along_from_station import distance_along_from_station
from .get_alignment import get_alignment
from .get_alignment_station import get_alignment_station
from .get_layout_segments import get_layout_segments
from .get_horizontal_layout import get_horizontal_layout
from .get_vertical_layout import get_vertical_layout
from .get_cant_layout import get_cant_layout
from .get_alignment_layouts import get_alignment_layouts
from .get_axis_subcontext import get_axis_subcontext
from .get_basis_curve import get_basis_curve
from .get_child_alignments import get_child_alignments
from .get_curve import get_curve
from .get_layout_curve import get_layout_curve
from .get_parent_alignment import get_parent_alignment
from .has_zero_length_segment import has_zero_length_segment
from .map_alignment_segments import map_alignment_segments
from .map_alignment_horizontal_segment import map_alignment_horizontal_segment
from .map_alignment_vertical_segment import map_alignment_vertical_segment
from .map_alignment_cant_segment import map_alignment_cant_segment
from .layout_horizontal_alignment_by_pi_method import layout_horizontal_alignment_by_pi_method
from .layout_vertical_alignment_by_pi_method import layout_vertical_alignment_by_pi_method
from .name_segments import name_segments
from .remove_last_segment import remove_last_segment
from .remove_zero_length_segment import remove_zero_length_segment
from .update_curve_segment_transition_code import update_curve_segment_transition_code
from .util import *
from ._get_segment_start_point_label import register_referent_name_callback
__all__ = [
"add_segment_to_curve",
"add_segment_to_layout",
"add_stationing_to_alignment",
"add_vertical_alignment_by_pi_method",
"add_vertical_alignment",
"add_zero_length_segment",
"create_alignment_by_pi_method",
"create_alignment_from_csv",
"create_horizontal_alignment_by_pi_method",
"create_geometric_representation",
"create_vertical_alignment_by_pi_method",
"add_stationing_referent",
"add_vertical_layout",
"create_layout_segment",
"create",
"create_by_pi_method",
"create_from_csv",
"distance_along_from_station",
"get_alignment",
"get_alignment_station",
"get_layout_segments",
"get_horizontal_layout",
"get_vertical_layout",
"get_cant_layout",
"get_alignment_layouts",
"get_axis_subcontext",
"get_basis_curve",
"get_child_alignments",
"get_curve",
"get_layout_curve",
"get_parent_alignment",
"has_zero_length_segment",
"map_alignment_segments",
"map_alignment_horizontal_segment",
"map_alignment_vertical_segment",
"map_alignment_cant_segment",
"layout_horizontal_alignment_by_pi_method",
"layout_vertical_alignment_by_pi_method",
"name_segments",
"remove_last_segment",
"remove_zero_length_segment",
"update_curve_segment_transition_code",
"register_referent_name_callback",
]
@@ -0,0 +1,132 @@
# 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
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import numpy as np
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._update_curve_segment_transition_code import _update_curve_segment_transition_code
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
from ifcopenshell.api.alignment._map_alignment_cant_segment import _map_alignment_cant_segment
def _add_curve_segment_to_composite_curve(
file: ifcopenshell.file, curve_segment: entity_instance, composite_curve: entity_instance
):
if 0 < len(curve_segment.UsingCurves):
raise TypeError("IfcCurveSegment cannot belong to other curves")
settings = ifcopenshell.geom.settings()
if composite_curve.Segments == None or 0 == len(composite_curve.Segments):
# this is the first segment so just add it
if composite_curve.Segments == None:
composite_curve.Segments = []
# the last segment is always discontinuous
curve_segment.Transition = "DISCONTINUOUS"
composite_curve.Segments += (curve_segment,)
assert len(curve_segment.UsingCurves) == 1
else:
# get the segment before the zero length segment
prev_segment = (
composite_curve.Segments[-2]
if composite_curve.Segments != None and 1 < len(composite_curve.Segments)
else None
)
curve_segment.Transition = "CONTINUOUS"
# must add the new segment to the curve before updating the transition code
# add the new segment before the zero length segment
zero_length_segment = composite_curve.Segments[-1]
if prev_segment:
segments = composite_curve.Segments[0:-1]
segments += (
curve_segment,
zero_length_segment,
)
composite_curve.Segments = []
composite_curve.Segments += segments
_update_curve_segment_transition_code(prev_segment, curve_segment)
else:
composite_curve.Segments = (curve_segment, 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])
# 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)
def _add_segment_to_curve(file: ifcopenshell.file, segment: entity_instance, curve: entity_instance) -> 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
that preceeds the new segment is updated.
:param segment: The segment to be added to the curve
:param curve: The representation curve receiving the segment
:return: None
"""
expected_types = ["IfcAlignmentSegment"]
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()}"
)
if 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"):
raise TypeError(f"Expected to see IfcGradientCurve, instead received '{curve.is_a()}'.")
elif 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"
if not curve.is_a(expected_type):
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)
else:
assert False
for mapped_segment in mapped_segments:
if mapped_segment:
_add_curve_segment_to_composite_curve(file, mapped_segment, curve)
@@ -0,0 +1,198 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
from ifcopenshell import ifcopenshell_wrapper
import numpy as np
import math
from ifcopenshell import entity_instance
from typing import Sequence
from ifcopenshell.api.alignment._add_segment_to_curve import _add_segment_to_curve
from ifcopenshell.api.alignment._get_mapped_segments import _get_mapped_segments
from ifcopenshell.api.alignment._get_segment_start_point_label import _get_segment_start_point_label
def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, segment: entity_instance) -> 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.
:param layout: The layout alignment
:param segment: The segment to be appended
:return: None
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if 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)
# add the new segment to the layout
ifcopenshell.api.nest.assign_object(file, related_objects=[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)
# add the new segment to the geometric representation curve
_add_segment_to_curve(file, 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_station(file, alignment)
station = start_station + dist_along
# update the zero length layout segment
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
zero_length_segment = layout.IsNestedBy[0].RelatedObjects[-1]
# DesignParameters.StartPoint for IfcAlignmentHorizontalSegment is automatically updated when the
# geometric representation is updated because the semantic and geometric data use the same IfcPoint.
# This is not the case of IfcAlignmentVerticalSegment and IfcAlignmentCantSegment. For these
# segment types, the design parameters of the zero length segment must be updated explicitly.
if zero_length_segment.DesignParameters.is_a(
"IfcAlignmentVerticalSegment"
) or zero_length_segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
# get the geometric representation for the new segment
mapped_segments = _get_mapped_segments(file, 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("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.IsNestedBy[0].RelatedObjects[0]
end_referent.Name = f"{_get_segment_start_point_label(zero_length_segment,None)} ({ifcopenshell.util.stationing.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)
# 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 = layout.IsNestedBy[0].RelatedObjects[-3] if 2 < len(layout.IsNestedBy[0].RelatedObjects) else None
name = f"{_get_segment_start_point_label(prev_segment,segment)} ({ifcopenshell.util.stationing.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, segment, basis_curve=basis_curve, distance_along=dist_along, station=station, name=name
)
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)
stationing_referent = alignment.IsNestedBy[0].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)
@@ -0,0 +1,144 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._get_segment_start_point_label import _get_segment_start_point_label
def _add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance) -> None:
"""
Adds a zero length segment to the end of a layout. Also adds a zero length segment to the end of the corresponding geometric curve.
If the layout already has a zero length segment, nothing is changed
:param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
:return: None
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected layout type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if ifcopenshell.api.alignment.has_zero_length_segment(layout):
return
segment = None
if layout.is_a("IfcAlignmentHorizontal"):
design_parameters = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint(
(0.0, 0.0)
), # this is a little problematic. need to know the end point and tangent
StartDirection=0.0, # of the previous segment, which requires geometry mapping
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=0.0,
PredefinedType="LINE",
)
segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters)
elif layout.is_a("IfcAlignmentVertical"):
last_segment_dist_along = 0.0
last_segment_end_gradient = 0.0
for rel in layout.IsNestedBy:
if 0 < len(rel.RelatedObjects):
last_segment = rel.RelatedObjects[1]
last_segment_dist_along = (
last_segment.DesignParameters.StartDistAlong + last_segment.DesignParameters.HorizontalLength
)
last_segment_end_gradient = last_segment.DesignParameters.EndGradient
design_parameters = file.createIfcAlignmentVerticalSegment(
StartDistAlong=last_segment_dist_along,
HorizontalLength=0.0,
StartHeight=0.0,
StartGradient=last_segment_end_gradient,
EndGradient=last_segment_end_gradient,
PredefinedType="CONSTANTGRADIENT",
)
segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters)
elif layout.is_a("IfcAlignmentCant"):
last_segment_dist_along = 0.0
last_segment_cant_left = 0.0
last_segment_cant_right = 0.0
for rel in layout.IsNestedBy:
if 0 < len(rel.RelatedObjects):
last_segment = rel.RelatedObjects[1]
last_segment_dist_along = (
last_segment.DesignParameters.StartDistAlong + last_segment.DesignParameters.HorizontalLength
)
last_segment_cant_left = (
last_segment.DesignParameters.EndCantLeft
if last_segment.DesignParameters.EndCantLeft != None
else last_segment.DesignParameters.StartCantLeft
)
last_segment_cant_right = (
last_segment.DesignParameters.EndCantRight
if last_segment.DesignParameters.EndCantRight != None
else last_segment.DesignParameters.StartCantRight
)
design_parameters = file.createIfcAlignmentCantSegment(
StartDistAlong=last_segment_dist_along,
HorizontalLength=0.0,
StartCantLeft=last_segment_cant_left,
StartCantRight=last_segment_cant_right,
PredefinedType="CONSTANTCANT",
)
segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters)
ifcopenshell.api.nest.assign_object(file, related_objects=[segment], relating_object=layout)
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
has_zero_length_segment = False
if curve.Segments != None and 0 < len(curve.Segments):
last_segment = curve.Segments[-1]
has_zero_length_segment = (
True
if last_segment.Transition == "DISCONTINUOUS" and last_segment.SegmentLength.wrappedValue == 0.0
else False
)
if not has_zero_length_segment:
parent_curve = file.createIfcLine(
Pnt=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))),
Dir=file.createIfcVector(
Orientation=file.createIfcDirection(DirectionRatios=((1.0, 0.0))),
Magnitude=1.0,
),
)
curve_segment = file.createIfcCurveSegment(
Transition="DISCONTINUOUS",
Placement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((0.0, 0.0)),
RefDirection=file.createIfcDirection((1.0, 0.0)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(0.0),
ParentCurve=parent_curve,
)
curve.Segments = [
curve_segment,
]
name = f"{_get_segment_start_point_label(segment,None)} {ifcopenshell.util.stationing.station_as_string(file,0.0)}"
ifcopenshell.api.alignment.add_stationing_referent(file, segment, curve, 0.0, 0.0, name=name)
@@ -25,17 +25,17 @@ import math
from collections.abc import Sequence
def create_geometric_representation(file: ifcopenshell.file, alignment: entity_instance) -> None:
def _create_geometric_representation(file: ifcopenshell.file, alignment: entity_instance) -> None:
"""
Create geometric representation for the alignment.
Create geometric representation for the alignment and its nested layouts.
There are 5 different cases:
There are 5 different cases (the IfcCurve created is indicated):
1) Horizontal only
2) Horizontal + Vertical
3) Horizontal + Vertical + Cant
4) Vertical only (this occurs when horizontal is reused from a parent alignment)
5) Vertical + Cant (this occurs when horizontal is reused from a parent alignment)
1) Horizontal only -> IfcCompositeCurve
2) Horizontal + Vertical -> IfcCompositeCurve and IfcGradientCurve
3) Horizontal + Vertical + Cant -> IfcCompositeCurve and IfcSegmentedReferentCurve
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
:param alignment: The alignment for which the representation is being created
:return: None
@@ -43,7 +43,7 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
expected_type = "IfcAlignment"
if not alignment.is_a(expected_type):
raise TypeError("Expected '{expected_type}' but got '{alignment.is_a()}'")
raise TypeError(f"Expected {expected_type} but got {alignment.is_a()}")
placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
@@ -60,11 +60,8 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
if len(layouts) == 1 and len(children) == 0:
assert layouts[0].is_a("IfcAlignmentHorizontal")
# Horizontal only - IFC CT 4.1.7.1.1.1
ifcopenshell.api.alignment.add_zero_length_segment(file, layouts[0])
composite_curve = file.createIfcCompositeCurve()
ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve)
representation = file.create_entity(
type="IfcShapeRepresentation",
composite_curve = file.createIfcCompositeCurve(Segments=[], SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve2D",
@@ -75,12 +72,8 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
# Horizontal and Vertical - IFC CT 4.1.7.1.1.1
assert layouts[0].is_a("IfcAlignmentHorizontal")
assert layouts[1].is_a("IfcAlignmentVertical")
ifcopenshell.api.alignment.add_zero_length_segment(file, layouts[0])
ifcopenshell.api.alignment.add_zero_length_segment(file, layouts[1])
composite_curve = file.createIfcCompositeCurve()
ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve)
representation = file.create_entity(
type="IfcShapeRepresentation",
composite_curve = file.createIfcCompositeCurve(Segments=[], SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="FootPrint",
RepresentationType="Curve2D",
@@ -88,10 +81,8 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
)
ifcopenshell.api.geometry.assign_representation(file, alignment, representation)
gradient_curve = file.createIfcGradientCurve(BaseCurve=composite_curve)
ifcopenshell.api.alignment.map_alignment_segments(file, layouts[1], gradient_curve)
representation = file.create_entity(
type="IfcShapeRepresentation",
gradient_curve = file.createIfcGradientCurve(Segments=[], BaseCurve=composite_curve, SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
@@ -103,13 +94,8 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
assert layouts[0].is_a("IfcAlignmentHorizontal")
assert layouts[1].is_a("IfcAlignmentVertical")
assert layouts[2].is_a("IfcAlignmentCant")
ifcopenshell.api.alignment.add_zero_length_segment(file, layouts[0])
ifcopenshell.api.alignment.add_zero_length_segment(file, layouts[1])
ifcopenshell.api.alignment.add_zero_length_segment(file, layouts[2])
composite_curve = file.createIfcCompositeCurve()
ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve)
representation = file.create_entity(
type="IfcShapeRepresentation",
composite_curve = file.createIfcCompositeCurve(Segments=[], SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="FootPrint",
RepresentationType="Curve2D",
@@ -117,10 +103,10 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
)
ifcopenshell.api.geometry.assign_representation(file, alignment, representation)
gradient_curve = file.createIfcGradientCurve(BaseCurve=composite_curve)
ifcopenshell.api.alignment.map_alignment_segments(file, layouts[1], gradient_curve)
segmented_reference_curve = file.createIfcSegmentedReferenceCurve(BaseCurve=gradient_curve)
ifcopenshell.api.alignment.map_alignment_segments(file, layouts[2], segmented_reference_curve)
gradient_curve = file.createIfcGradientCurve(Segments=[], BaseCurve=composite_curve, SelfIntersect=False)
segmented_reference_curve = file.createIfcSegmentedReferenceCurve(
Segments=[], BaseCurve=gradient_curve, SelfIntersect=False
)
representation = file.create_entity(
type="IfcShapeRepresentation",
ContextOfItems=axis_geom_subcontext,
@@ -132,11 +118,8 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
else:
# Reusing Horizontal - CT 4.1.4.4.1.2
# Create a representation on the parent alignment
ifcopenshell.api.alignment.add_zero_length_segment(file, layouts[0])
composite_curve = file.createIfcCompositeCurve()
ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve)
representation = file.create_entity(
type="IfcShapeRepresentation",
composite_curve = file.createIfcCompositeCurve(Segments=[], SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="FootPrint",
RepresentationType="Curve2D",
@@ -149,12 +132,9 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
child_layouts = ifcopenshell.api.alignment.get_alignment_layouts(child_alignment)
if len(child_layouts) == 1:
assert child_layouts[0].is_a("IfcAlignmentVertical")
ifcopenshell.api.alignment.add_zero_length_segment(file, child_layouts[0])
base_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
gradient_curve = file.createIfcGradientCurve(BaseCurve=base_curve)
ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[0], gradient_curve)
representation = file.create_entity(
type="IfcShapeRepresentation",
gradient_curve = file.createIfcGradientCurve(Segments=[], BaseCurve=base_curve, SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
@@ -164,15 +144,12 @@ def create_geometric_representation(file: ifcopenshell.file, alignment: entity_i
elif len(child_layouts) == 2:
assert child_layouts[0].is_a("IfcAlignmentVertical")
assert child_layouts[1].is_a("IfcAlignmentCant")
ifcopenshell.api.alignment.add_zero_length_segment(file, child_layouts[0])
ifcopenshell.api.alignment.add_zero_length_segment(file, child_layouts[1])
base_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
gradient_curve = file.createIfcGradientCurve(BaseCurve=base_curve)
ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[0], gradient_curve)
segmented_reference_curve = file.createIfcSegmentedReferenceCurve(BaseCurve=gradient_curve)
ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[1], segmented_reference_curve)
representation = file.create_entity(
type="IfcShapeRepresentation",
gradient_curve = file.createIfcGradientCurve(Segments=[], BaseCurve=base_curve, SelfIntersect=False)
segmented_reference_curve = file.createIfcSegmentedReferenceCurve(
Segments=[], BaseCurve=gradient_curve, SelfIntersect=False
)
representation = file.creatIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
@@ -0,0 +1,60 @@
# 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
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
from typing import Sequence
def __get_curve_segment_count(segment: entity_instance) -> int:
"""
returns the number of IfcCurveSegment that an IfcAlignmentSegment maps to.
generally this is a 1 to 1 mapping, with helmert curve being the exception
"""
if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
return 2 if segment.DesignParameters.PredefinedType == "HELMERTCURVE" else 1
elif segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
return 1
elif segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
return 2 if segment.DesignParameters.PredefinedType == "HELMERTCURVE" else 1
def _get_mapped_segments(file: ifcopenshell.file, layout_segment: entity_instance) -> Sequence[entity_instance]:
"""
Finds the IfcCurveSegment related to segment
"""
expected_type = "IfcAlignmentSegment"
if not layout_segment.is_a(expected_type):
raise TypeError(f"Expected to see type '{expected_type}', instead received '{layout_segment.is_a()}'.")
layout = layout_segment.Nests[0].RelatingObject
alignment = ifcopenshell.api.alignment.get_alignment(layout)
curve = ifcopenshell.api.alignment.get_curve(alignment)
index = 0
for seg in layout.IsNestedBy[0].RelatedObjects:
index += __get_curve_segment_count(seg)
if seg == layout_segment:
break
segment_count = __get_curve_segment_count(layout_segment)
if segment_count == 1:
return (curve.Segments[index - segment_count], None)
else:
return (curve.Segments[index - segment_count], curve.Segment[index])
@@ -0,0 +1,314 @@
# 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
from ifcopenshell import entity_instance
from typing import Sequence
_horizontal_callback = None
_vertical_callback = None
_cant_callback = None
def register_referent_name_callback(horizontal=None, vertical=None, cant=None):
"""
Referents are automatically created at the start of each horizontal, vertical, and cant segment.
The referents represent key points in the alignment layout such as Point of Curvature, Point of Tangent, and others.
Different juristicions use different naming systems for these key points.
The referent name callback functions provide a customizable method for naming these referents. If a callback is registered,
it is called when creating the referent name, otherwise the default naming is used.
The callback function signature is
def mycallback(prev_segment : entity_instance, segment : entity_instance) -> str:
The callback function returns a string that is used in the referent name for the referent at the start of `segment`.
The callback must accomodate the following cases:
* prev_segment = None and segment != None - this indicates the last segment so the "End of Alignment" name is returned
* prev_segment != None and segment == None - this indicates the first segment so the "Beginning of Alignment" name is returned
* prev_segment != None and segment != None - this indicates an intermediate segment so a name representitive of the transition is returned
Setting any or all of the callbacks to None causes the default naming to be used.
"""
global _horizontal_callback
_horizontal_callback = horizontal
global _vertical_callback
_vertical_callback = vertical
global _cant_callback
_cant_callback = cant
def _horizontal_label(prev_segment: entity_instance, segment: entity_instance) -> str:
if prev_segment == None and segment != None:
label = "P.O.B."
elif prev_segment != None and segment == None:
label = "P.O.E."
else:
lookup_table = {
"BLOSSCURVE": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"CIRCULARARC": {
"BLOSSCURVE": "C.S.",
"CIRCULARARC": "P.C.C.",
"CLOTHOID": "C.S.",
"COSINECURVE": "C.S.",
"CUBIC": "C.S.",
"HELMERTCURVE": "C.S.",
"LINE": "P.T.",
"SINECURVE": "C.S.",
"VIENNESEBEND": "C.S.",
},
"CLOTHOID": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"COSINECURVE": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"CUBIC": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"HELMERTCURVE": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"LINE": {
"BLOSSCURVE": "T.S.",
"CIRCULARARC": "P.C.",
"CLOTHOID": "T.S.",
"COSINECURVE": "T.S.",
"CUBIC": "T.S.",
"HELMERTCURVE": "T.S.",
"LINE": "P.I.",
"SINECURVE": "T.S.",
"VIENNESEBEND": "T.S.",
},
"SINECURVE": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"VIENNESEBEND": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
}
label = lookup_table[prev_segment.DesignParameters.PredefinedType][segment.DesignParameters.PredefinedType]
return label
def _vertical_label(prev_segment: entity_instance, segment: entity_instance) -> str:
if prev_segment == None and segment != None:
label = "V.P.O.B."
elif prev_segment != None and segment == None:
label = "V.P.O.E."
else:
lookup_table = {
"CIRCULARARC": {"CIRCULARARC": "xx", "CLOTHOID": "xx", "CONSTANTGRADIENT": "xx", "PARABOLICARC": "xx"},
"CLOTHOID": {"CIRCULARARC": "xx", "CLOTHOID": "xx", "CONSTANTGRADIENT": "xx", "PARABOLICARC": "xx"},
"CONSTANTGRADIENT": {
"CIRCULARARC": "xx",
"CLOTHOID": "xx",
"CONSTANTGRADIENT": "P.V.I",
"PARABOLICARC": "P.V.C.",
},
"PARABOLICARC": {
"CIRCULARARC": "xx",
"CLOTHOID": "xx",
"CONSTANTGRADIENT": "P.V.T.",
"PARABOLICARC": "V.C.C.",
},
}
label = lookup_table[prev_segment.DesignParameters.PredefinedType][segment.DesignParameters.PredefinedType]
return label
def _cant_label(prev_segment: entity_instance, segment: entity_instance) -> str:
if prev_segment == None and segment != None:
label = "C.P.O.B."
elif prev_segment != None and segment == None:
label = "C.P.O.E."
else:
lookup_table = {
"BLOSSCURVE": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"CONSTANTCANT": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"COSINECURVE": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"HELMERTCURVE": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"LINEARTRANSITION": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"SINECURVE": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"VIENNESEBEND": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
}
label = lookup_table[prev_segment.DesignParameters.PredefinedType][segment.DesignParameters.PredefinedType]
return label
def _get_segment_start_point_label(prev_segment: entity_instance, segment: entity_instance) -> str:
"""
Returns the label for the start point of a segment. Typically used in the name of an IfcReferent
"""
if prev_segment != None and segment != None and prev_segment.is_a() != segment.is_a():
raise TypeError(
f"Expected entity type to be the same type, instead received {prev_segment.is_a()} and {segment.is_a()}"
)
expected_types = ["IfcAlignmentHorizontalSegment", "IfcAlignmentVerticalSegment", "IfcAlignmentCantSegment"]
if prev_segment != None and not prev_segment.DesignParameters.is_a() in expected_types:
raise TypeError(
f"Expected prev_segment.DesignParameters type to be one of {[_ for _ in expected_types]}, instead received {prev_segment.DesignParameters.is_a()}"
)
if segment != None and not segment.DesignParameters.is_a() in expected_types:
raise TypeError(
f"Expected segment.DesignParameters type to be one of {[_ for _ in expected_types]}, instead received {segment.DesignParameters.is_a()}"
)
s = segment if segment != None else prev_segment
if s.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
global _horizontal_callback
if _horizontal_callback:
label = _horizontal_callback(prev_segment, segment)
else:
label = _horizontal_label(prev_segment, segment)
elif s.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
global _vertical_callback
if _vertical_callback:
label = _vertical_callback(prev_segment, segment)
else:
label = _vertical_label(prev_segment, segment)
elif s.DesignParameters.is_a("IfcAlignmentCantSegment"):
global _cant_callback
if _cant_callback:
label = _cant_callback(prev_segment, segment)
else:
label = _cant_label(prev_segment, segment)
return label
@@ -380,7 +380,7 @@ def _map_viennese_bend(
raise NotImplementedError("VIENNESEBEND not implemented")
def map_alignment_cant_segment(
def _map_alignment_cant_segment(
file: ifcopenshell.file, segment: entity_instance, rail_head_distance: float
) -> Sequence[entity_instance]:
"""
@@ -401,7 +401,7 @@ def _map_viennese_bend(file: ifcopenshell.file, design_parameters: entity_instan
raise NotImplementedError("VIENNESEBEND not implemented")
def map_alignment_horizontal_segment(file: ifcopenshell.file, segment: entity_instance) -> Sequence[entity_instance]:
def __map_alignment_horizontal_segment(file: ifcopenshell.file, segment: entity_instance) -> Sequence[entity_instance]:
"""
Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentHorizontalSegment business logic entity instance.
A pair of entities is returned because a single business logic segment of type HELMERTCURVE maps to two representaiton entities.
@@ -189,7 +189,7 @@ def _map_clothoid(file: ifcopenshell.file, design_parameters: entity_instance) -
raise NotImplementedError("mapping for IfcVerticalSegment.CLOTHOID not implemented")
def map_alignment_vertical_segment(file: ifcopenshell.file, segment: entity_instance) -> Sequence[entity_instance]:
def _map_alignment_vertical_segment(file: ifcopenshell.file, segment: entity_instance) -> Sequence[entity_instance]:
"""
Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentVerticalSegment business logic entity instance.
A pair of entities is returned for consistency with map_alignment_horizontal_segment and map_alignment_cant_segment.
@@ -25,7 +25,7 @@ import numpy as np
import math
def update_curve_segment_transition_code(prev_segment: entity_instance, segment: entity_instance) -> None:
def _update_curve_segment_transition_code(prev_segment: entity_instance, segment: entity_instance) -> None:
"""
Updates IfcCurveSegment.Transition of prev_segment based on a comparison of
the position, ref. direction, and curvature at the end of the prev_segment and the start of segment.
@@ -62,9 +62,9 @@ def update_curve_segment_transition_code(prev_segment: entity_instance, segment:
s = segment_evaluator.evaluate(segment_fn.start())
start = np.array(s)
same_position = True if np.allclose(end[:3], start[:3]) else False
same_gradient = True if np.allclose(end[:0], start[:0]) else False
same_curvature = True if np.allclose(end[3:], start[3:]) else False
same_position = True if np.allclose(end[:3, 3], start[:3, 3]) else False
same_gradient = True if np.allclose(end[:3, 0], start[:3, 0]) else False
same_curvature = True if np.allclose(end[3:, :3], start[3:, :3]) else False
if same_position:
prev_segment.Transition = "CONTINUOUS"
@@ -1,76 +0,0 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.geom
from ifcopenshell import entity_instance
def add_segment_to_curve(file: ifcopenshell.file, segment: entity_instance, composite_curve: entity_instance) -> None:
"""
Adds a segment to a composite curve. The segment must not belong to another composite curve (len(segment.UsingCurves) == 0).
If the composite curve does not have any segments, the segment is simply appended to the curve.
If the composite curve has segments, the position, ref. direction, and curvature at the end of the last segment is
compared to the position, ref. direction and curvature at the start of the new segment. The IfcCurveSegment.Transition of the last curve segment is updated.
:param segment: The segment to be added to the curve
:param composite_curve: The curve receiving the segment
:return: None
"""
expected_type = "IfcCurveSegment"
if not segment.is_a(expected_type):
raise TypeError(f"Expected to see '{expected_type}', instead received '{segment.is_a()}'.")
if 0 < len(segment.UsingCurves):
raise TypeError("IfcCurveSegment cannot belong to other curves")
expected_type = "IfcCompositeCurve"
if not composite_curve.is_a(expected_type):
raise TypeError(f"Expected to see '{expected_type}', instead received '{composite_curve.is_a()}'.")
settings = ifcopenshell.geom.settings()
if composite_curve.Segments == None or 0 == len(composite_curve.Segments):
# this is the first segment so just add it
if composite_curve.Segments == None:
composite_curve.Segments = []
# the last segment is always discontinuous
segment.Transition = "DISCONTINUOUS"
composite_curve.Segments += (segment,)
assert len(segment.UsingCurves) == 1
else:
zero_length_segment = (
ifcopenshell.api.alignment.remove_zero_length_segment(file, composite_curve)
if ifcopenshell.api.alignment.has_zero_length_segment(composite_curve)
else None
)
prev_segment = composite_curve.Segments[-1]
# the last segment is always discontinuous
segment.Transition = "DISCONTINUOUS"
# must add the new segment to the curve before updating the transition code
composite_curve.Segments += (segment,)
ifcopenshell.api.alignment.update_curve_segment_transition_code(prev_segment, segment)
if zero_length_segment:
ifcopenshell.api.alignment.add_segment_to_curve(zero_length_segment, composite_curve)
@@ -1,51 +0,0 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
from ifcopenshell import entity_instance
def add_segment_to_layout(file: ifcopenshell.file, alignment: entity_instance, segment: entity_instance) -> None:
"""
Adds a segment to a layout alignment (horizontal, vertical, or cant)
:param alignment: The alignment
:param segment: The segment to be appended
:return: None
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not alignment.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{alignment.is_a()}"
)
if not (segment.is_a("IfcAlignmentSegment")):
raise TypeError(f"Expected to see IfcAlignmentSegment, instead received '{segment.is_a()}.")
zero_length_segment = (
ifcopenshell.api.alignment.remove_zero_length_segment(file, alignment)
if ifcopenshell.api.alignment.has_zero_length_segment(alignment)
else None
)
ifcopenshell.api.nest.assign_object(file, related_objects=[segment], relating_object=alignment)
if zero_length_segment:
ifcopenshell.api.nest.assign_object(file, related_objects=[zero_length_segment], relating_object=alignment)
@@ -0,0 +1,137 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
import ifcopenshell.api.pset
import ifcopenshell.util.stationing
import ifcopenshell.guid
from ifcopenshell import entity_instance
from ifcopenshell import ifcopenshell_wrapper
import numpy as np
def add_stationing_referent(
file: ifcopenshell.file,
element: entity_instance,
basis_curve: entity_instance,
distance_along: float,
station: float,
name: str,
) -> entity_instance:
"""
Adds an IfcReferent to the element with the Pset_Stationing property set.
If element is an IfcAlignment, IfcReferent.PredefinedType is set to "STATION", otherwise "POSITION"
:param element: the element to receive the referent, expected to be an IfcAlignment or IfcAlignmentSegment
:param basis_curve: the basis curve for positining
:param distance_along: distance along the basis curve
:param station: station value
:param name: name to assign to IfcReferent.Name, typically a stringized version of the station value
:return: referent
Example:
.. code:: python
alignment = model.by_type("IfcAlignment")[0]
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
ifcopenshell.api.alignment.add_stationing_referent(model,entity=alignment,basis_curve=basis_curve,distance_along=0.0,station=100.0)
"""
object_placement = None
representation = None
if basis_curve:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
settings = ifcopenshell.geom.settings()
fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data)
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])
object_placement = file.createIfcLinearPlacement(
RelativePlacement=file.createIfcAxis2PlacementLinear(
Location=file.createIfcPointByDistanceExpression(
DistanceAlong=file.createIfcLengthMeasure(distance_along),
OffsetLateral=None,
OffsetVertical=None,
OffsetLongitudinal=None,
BasisCurve=basis_curve,
)
),
CartesianPosition=file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint(((x, y, z))),
Axis=file.createIfcDirection((ax, ay, az)),
RefDirection=file.createIfcDirection((rx, ry, rz)),
),
)
# this commented out code is what you would do to add a geometric representation of the referent
# the example is a circle. a better way would be to pass a representation into the function
# representation = file.create_entity(
# name="IfcCircle",
# position=file.createIfcAxis2Placement2D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)),
# radius=1.0)
# )
# create referent for the station
predefined_type = "STATION" if element.is_a("IfcAlignment") else "POSITION"
referent = file.createIfcReferent(
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=name,
Description=None,
ObjectType=None,
ObjectPlacement=object_placement,
Representation=representation,
PredefinedType=predefined_type,
)
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": station})
ifcopenshell.api.nest.assign_object(file, related_objects=[referent], relating_object=element)
alignment = element
if element.is_a("IfcAlignmentSegment"):
layout = element.Nests[0].RelatingObject
alignment = ifcopenshell.api.alignment.get_alignment(layout)
if len(alignment.Positions) == 0:
rel_positions = file.createIfcRelPositions(
GlobalId=ifcopenshell.guid.new(),
RelatingPositioningElement=alignment,
RelatedProducts=[
referent,
],
)
else:
alignment.Positions[0].RelatedProducts += (referent,)
return referent
@@ -1,83 +0,0 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
import ifcopenshell.api.pset
import ifcopenshell.util.stationing
import ifcopenshell.guid
from ifcopenshell import entity_instance
def add_stationing_to_alignment(
file: ifcopenshell.file, alignment: entity_instance, start_station: float, plus_seperator=3, accuracy=3
) -> None:
"""
Adds stationing to an alignment by creating an IfcReferent with the Pset_Stationing property set to establish the stationing at the start of the alignment.
Note - this function assumes the stationing has not been previously defined
:param alignment: the alignment to be stationed
:param start_station: station value at the start of the alignment
:return: None
Example:
.. code:: python
alignment = model.by_type("IfcAlignment")[0]
ifcopenshell.api.alignment.add_stationing_to_alignment(model,alignment=alignment,start_station=100.0)
"""
# this commented out code is what you would do to add a geometric representation of the referent
# the example is a circle. a better way would be to pass a representation into the function
object_placement = None
representation = None
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
if basis_curve:
object_placement = file.createIfcLinearPlacement(
RelativePlacement=file.createIfcAxis2PlacementLinear(
Location=file.createIfcPointByDistanceExpression(
DistanceAlong=file.createIfcLengthMeasure(0.0),
OffsetLateral=None,
OffsetVertical=None,
OffsetLongitudinal=None,
BasisCurve=basis_curve,
)
),
CartesianPosition=None,
)
# representation = file.create_entity(
# name="IfcCircle",
# position=file.createIfcAxis2Placement2D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)),
# radius=1.0)
# )
# create referent for start station
start_referent = file.createIfcReferent(
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=ifcopenshell.util.stationing.station_as_string(start_station, plus_seperator, accuracy),
Description=None,
ObjectType=None,
ObjectPlacement=object_placement,
Representation=representation,
PredefinedType="STATION",
)
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=start_referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": start_station})
ifcopenshell.api.nest.assign_object(file, related_objects=[start_referent], relating_object=alignment)
@@ -1,199 +0,0 @@
# 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
import ifcopenshell.api.aggregate
import ifcopenshell.api.alignment
import ifcopenshell.api.geometry
import ifcopenshell.api.nest
import ifcopenshell.api.root
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.stationing
import ifcopenshell.api
from ifcopenshell import entity_instance
def _move_vertical_to_child_alignment(
file: ifcopenshell.file, parent_alignment: entity_instance, vertical_alignment: entity_instance
):
"""
Creates a new child alignment and aggregates it to the parent alignment. Moves the vertical alignment from the parent
alignment to the child alignment. Also moves the "Axis/Curve3D" representation to the child alignment, if present.
This function supports the transition of vertical alignment between CT 4.1.4.4.1.1 and 4.1.4.4.1.2 because a subsequent
vertical alignment is being added and the Alignment Layout - Reusing Horizontal Layout concept applies.
"""
# unhook the vertical alignment from the parent alignment
ifcopenshell.api.nest.unassign_object(file, related_objects=[vertical_alignment])
# create the child alignment
child_alignment = ifcopenshell.api.root.create_entity(
file, ifc_class="IfcAlignment", name=f"Child of {parent_alignment.Name}"
)
# nest the vertical alignment onto the child alignment
ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_alignment], relating_object=child_alignment)
# aggreage the child alignment to the parent alignment
ifcopenshell.api.aggregate.assign_object(file, products=[child_alignment], relating_object=parent_alignment)
# if the parent alignment has a representation, move the Axis/Curve3D represention to the child alignment
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
if base_curve:
representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment)
for representation in representations:
if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D":
ifcopenshell.api.geometry.unassign_representation(file, parent_alignment, representation)
ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation)
break
def add_vertical_alignment(
file: ifcopenshell.file, parent_alignment: entity_instance, vertical_alignment: entity_instance
) -> None:
"""
Adds a vertical alignment to a previously created alignment.
If this is the first vertical alignment assigned to the parent_alignment the IFC CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant
is followed. If this is the second or subsequent vertical alignment assigned to the parent_alignment the
IFC CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout is followed.
When the second vertical alignment is added, the structure of the IFC model must transition from one concept template to the other.
Specifically, the following occurs:
1) The first child IfcAlignment is created and is IfcRelAggregates with the parent alignment.
2) The first vertical alignment is unassigned from the IfcRelNests of the parent alignment and assigned to the new child alignment IfcRelNests
3) A second child IfcAlignment is created ant is is IfcRelAggregates with the parent alignment.
4) The vertical_alignment is assigned to the second child alignment
For the third and subsequent vertical alignments, a new child alignment is created and aggregated to the parent alignment and an IfcAlignmentVertical is created
from vpoints and lengths and assigned to the new child alignment.
If the parent_alignment has a geometric representation, a geometric representation will be created for the vertical alignment.
:param parent_alignment: The parent alignment
:param vertical_alignment: The vertical alignment to be added
:return: None
"""
# get all the child alignments under alignment
child_alignments = [
c for c in ifcopenshell.util.element.get_decomposition(parent_alignment) if c.is_a("IfcAlignment")
]
# Get all the IfcAlignmentVertical that are nesting alignment (there should be 0 or 1)
# if 0, alignment is just horizontal and we are adding the first vertical so it will nest to the alignment,
# or there are multiple vertical and they nest to the aggregated child alignments
# if 1, there is one vertical alignments. Move it to a child alignment
vertical_alignments_nesting_alignment = [
c for c in ifcopenshell.util.element.get_components(parent_alignment) if c.is_a("IfcAlignmentVertical")
]
# move the vertical alignment to a child alignment because there is going to be more than one vertical
assert len(vertical_alignments_nesting_alignment) == 0 or len(vertical_alignments_nesting_alignment) == 1
for vertical_alignment_nesting_alignment in vertical_alignments_nesting_alignment:
_move_vertical_to_child_alignment(file, parent_alignment, vertical_alignment_nesting_alignment)
if len(child_alignments) == 0 and len(vertical_alignments_nesting_alignment) == 0:
# this is the first vertical alignment so nest it into the parent alignment (IFC CT 4.1.4.4.1.1)
ifcopenshell.api.nest.assign_object(
file, related_objects=[vertical_alignment], relating_object=parent_alignment
)
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
if base_curve:
# the parent alignment has a Representation so create a representation for the vertical
gradient_curve = file.create_entity(
type="IfcGradientCurve", Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None
)
# using the business logic definition of vertical_alignment, create the curve segments and assign to gradient_curve
ifcopenshell.api.alignment.map_alignment_segments(file, vertical_alignment, gradient_curve)
# Per IFC CT 4.1.7.1.1.1, the shape representation for Horizontal geometry only is
# RepresentationIdentifier="Axis" and RepresentationType="Curve2D".
# However, per IFC CT 4.1.7.1.1.2 and 3 the shape represenation with Horizontal, Vertical and Cant
# is RepresentationIdentifier="FootPrint" and RepresentationType="Curve2D" for the horizontal and
# RepresentationIdentifier="Axis" and RepresentationType="Curve3D" for the 2.5D curve.
# Since the alignment is transitioning from horizontal only to horizontal+vertical, the
# RepresentationIdentifier must change from "Axis" to "FootPrint"
representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment)
for representation in representations:
if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D":
representation.RepresentationIdentifier = "FootPrint"
break
# create the Axis,Curve3D representation
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
axis3d_shape_representation = file.create_entity(
type="IfcShapeRepresentation",
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
Items=(gradient_curve,),
)
ifcopenshell.api.geometry.assign_representation(file, parent_alignment, axis3d_shape_representation)
else:
# there are multiple vertical reusing the horizontal (IFC CT 4.1.4.4.1.2)
# this is the second or subsequent vertical reusing the horizontal
# create a new child alignment for the new vertical
child_alignment = file.create_entity(
type="IfcAlignment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=f"Child of {parent_alignment.Name}",
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
PredefinedType=None,
)
# Aggregate the child alignment to the parent alignment
ifcopenshell.api.aggregate.assign_object(file, (child_alignment,), parent_alignment)
# nest the vertical under the child alignment
ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_alignment], relating_object=child_alignment)
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
if base_curve:
child_alignment.ObjectPlacement = parent_alignment.ObjectPlacement
# the parent alignment has a Representation so create a representation for the vertical
gradient_curve = file.create_entity(
type="IfcGradientCurve", Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None
)
ifcopenshell.api.alignment.map_alignment_segments(file, vertical_alignment, gradient_curve)
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
# create the Curve3D representation
axis3d_shape_representation = file.create_entity(
type="IfcShapeRepresentation",
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
Items=(gradient_curve,),
)
# add the representation to the child alignment
ifcopenshell.api.geometry.assign_representation(file, child_alignment, axis3d_shape_representation)
@@ -1,59 +0,0 @@
# 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
import ifcopenshell.api.alignment.add_vertical_alignment
from ifcopenshell import entity_instance
from collections.abc import Sequence
def add_vertical_alignment_by_pi_method(
file: ifcopenshell.file,
parent_alignment: entity_instance,
vpoints: Sequence[Sequence[float]],
lengths: Sequence[float],
) -> None:
"""
Adds a vertical alignment to a previously created alignment using the PI method.
If this is the first vertical alignment assigned to the parent_alignment the IFC CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant
is followed. If this is the second or subsequent vertical alignment assigned to the parent_alignment the
IFC CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout is followed.
When the second vertical alignment is added, the structure of the IFC model must transition from one concept template to the other.
Specifically, the following occurs:
1) The first child IfcAlignment is created and is IfcRelAggregates with the parent alignment.
2) The first vertical alignment is unassigned from the IfcRelNests of the parent alignment and assigned to the new child alignment IfcRelNests
3) A second child IfcAlignment is created and it is IfcRelAggregates with the parent alignment.
4) An IfcAlignmentVertical is created from vpoints and lengths and it is assigned to the second child alignment
For the third and subsequent vertical alignments, a new child alignment is created and aggregated to the parent alignment and an IfcAlignmentVertical is created
from vpoints and lengths and assigned to the new child alignment.
If the parent_alignment has a geometric representation, a geometric representation will be created for the vertical alignment.
:param parent_alignment: The parent alignment
:param vpoints: A sequence of (D,Z) points where D is distance along horizontal and Z is elevation
:param: lengths: Lengths of parabolic vertical curves occuring at each VPI
:return: None
"""
vertical_alignment = ifcopenshell.api.alignment.create_vertical_alignment_by_pi_method(
file, parent_alignment.Name, vpoints, lengths
)
ifcopenshell.api.alignment.add_vertical_alignment(file, parent_alignment, vertical_alignment)
@@ -0,0 +1,192 @@
# 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
import ifcopenshell.api.aggregate
import ifcopenshell.api.alignment
import ifcopenshell.api.geometry
import ifcopenshell.api.nest
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.api
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._add_zero_length_segment import _add_zero_length_segment
def _move_vertical_layout_to_child_alignment(
file: ifcopenshell.file, parent_alignment: entity_instance, vertical_layout: entity_instance
):
"""
Creates a new child alignment and aggregates it to the parent alignment. Moves the vertical alignment from the parent
alignment to the child alignment. Also moves the "Axis/Curve3D" representation to the child alignment, if present.
This function supports the transition of vertical alignment between CT 4.1.4.4.1.1 and 4.1.4.4.1.2 because a subsequent
vertical alignment is being added and the Alignment Layout - Reusing Horizontal Layout concept applies.
"""
# unhook the vertical layout from the parent alignment
ifcopenshell.api.nest.unassign_object(file, related_objects=[vertical_layout])
# create the child alignment
child_alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(), Name=f"Child of {parent_alignment.Name}"
)
# nest the vertical layout onto the child alignment
ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=child_alignment)
# aggreage the child alignment to the parent alignment
ifcopenshell.api.aggregate.assign_object(file, products=[child_alignment], relating_object=parent_alignment)
# if the parent alignment has a representation, move the Axis/Curve3D represention to the child alignment
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
if base_curve:
representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment)
for representation in representations:
if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D":
ifcopenshell.api.geometry.unassign_representation(file, parent_alignment, representation)
ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation)
break
def add_vertical_layout(file: ifcopenshell.file, parent_alignment: entity_instance) -> entity_instance:
"""
Adds a vertical layout to a previously created alignment.
If this is the first vertical layout assigned to the parent_alignment the IFC CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant
is followed. If this is the second or subsequent vertical layout assigned to the parent_alignment the
IFC CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout is followed.
When the second vertical layout is added, the structure of the IFC model must transition from one concept template to the other.
Specifically, the following occurs:
1) The first child IfcAlignment is created and is IfcRelAggregates with the parent alignment.
2) The first vertical layout is unassigned from the IfcRelNests of the parent alignment and is IfcRelNests to the new child alignment.
3) A second child IfcAlignment is created and it is IfcRelAggregates with the parent alignment.
4) The vertical layout is IfcRelNests to the second child alignment
For the third and subsequent vertical layouts, a new child alignment is created and aggregated to the parent alignment.
A zero segment length terminated IfcGradientCurve is created for the new vertical layout
:param parent_alignment: The parent alignment
:return: The new vertical layout, including the manditory zero length segment
"""
vertical_layout = file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new())
# get all the child alignments under alignment
child_alignments = [
c for c in ifcopenshell.util.element.get_decomposition(parent_alignment) if c.is_a("IfcAlignment")
]
# Get all the IfcAlignmentVertical that are nesting alignment (there should be 0 or 1)
# if 0, alignment is just horizontal and we are adding the first vertical so it will nest to the alignment,
# or there are multiple vertical and they nest to the aggregated child alignments
# if 1, there is one vertical alignments. Move it to a child alignment
vertical_layouts_nesting_alignment = [
c for c in ifcopenshell.util.element.get_components(parent_alignment) if c.is_a("IfcAlignmentVertical")
]
# move the vertical layout to a child alignment because there is going to be more than one vertical
assert len(vertical_layouts_nesting_alignment) == 0 or len(vertical_layouts_nesting_alignment) == 1
for vertical_layout_nesting_alignment in vertical_layouts_nesting_alignment:
_move_vertical_layout_to_child_alignment(file, parent_alignment, vertical_layout_nesting_alignment)
if len(child_alignments) == 0 and len(vertical_layouts_nesting_alignment) == 0:
# this is the first vertical layout so nest it into the parent alignment (IFC CT 4.1.4.4.1.1)
ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=parent_alignment)
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
# the parent alignment has a Representation so create a representation for the vertical
gradient_curve = file.createIfcGradientCurve(
Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None
)
# Per IFC CT 4.1.7.1.1.1, the shape representation for Horizontal geometry only is
# RepresentationIdentifier="Axis" and RepresentationType="Curve2D".
# However, per IFC CT 4.1.7.1.1.2 and 3 the shape represenation with Horizontal, Vertical and Cant
# is RepresentationIdentifier="FootPrint" and RepresentationType="Curve2D" for the horizontal and
# RepresentationIdentifier="Axis" and RepresentationType="Curve3D" for the 2.5D curve.
# Since the alignment is transitioning from horizontal only to horizontal+vertical, the
# RepresentationIdentifier must change from "Axis" to "FootPrint"
representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment)
for representation in representations:
if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D":
representation.RepresentationIdentifier = "FootPrint"
break
# create the Axis,Curve3D representation
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
axis3d_shape_representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
Items=(gradient_curve,),
)
ifcopenshell.api.geometry.assign_representation(file, parent_alignment, axis3d_shape_representation)
else:
# there are multiple vertical reusing the horizontal (IFC CT 4.1.4.4.1.2)
# this is the second or subsequent vertical reusing the horizontal
# create a new child alignment for the new vertical
child_alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=f"Child of {parent_alignment.Name}",
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
PredefinedType=None,
)
# Aggregate the child alignment to the parent alignment
ifcopenshell.api.aggregate.assign_object(file, (child_alignment,), parent_alignment)
# nest the vertical under the child alignment
ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=child_alignment)
child_alignment.ObjectPlacement = parent_alignment.ObjectPlacement
# the parent alignment has a Representation so create a representation for the vertical
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
gradient_curve = file.createIfcGradientCurve(
Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None
)
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
# create the Curve3D representation
axis3d_shape_representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
Items=(gradient_curve,),
)
# add the representation to the child alignment
ifcopenshell.api.geometry.assign_representation(file, child_alignment, axis3d_shape_representation)
# All alignment layouts must end with a zero length segment. Their geometric representations must also end with a zero length segment.
# Now that all the geometry is setup, add the zero length segment to the layout, which also adds a zero length segment to the representation
_add_zero_length_segment(file, vertical_layout)
return vertical_layout
@@ -1,154 +0,0 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import numpy as np
from ifcopenshell import entity_instance
def add_zero_length_segment(file: ifcopenshell.file, entity: entity_instance) -> None:
"""
Adds a zero length segment to the end of entity.
:param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve (or subtype)
:return: None
"""
expected_types = [
"IfcAlignmentHorizontal",
"IfcAlignmentVertical",
"IfcAlignmentCant",
"IfcCompositeCurve",
"IfcGradientCurve",
"IfcSegmentedReferenceCurve",
]
if not entity.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}"
)
if ifcopenshell.api.alignment.has_zero_length_segment(entity):
return # do nothing if the entity already has a zero length segment
if entity.is_a("IfcCompositeCurve"):
last_segment = entity.Segments[-1]
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, last_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
x = float(end[0, 3])
y = float(end[1, 3])
dx = float(end[0, 0])
dy = float(end[1, 0])
parent_curve = file.createIfcLine(
Pnt=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))),
Dir=file.createIfcVector(
Orientation=file.createIfcDirection(DirectionRatios=((1.0, 0.0))),
Magnitude=1.0,
),
)
curve_segment = file.createIfcCurveSegment(
Transition="DISCONTINUOUS",
Placement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((x, y)),
RefDirection=file.createIfcDirection((dx, dy)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(0.0),
ParentCurve=parent_curve,
)
ifcopenshell.api.alignment.add_segment_to_curve(file, curve_segment, entity)
else:
for rel in entity.IsNestedBy:
if 0 < len(rel.RelatedObjects):
last_segment = rel.RelatedObjects[-1]
if last_segment.is_a("IfcAlignmentSegment"):
if entity.is_a("IfcAlignmentHorizontal"):
design_parameters = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint(
(0.0, 0.0)
), # this is a little problematic. need to know the end point and tangent
StartDirection=0.0, # of the previous segment, which requires geometry mapping
SegmentLength=0.0,
PredefinedType="LINE",
)
segment = file.createIfcAlignmentSegment(
GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters
)
ifcopenshell.api.nest.assign_object(
file,
related_objects=[
segment,
],
relating_object=entity,
)
break
elif entity.is_a("IfcAlignmentVertical"):
design_parameters = file.createIfcAlignmentVerticalSegment(
StartDistAlong=last_segment.DesignParameters.StartDistAlong
+ last_segment.DesignParameters.HorizontalLength,
HorizontalLength=0.0,
StartHeight=0.0,
StartGradient=last_segment.DesignParameters.EndGradient,
EndGradient=last_segment.DesignParameters.EndGradient,
PredefinedType="CONSTANTGRADIENT",
)
segment = file.createIfcAlignmentSegment(
GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters
)
ifcopenshell.api.nest.assign_object(
file,
related_objects=[
segment,
],
relating_object=entity,
)
break
elif entity.is_a("IfcAlignmentCant"):
design_parameters = file.createIfcAlignmentCantSegment(
StartDistAlong=last_segment.DesignParameters.StartDistAlong
+ last_segment.DesignParameters.HorizontalLength,
HorizontalLength=0.0,
StartCantLeft=(
last_segment.DesignParameters.EndCantLeft
if last_segment.DesignParameters.EndCantLeft != None
else last_segment.DesignParameters.StartCantLeft
),
StartCantRight=(
last_segment.DesignParameters.EndCantRight
if last_segment.DesignParameters.EndCantRight != None
else last_segment.DesignParameters.StartCantRight
),
PredefinedType="CONSTANTCANT",
)
segment = file.createIfcAlignmentSegment(
GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters
)
ifcopenshell.api.nest.assign_object(
file,
related_objects=[
segment,
],
relating_object=entity,
)
break
@@ -0,0 +1,87 @@
# 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
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._create_geometric_representation import _create_geometric_representation
from ifcopenshell.api.alignment._add_zero_length_segment import _add_zero_length_segment
def create(
file: ifcopenshell.file,
name: str,
include_vertical: bool = False,
include_cant: bool = False,
start_station: float = 0.0,
) -> entity_instance:
"""
Creates a new IfcAlignment with an IfcRelNests nesting an IfcReferent (for stationing) and IfcAlignmentHorizontal. The nest relationship can optionally
include IfcAlignmentVertical and IfcAlignmentCant. Geometric representations for the alignment layouts (IfcCompositeCurve,
IfcGradientCurve, IfcSegmentedReferenceCurve) are created as well.
Zero length segments are added at the end.
The IfcAlignment is aggreated to IfcProject
Use get_horizontal_layout(alignment) to get the IfcAlignmentHorizontal layout.
:param file:
:param name: name assigned to IfcAlignment.Name
:param include_vertical: If True, IfcAlignmentVertical and IfcGradientCurve are created
:param include_cant: If True, IfcAlignmentCant and IfcSegmentedReferenceCurve are created
:param start_station: station value at the start of the alignment
:return: Returns an IfcAlignment
"""
alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(),
Name=name,
)
alignment_layouts = []
alignment_layouts.append(file.createIfcAlignmentHorizontal(GlobalId=ifcopenshell.guid.new()))
if include_vertical:
alignment_layouts.append(file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new()))
if include_cant:
alignment_layouts.append(file.createIfcAlignmentCant(GlobalId=ifcopenshell.guid.new(), RailHeadDistance=1.0))
ifcopenshell.api.nest.assign_object(file, related_objects=alignment_layouts, relating_object=alignment)
_create_geometric_representation(file, alignment)
for layout in alignment_layouts:
_add_zero_length_segment(file, layout)
# define stationing
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
name = ifcopenshell.util.stationing.station_as_string(file, start_station)
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, basis_curve, 0.0, start_station, name
)
ifcopenshell.api.nest.reorder_nesting(file, referent, -1, 0)
# IFC 4.1.4.1.1 Alignment Aggregation To Project
project = file.by_type("IfcProject")[0]
if project:
ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project)
return alignment
@@ -24,59 +24,34 @@ from ifcopenshell import entity_instance
from collections.abc import Sequence
def create_alignment_by_pi_method(
def create_by_pi_method(
file: ifcopenshell.file,
alignment_name: str,
name: str,
hpoints: Sequence[Sequence[float]],
radii: Sequence[float],
vpoints: Sequence[Sequence[float]] = None,
lengths: Sequence[float] = None,
alignment_description: str = None,
start_station: float = 0.0,
) -> entity_instance:
"""
Create an alignment using the PI layout method for both horizontal and vertical alignments.
If vpoints and lengths are omitted, only a horizontal alignment is created. Only the business logic
entities are creaed. Use create_geometric_representation() to create the geometric entities.
If vpoints and lengths are omitted, only a horizontal alignment is created.
:param alignment_name: value for Name attribute
:param name: value for Name attribute
:param points: (X,Y) pairs denoting the location of the horizontal PIs, including start and end
:param radii: radii values to use for transition
:param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end.
:param lengths: parabolic vertical curve horizontal length values to use for transition
:param alignment_description: value for Description attribute
:return: Returns an IfcAlignment
"""
alignments = []
horizontal_alignment = ifcopenshell.api.alignment.create_horizontal_alignment_by_pi_method(
file, alignment_name, hpoints, radii
include_vertical = True if vpoints and lengths else False
alignment = ifcopenshell.api.alignment.create(
file, name, include_vertical=include_vertical, start_station=start_station
)
alignments.append(horizontal_alignment)
if vpoints and lengths:
vertical_alignment = ifcopenshell.api.alignment.create_vertical_alignment_by_pi_method(
file, alignment_name, vpoints, lengths
)
alignments.append(vertical_alignment)
# create the alignment
alignment = file.create_entity(
type="IfcAlignment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=alignment_name,
Description=alignment_description,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
PredefinedType=None,
)
# nest the horizontal and vertical under the alignment
ifcopenshell.api.nest.assign_object(file, related_objects=alignments, relating_object=alignment)
# IFC 4.1.4.1.1 Alignment Aggregation To Project
project = file.by_type("IfcProject")[0]
ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project)
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
ifcopenshell.api.alignment.layout_horizontal_alignment_by_pi_method(file, horizontal_layout, hpoints, radii)
if include_vertical:
vertical_layout = ifcopenshell.api.alignment.get_vertical_layout(alignment)
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(file, vertical_layout, vpoints, lengths)
return alignment
@@ -17,27 +17,19 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.alignment
import ifcopenshell.api.geometry
import ifcopenshell.api.nest
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.stationing
import ifcopenshell.api
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment import get_axis_subcontext
import math
from typing import Sequence
import csv
def create_alignment_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
"""
Creates an alignment from PI data stored in a CSV file. Only the business logic
entities are creaed. Use create_geometric_representation() to create the geometric entities.
Creates an alignment from PI data stored in a CSV file.
The format of the file is:
@@ -94,22 +86,16 @@ def create_alignment_from_csv(file: ifcopenshell.file, filepath: str) -> entity_
radii = radii[1:-1] # The first radius value is a placeholder, remove it
if row_count == 1:
# create the alignment
alignment = file.createIfcAlignment(GlobalId=ifcopenshell.guid.new())
# create the horizontal alignment
horizontal_alignment = ifcopenshell.api.alignment.create_horizontal_alignment_by_pi_method(
file, "Alignment_from_CSV", coordinates, radii
)
# nest them together
ifcopenshell.api.nest.assign_object(
file, related_objects=(horizontal_alignment,), relating_object=alignment
alignment = ifcopenshell.api.alignment.create(file, "Alignment_from_CSV")
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
ifcopenshell.api.alignment.layout_horizontal_alignment_by_pi_method(
file, horizontal_layout, coordinates, radii
)
else:
# add all subsequent vertical alignments
ifcopenshell.api.alignment.add_vertical_alignment_by_pi_method(file, alignment, coordinates, radii)
# IFC 4.1.4.1.1 Alignment Aggregation To Project
project = file.by_type("IfcProject")[0]
ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project)
vertical_layout = ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(
file, vertical_layout, coordinates, radii
)
return alignment
@@ -1,216 +0,0 @@
# 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
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
import math
from collections.abc import Sequence
def create_horizontal_alignment_by_pi_method(
file: ifcopenshell.file, name: str, hpoints: Sequence[Sequence[float]], radii: Sequence[float]
) -> entity_instance:
"""
Create a horizontal alignment using the PI layout method.
:param name: value for Name attribute
:param hpoints: (X, Y) pairs denoting the location of the horizontal PIs, including start (POB) and end (POE).
:param radii: radius values to use for transition
:return: Returns a IfcAlignmentHorizontal
"""
if not (len(hpoints) - 2 == len(radii)):
raise ValueError("radii should have two fewer elements that hpoints")
# Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments
horizontal_alignment = file.create_entity(
type="IfcAlignmentHorizontal",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=f"{name} - Horizontal",
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
)
xBT, yBT = hpoints[0]
xPI, yPI = hpoints[1]
i = 1
for radius in radii:
# back tangent
dxBT = xPI - xBT
dyBT = yPI - yBT
angleBT = math.atan2(dyBT, dxBT)
lengthBT = math.sqrt(dxBT * dxBT + dyBT * dyBT)
# forward tangent
i += 1
xFT, yFT = hpoints[i]
dxFT = xFT - xPI
dyFT = yFT - yPI
angleFT = math.atan2(dyFT, dxFT)
delta = angleFT - angleBT
tangent = abs(radius * math.tan(delta / 2))
lc = abs(radius * delta)
radius *= delta / abs(delta)
xPC = xPI - tangent * math.cos(angleBT)
yPC = yPI - tangent * math.sin(angleBT)
xPT = xPI + tangent * math.cos(angleFT)
yPT = yPI + tangent * math.sin(angleFT)
tangent_run = lengthBT - tangent
# create back tangent run
pt = file.create_entity(
type="IfcCartesianPoint",
Coordinates=(xBT, yBT),
)
design_parameters = file.create_entity(
type="IfcAlignmentHorizontalSegment",
StartTag=None,
EndTag=None,
StartPoint=pt,
StartDirection=angleBT,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=tangent_run,
GravityCenterLineHeight=None,
PredefinedType="LINE",
)
alignment_segment = file.create_entity(
type="IfcAlignmentSegment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=None,
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
DesignParameters=design_parameters,
)
ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment)
# create circular curve
if radius != 0.0:
pc = file.create_entity(
type="IfcCartesianPoint",
Coordinates=(xPC, yPC),
)
design_parameters = file.create_entity(
type="IfcAlignmentHorizontalSegment",
StartTag=None,
EndTag=None,
StartPoint=pc,
StartDirection=angleBT,
StartRadiusOfCurvature=float(radius),
EndRadiusOfCurvature=float(radius),
SegmentLength=lc,
GravityCenterLineHeight=None,
PredefinedType="CIRCULARARC",
)
alignment_segment = file.create_entity(
type="IfcAlignmentSegment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=None,
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
DesignParameters=design_parameters,
)
ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment)
xBT = xPT
yBT = yPT
xPI = xFT
yPI = yFT
# done processing radii
# create last tangent run
dx = xPI - xBT
dy = yPI - yBT
angleBT = math.atan2(dy, dx)
tangent_run = math.sqrt(dx * dx + dy * dy)
pt = file.create_entity(type="IfcCartesianPoint", Coordinates=(xBT, yBT))
design_parameters = file.create_entity(
type="IfcAlignmentHorizontalSegment",
StartTag=None,
EndTag=None,
StartPoint=pt,
StartDirection=angleBT,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=tangent_run,
GravityCenterLineHeight=None,
PredefinedType="LINE",
)
alignment_segment = file.create_entity(
type="IfcAlignmentSegment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=None,
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
DesignParameters=design_parameters,
)
ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment)
# create zero length terminator segment
poe = file.create_entity(type="IfcCartesianPoint", Coordinates=(xPI, yPI))
design_parameters = file.create_entity(
type="IfcAlignmentHorizontalSegment",
StartTag="POE",
EndTag="POE",
StartPoint=poe,
StartDirection=angleBT,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=0.0,
GravityCenterLineHeight=None,
PredefinedType="LINE",
)
alignment_segment = file.create_entity(
type="IfcAlignmentSegment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=None,
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
DesignParameters=design_parameters,
)
ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment)
return horizontal_alignment
@@ -0,0 +1,85 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.get_alignment
import ifcopenshell.geom
from ifcopenshell import entity_instance
import math
from ifcopenshell import ifcopenshell_wrapper
import numpy as np
from ifcopenshell.api.alignment._add_segment_to_layout import _add_segment_to_layout
def create_layout_segment(
file: ifcopenshell.file, layout: entity_instance, design_parameters: entity_instance
) -> np.array:
"""
Creates a new IfcAlignmentSegment using the IfcAlignmentParameterSegment design parameters.
The new segment is appended to the layout alignment and the corresponding IfcCurveSegment is created in the geometric representation
:param layout: The layout to receive the new layout segment. This parameter is expected to be IfcAlignmentHorizontal, IfcAlignmentVertical or IfcAlignmentCant
:param design_parameters: The parameters defining the segment. Expected to be the appropreate subclass of IfcAlignmentParameterSegment
:return: 4x4 matrix at end of segment as np.array intended to be used as the start point geometry for the next segment.
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if layout.is_a("IfcAlignmentHorizontal") and not design_parameters.is_a("IfcAlignmentHorizontalSegment"):
raise TypeError("Expected design_parameters to be IfcAlignmentHorizontalSegment")
elif layout.is_a("IfcAlignmentVertical") and not design_parameters.is_a("IfcAlignmentVerticalSegment"):
raise TypeError("Expected design_parameters to be IfcAlignmentVerticalSegment")
elif layout.is_a("IfcAlignmentCant") and not design_parameters.is_a("IfcAlignmentCantSegment"):
raise TypeError("Expected design_parameters to be IfcAlignmentCantSegment")
# 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
# 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 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
@@ -58,7 +58,7 @@ def create_segment_representations(
)
curve_segments = curve.Segments
segments = nested_alignment.IsNestedBy[0].RelatingObjects
segments = nested_alignment.IsNestedBy[0].RelatedObjects
for curve_segment, alignment_segment in zip(curve_segments, segments):
axis_representation = file.create_entity(
@@ -1,194 +0,0 @@
# 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
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
import math
from collections.abc import Sequence
def create_vertical_alignment_by_pi_method(
file: ifcopenshell.file, name: str, vpoints: Sequence[Sequence[float]], lengths: Sequence[float]
) -> entity_instance:
"""
Create a vertical alignment using the PI layout method.
:param name: value for Name attribute
:param base_curve: base curve representing the 2D projection of the gradient curve
:param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end.
:param lengths: horizontal length of parabolic vertical curves
:return: IfcAlignmentHorizontal
"""
if not (len(vpoints) - 2 == len(lengths)):
raise ValueError("lengths should have two fewer elements that vpoints")
# Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments
vertical_alignment = file.create_entity(
type="IfcAlignmentVertical",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=f"{name} - Vertical",
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
)
xPBG, yPBG = vpoints[0]
xPVI, yPVI = vpoints[1]
i = 1
for length in lengths:
# back gradient
dxBG = xPVI - xPBG
dyBG = yPVI - yPBG
start_slope = math.tan(math.atan2(dyBG, dxBG))
# forward gradient
i += 1
xPFG, yPFG = vpoints[i]
dxFG = xPFG - xPVI
dyFG = yPFG - yPVI
end_slope = math.tan(math.atan2(dyFG, dxFG))
xEVC = xPVI + length / 2.0
yEVC = yPVI + end_slope * length / 2.0
# create gradient
gradient_length = dxBG - length / 2.0
design_parameters = file.create_entity(
type="IfcAlignmentVerticalSegment",
StartTag=None,
EndTag=None,
StartDistAlong=xPBG,
HorizontalLength=gradient_length,
StartHeight=yPBG,
StartGradient=start_slope,
EndGradient=start_slope,
RadiusOfCurvature=None,
PredefinedType="CONSTANTGRADIENT",
)
alignment_segment = file.create_entity(
type="IfcAlignmentSegment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=None,
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
DesignParameters=design_parameters,
)
ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment)
# create vertical curve
if 0.0 < length:
k = (end_slope - start_slope) / length
xBVC = xPVI - length / 2.0
yBVC = yPVI - start_slope * length / 2.0
design_parameters = file.create_entity(
type="IfcAlignmentVerticalSegment",
StartTag=None,
EndTag=None,
StartDistAlong=xBVC,
HorizontalLength=length,
StartHeight=yBVC,
StartGradient=start_slope,
EndGradient=end_slope,
RadiusOfCurvature=1 / k,
PredefinedType="PARABOLICARC",
)
alignment_segment = file.create_entity(
type="IfcAlignmentSegment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=None,
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
DesignParameters=design_parameters,
)
ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment)
# start of next curve is end of this curve
xPBG = xEVC
yPBG = yEVC
xPVI = xPFG
yPVI = yPFG
# create last gradient run
dx = xPVI - xPBG
dy = yPVI - yPBG
slope = math.tan(math.atan2(dy, dx))
gradient_length = dx
design_parameters = file.create_entity(
type="IfcAlignmentVerticalSegment",
StartTag=None,
EndTag=None,
StartDistAlong=xPBG,
HorizontalLength=gradient_length,
StartHeight=yPBG,
StartGradient=slope,
EndGradient=slope,
RadiusOfCurvature=None,
PredefinedType="CONSTANTGRADIENT",
)
alignment_segment = file.create_entity(
type="IfcAlignmentSegment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=None,
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
DesignParameters=design_parameters,
)
ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment)
# create zero length terminator segment
design_parameters = file.create_entity(
type="IfcAlignmentVerticalSegment",
StartTag="VPOE",
EndTag="VPOE",
StartDistAlong=xPVI,
HorizontalLength=0.0,
StartHeight=yPVI,
StartGradient=slope,
EndGradient=slope,
RadiusOfCurvature=None,
PredefinedType="CONSTANTGRADIENT",
)
alignment_segment = file.create_entity(
type="IfcAlignmentSegment",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=None,
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
DesignParameters=design_parameters,
)
ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment)
return vertical_alignment
@@ -43,12 +43,6 @@ def distance_along_from_station(file: ifcopenshell.file, alignment: entity_insta
print(dist_along) # 100.00
"""
start_station = 0.0
components = ifcopenshell.util.element.get_components(alignment)
for c in components:
if c.is_a("IfcReferent") and ifcopenshell.util.element.get_predefined_type(c) == "STATION":
start_station = ifcopenshell.util.element.get_pset(c, name="Pset_Stationing", prop="Station")
break
start_station = ifcopenshell.api.alignment.get_alignment_station(file, alignment)
dist_along = station - start_station
return dist_along
@@ -17,19 +17,15 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.util
from ifcopenshell import entity_instance
import ifcopenshell.api.alignment.remove_last_segment
from typing import Sequence
import ifcopenshell.util.representation
def remove_zero_length_segment(file: ifcopenshell.file, entity: entity_instance) -> entity_instance:
def get_alignment(layout: entity_instance) -> entity_instance:
"""
Removes the zero length segment from the end of entity.
:param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve
:return: The zero length segment
Returns the alignment that nests this layout
"""
if not ifcopenshell.api.alignment.has_zero_length_segment(entity):
return None
return ifcopenshell.api.alignment.remove_last_segment(file, entity)
return layout.Nests[0].RelatingObject if 0 < len(layout.Nests) else None
@@ -0,0 +1,40 @@
# 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
from ifcopenshell import entity_instance
def get_alignment_station(file: ifcopenshell.file, alignment: entity_instance) -> float:
"""
Returns the start station of the alignment. If the alignment is nested by an IfcReferent
the referent is checked for PredefinedType of STATION and an occurance of Pset_Stationing.Station,
otherwise start station is taken to be 0.0.
"""
if not alignment.is_a("IfcAlignment"):
raise TypeError(f"Expected entity type to be IfcAlignment, instead received {alignment.is_a()}")
start_station = 0.0
components = ifcopenshell.util.element.get_components(alignment)
for c in components:
if c.is_a("IfcReferent") and ifcopenshell.util.element.get_predefined_type(c) == "STATION":
start_station = ifcopenshell.util.element.get_pset(c, name="Pset_Stationing", prop="Station")
break
return start_station
@@ -45,6 +45,10 @@ def get_basis_curve(alignment: entity_instance) -> entity_instance:
representation.RepresentationIdentifier == "FootPrint" and representation.RepresentationType == "Curve2D"
):
axis = representation
break
return None if axis.Items == None or len(axis.Items) == 0 else axis.Items[0]
return None if axis == None or axis.Items == None or len(axis.Items) == 0 else axis.Items[0]
if axis == None and 0 < len(alignment.Decomposes):
parent_alignment = alignment.Decomposes[0].RelatingObject
return get_basis_curve(parent_alignment)
return None
@@ -0,0 +1,31 @@
# 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_cant_layout(alignment: entity_instance) -> entity_instance:
"""
Returns the IfcAlignmentCant assocated with this alignment
"""
for rel in alignment.IsNestedBy:
for layout in rel.RelatedObjects:
if layout.is_a("IfcAlignmentCant"):
return layout
return None
@@ -26,7 +26,7 @@ import ifcopenshell.util.element
def get_child_alignments(alignment: entity_instance) -> Sequence[entity_instance]:
"""
Returns the aggregated child alignments to this alignment
Returns the aggregated child alignments to this alignment per CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout
Example:
@@ -0,0 +1,31 @@
# 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_horizontal_layout(alignment: entity_instance) -> entity_instance:
"""
Returns the IfcAlignmentHorizontal assocated with this alignment
"""
for rel in alignment.IsNestedBy:
for layout in rel.RelatedObjects:
if layout.is_a("IfcAlignmentHorizontal"):
return layout
return None
@@ -0,0 +1,55 @@
# 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
import ifcopenshell.util
from ifcopenshell import entity_instance
import ifcopenshell.util.representation
def get_layout_curve(layout: entity_instance) -> entity_instance:
"""
Returns the representation curve for the layout. This will be an IfcCompositeCurve, IfcGradientCurve, or IfcSegmentReferenceCurve
for IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant, respectively.
:param layout: An alignment layout
:return: The geometric representation curve
Example:
.. code:: python
alignment = model.by_type("IfcAlignment")[0]
layout = ifcopenshell.api.get_horizontal_layout(alignment)
composite_curve = ifcopenshell.api.alignment.get_layout_curve(layout)
"""
alignment = ifcopenshell.api.alignment.get_alignment(layout)
curve = ifcopenshell.api.alignment.get_curve(alignment)
if layout.is_a("IfcAlignmentHorizontal"):
# Layout is horizontal so get the IfcCompositeCurve
if curve.is_a("IfcGradientCurve"):
curve = curve.BaseCurve
elif curve.is_a("IfcSegmentedReferenceCurve"):
curve = curve.BaseCurve.BaseCurve
elif layout.is_a("IfcAlignmentVertical"):
# Layout is vertical so get the IfcGradientCurve
if curve.is_a("IfcSegmentedReferenceCurve"):
curve = curve.BaseCurve
return curve
@@ -0,0 +1,44 @@
# 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
import ifcopenshell.util
from ifcopenshell import entity_instance
from typing import Sequence
import ifcopenshell.util.element
def get_layout_segments(layout: entity_instance) -> Sequence[entity_instance]:
"""
Returns the IfcAlignmentSegment nested to this alignment layout
Example:
.. code:: python
horizontal = model.by_type("IfcAlignmentHorizontal")[0]
segments = ifcopenshell.api.alignment.get_layout_segments(horizontal)
"""
segments = []
for rel in layout.IsNestedBy:
for segment in rel.RelatedObjects:
if segment.is_a("IfcAlignmentSegment"):
segments.append(segment)
return segments
@@ -0,0 +1,31 @@
# 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_vertical_layout(alignment: entity_instance) -> entity_instance:
"""
Returns the IfcAlignmentVertical assocated with this alignment
"""
for rel in alignment.IsNestedBy:
for layout in rel.RelatedObjects:
if layout.is_a("IfcAlignmentVertical"):
return layout
return None
@@ -22,40 +22,29 @@ import ifcopenshell.util.element
from ifcopenshell import entity_instance
def has_zero_length_segment(entity: entity_instance) -> bool:
def has_zero_length_segment(layout: entity_instance) -> bool:
"""
Returns true if the entity ends with a zero length segment. If the entity is an IfcCompositeCurve the IfcCurveSegment.Transition must be DISCONTINUOUS
Returns true if the layout ends with a zero length segment.
:param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve
:param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
:return: True if the zero length segment is present
"""
expected_types = [
"IfcAlignmentHorizontal",
"IfcAlignmentVertical",
"IfcAlignmentCant",
"IfcCompositeCurve",
"IfcGradientCurve",
"IfcSegmentedReferenceCurve",
]
if not entity.is_a() in expected_types:
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}"
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{layout.is_a()}"
)
if entity.is_a("IfcCompositeCurve"):
last_segment = entity.Segments[-1]
return last_segment.Transition == "DISCONTINUOUS" and last_segment.SegmentLength.wrappedValue == 0.0
else:
segments = ifcopenshell.util.element.get_components(entity)
for rel in entity.IsNestedBy:
if 0 < len(rel.RelatedObjects):
last_segment = rel.RelatedObjects[-1]
if last_segment.is_a("IfcAlignmentSegment"):
if last_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
return last_segment.DesignParameters.SegmentLength == 0.0
elif last_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
return last_segment.DesignParameters.HorizontalLength == 0.0
elif last_segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
return last_segment.DesignParameters.HorizontalLength == 0.0
result = False
for rel in layout.IsNestedBy:
if 0 < len(rel.RelatedObjects):
last_segment = rel.RelatedObjects[-1]
if last_segment.is_a("IfcAlignmentSegment"):
if last_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
result = last_segment.DesignParameters.SegmentLength == 0.0
elif last_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
result = last_segment.DesignParameters.HorizontalLength == 0.0
elif last_segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
result = last_segment.DesignParameters.HorizontalLength == 0.0
return False
return result
@@ -0,0 +1,141 @@
# 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
from ifcopenshell import entity_instance
import math
from collections.abc import Sequence
import ifcopenshell.api.alignment.create_layout_segment
def layout_horizontal_alignment_by_pi_method(
file: ifcopenshell.file, layout: entity_instance, hpoints: Sequence[Sequence[float]], radii: Sequence[float]
) -> None:
"""
Appends IfcAlignmentHorizontalSegment to a previously defined IfcAlignmentHorizontal using the PI layout method.
The zero length segment is updated.
:param file: file
:param layout: An IfcAlignmentHorizontal layout
:param hpoints: (X, Y) pairs denoting the location of the horizontal PIs, including start (POB) and end (POE).
:param radii: radius values to use for transition
:return: None
"""
if not (len(hpoints) - 2 == len(radii)):
raise ValueError("radii should have two fewer elements that hpoints")
xBT, yBT = hpoints[0]
xPI, yPI = hpoints[1]
i = 1
for radius in radii:
# back tangent
dxBT = xPI - xBT
dyBT = yPI - yBT
angleBT = math.atan2(dyBT, dxBT)
lengthBT = math.sqrt(dxBT * dxBT + dyBT * dyBT)
# forward tangent
i += 1
xFT, yFT = hpoints[i]
dxFT = xFT - xPI
dyFT = yFT - yPI
angleFT = math.atan2(dyFT, dxFT)
delta = angleFT - angleBT
tangent = abs(radius * math.tan(delta / 2))
lc = abs(radius * delta)
radius *= delta / abs(delta)
xPC = xPI - tangent * math.cos(angleBT)
yPC = yPI - tangent * math.sin(angleBT)
xPT = xPI + tangent * math.cos(angleFT)
yPT = yPI + tangent * math.sin(angleFT)
tangent_run = lengthBT - tangent
# create back tangent run
if 1.0e-03 < tangent_run:
pt = file.createIfcCartesianPoint(
Coordinates=(xBT, yBT),
)
design_parameters = file.createIfcAlignmentHorizontalSegment(
StartTag=None,
EndTag=None,
StartPoint=pt,
StartDirection=angleBT,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=tangent_run,
GravityCenterLineHeight=None,
PredefinedType="LINE",
)
ifcopenshell.api.alignment.create_layout_segment(file, layout, design_parameters)
# create circular curve
if radius != 0.0:
pc = file.createIfcCartesianPoint(
Coordinates=(xPC, yPC),
)
design_parameters = file.createIfcAlignmentHorizontalSegment(
StartTag=None,
EndTag=None,
StartPoint=pc,
StartDirection=angleBT,
StartRadiusOfCurvature=float(radius),
EndRadiusOfCurvature=float(radius),
SegmentLength=lc,
GravityCenterLineHeight=None,
PredefinedType="CIRCULARARC",
)
ifcopenshell.api.alignment.create_layout_segment(file, layout, design_parameters)
xBT = xPT
yBT = yPT
xPI = xFT
yPI = yFT
# done processing radii
# create last tangent run
dx = xPI - xBT
dy = yPI - yBT
angleBT = math.atan2(dy, dx)
tangent_run = math.sqrt(dx * dx + dy * dy)
if 1.0e-03 < tangent_run:
pt = file.createIfcCartesianPoint(Coordinates=(xBT, yBT))
design_parameters = file.createIfcAlignmentHorizontalSegment(
StartTag=None,
EndTag=None,
StartPoint=pt,
StartDirection=angleBT,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=tangent_run,
GravityCenterLineHeight=None,
PredefinedType="LINE",
)
ifcopenshell.api.alignment.create_layout_segment(file, layout, design_parameters)
@@ -0,0 +1,123 @@
# 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
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
import math
from collections.abc import Sequence
import ifcopenshell.api.alignment.create_layout_segment
def layout_vertical_alignment_by_pi_method(
file: ifcopenshell.file, layout: entity_instance, vpoints: Sequence[Sequence[float]], lengths: Sequence[float]
) -> None:
"""
Appends IfcAlignmentVerticalSegment to a previously defined IfcAlignmentVertical using the PI layout method.
The zero length segment is updated.
:param file: file
:param layout: An IfcAlignmentVertical layout
:param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end.
:param lengths: horizontal length of parabolic vertical curves
:return: None
"""
if not (len(vpoints) - 2 == len(lengths)):
raise ValueError("lengths should have two fewer elements that vpoints")
xPBG, yPBG = vpoints[0]
xPVI, yPVI = vpoints[1]
i = 1
for length in lengths:
# back gradient
dxBG = xPVI - xPBG
dyBG = yPVI - yPBG
start_slope = math.tan(math.atan2(dyBG, dxBG))
# forward gradient
i += 1
xPFG, yPFG = vpoints[i]
dxFG = xPFG - xPVI
dyFG = yPFG - yPVI
end_slope = math.tan(math.atan2(dyFG, dxFG))
xEVC = xPVI + length / 2.0
yEVC = yPVI + end_slope * length / 2.0
# create gradient
gradient_length = dxBG - length / 2.0
if 1.0e-03 < gradient_length:
design_parameters = file.createIfcAlignmentVerticalSegment(
StartTag=None,
EndTag=None,
StartDistAlong=xPBG,
HorizontalLength=gradient_length,
StartHeight=yPBG,
StartGradient=start_slope,
EndGradient=start_slope,
RadiusOfCurvature=None,
PredefinedType="CONSTANTGRADIENT",
)
ifcopenshell.api.alignment.create_layout_segment(file, layout, design_parameters)
# create vertical curve
if 0.0 < length:
k = (end_slope - start_slope) / length
xBVC = xPVI - length / 2.0
yBVC = yPVI - start_slope * length / 2.0
design_parameters = file.createIfcAlignmentVerticalSegment(
StartTag=None,
EndTag=None,
StartDistAlong=xBVC,
HorizontalLength=length,
StartHeight=yBVC,
StartGradient=start_slope,
EndGradient=end_slope,
RadiusOfCurvature=1 / k,
PredefinedType="PARABOLICARC",
)
ifcopenshell.api.alignment.create_layout_segment(file, layout, design_parameters)
# start of next curve is end of this curve
xPBG = xEVC
yPBG = yEVC
xPVI = xPFG
yPVI = yPFG
# create last gradient run
dx = xPVI - xPBG
dy = yPVI - yPBG
slope = math.tan(math.atan2(dy, dx))
gradient_length = dx
if 1.0e-03 < gradient_length:
design_parameters = file.createIfcAlignmentVerticalSegment(
StartTag=None,
EndTag=None,
StartDistAlong=xPBG,
HorizontalLength=gradient_length,
StartHeight=yPBG,
StartGradient=slope,
EndGradient=slope,
RadiusOfCurvature=None,
PredefinedType="CONSTANTGRADIENT",
)
ifcopenshell.api.alignment.create_layout_segment(file, layout, design_parameters)
@@ -1,68 +0,0 @@
# 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
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
def map_alignment_segments(
file: ifcopenshell.file, alignment: entity_instance, composite_curve: entity_instance
) -> None:
"""
Creates IfcCurveSegment entities for the supplied alignment business logic entity instance and assigns them to the composite curve.
End-Start points of adjacent segments are evaluated and the IfcCurveSegment.Transition is set.
This function does not create an IfcShapeRepresentation. Use create_geometric_representation to create all the representations
for an alignment. This function only populates the composite curve with IfcCurveSegment entities.
:param alignment: The business logic alignment, expected to be IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
:param composite_curve: The IfcCompositeCurve (or subclass) which will receive the IfcCurveSegment
:return: None
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not alignment.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{alignment.is_a()}"
)
if alignment.is_a("IfcAlignmentHorizontal") and not composite_curve.is_a("IfcCompositeCurve"):
raise TypeError(f"Expected to see IfcCompositeCurve, instead received '{composite_curve.is_a()}'.")
elif alignment.is_a("IfcAlignmentVertical") and not composite_curve.is_a("IfcGradientCurve"):
raise TypeError(f"Expected to see IfcGradientCurve, instead received '{composite_curve.is_a()}'.")
elif alignment.is_a("IfcAlignmentCant") and not composite_curve.is_a("IfcSegmentedReferenceCurve"):
raise TypeError(f"Expected to see IfcSegmentedReferenceCurve, instead received '{composite_curve.is_a()}'.")
composite_curve.SelfIntersect = False
for rel_nests in alignment.IsNestedBy:
for layout in rel_nests.RelatedObjects:
if layout.is_a("IfcLinearElement"):
if alignment.is_a("IfcAlignmentHorizontal"):
mapped_segments = ifcopenshell.api.alignment.map_alignment_horizontal_segment(file, layout)
elif alignment.is_a("IfcAlignmentVertical"):
mapped_segments = ifcopenshell.api.alignment.map_alignment_vertical_segment(file, layout)
elif alignment.is_a("IfcAlignmentCant"):
mapped_segments = ifcopenshell.api.alignment.map_alignment_cant_segment(
file, layout, alignment.RailHeadDistance
)
else:
assert False
for mapped_segment in mapped_segments:
if mapped_segment:
ifcopenshell.api.alignment.add_segment_to_curve(file, mapped_segment, composite_curve)
@@ -20,21 +20,21 @@ import ifcopenshell
from ifcopenshell import entity_instance
def name_segments(prefix: str, alignment: entity_instance) -> None:
def name_segments(prefix: str, layout: entity_instance) -> None:
"""
Sets the segment name like ("H1" for horizontal, "V1" for vertical, "C1" for cant)
Sets the IfcAlignmentSegment.Name attribute using a prefix and sequence number (e.g. "H1" for horizontal, "V1" for vertical, "C1" for cant)
:param prefix: The naming prefix
:param alignment: The alignment whose segments are to be named. This should be a IfcAlignmentHorizontal, IfcAlignmentVertical or IfcAlignmentCant
:param layout: The layout alignment whose segments are to be named. This should be a IfcAlignmentHorizontal, IfcAlignmentVertical or IfcAlignmentCant
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not alignment.is_a() in expected_types:
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{v.is_a()}"
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{layout.is_a()}"
)
i = 1
for rel in alignment.IsNestedBy:
for rel in layout.IsNestedBy:
for segment in rel.RelatedObjects:
if segment.is_a("IfcAlignmentSegment"):
segment.Name = f"{prefix}{i}"
@@ -1,59 +0,0 @@
# 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
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import numpy as np
from ifcopenshell import entity_instance
import ifcopenshell.util
import ifcopenshell.util.element
def remove_last_segment(file: ifcopenshell.file, entity: entity_instance) -> entity_instance:
"""
Removes the last segment from the end of entity.
:param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve
:return: The segment
"""
expected_types = [
"IfcAlignmentHorizontal",
"IfcAlignmentVertical",
"IfcAlignmentCant",
"IfcCompositeCurve",
"IfcGradientCurve",
"IfcSegmentedReferenceCurve",
]
if not entity.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}"
)
if entity.is_a("IfcCompositeCurve"):
last_segment = entity.Segments[-1]
entity.Segments = tuple(set(entity.Segments) - {last_segment})
entity.Segments[-1].Transition = "DISCONTINUOUS"
return last_segment
else:
components = ifcopenshell.util.element.get_components(entity)
last_segment = components[-1]
ifcopenshell.api.nest.unassign_object(file, (last_segment,))
return last_segment
@@ -123,6 +123,23 @@ def print_alignment(alignment, indent=0):
print_alignment(child, indent + 2)
def print_alignment_deep(alignment, indent=0):
"""
Debugging function to print alignment decomposition, including layout segments
"""
print(" " * indent, alignment)
for rel in alignment.IsNestedBy:
for child in rel.RelatedObjects:
print_alignment_deep(child, indent + 2)
if child.is_a("IfcAlignmentSegment"):
print(" " * (indent + 4), child.DesignParameters)
for agg in alignment.IsDecomposedBy:
for child in agg.RelatedObjects:
print_alignmen_deep(child, indent + 2)
def print_composite_curve(curve):
"""
Debugging function to print composite curve segments
@@ -131,3 +148,17 @@ def print_composite_curve(curve):
for segment in curve.Segments:
print(" " * 2, segment)
def print_composite_curve_deep(curve):
"""
Debugging function to print composite curve segments, including curve segment details
"""
print(str(curve)[0:100])
for segment in curve.Segments:
print(" " * 2, segment)
print(" " * 4, segment.ParentCurve)
print(" " * 4, segment.Placement)
print(" " * 4, segment.Placement.Location)
print(" " * 4, segment.Placement.RefDirection)
@@ -0,0 +1,33 @@
# 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/>.
"""
Coordinate Geometry (cogo) functions primarily for survey points and control monument for layout, parcels, etc.
"""
from .add_survey_point import add_survey_point
from .assign_survey_point import assign_survey_point
from .edit_survey_point import edit_survey_point
from .bearing2dd import bearing2dd
__all__ = [
"add_survey_point",
"assign_survey_point",
"edit_survey_point",
"bearing2dd",
]
@@ -0,0 +1,47 @@
# 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
from ifcopenshell import entity_instance
import typing
def add_survey_point(file: ifcopenshell.file, survey_point: entity_instance, site:entity_instance = None) -> entity_instance:
"""
Adds a single survey point to the model based on IFC Concept Template 4.1.7.1.2.5.
Survey points are located relative to IfcRepresentationContext.WorldCoordinateSystem
:param survey_point: The survey point
:return: an IfcAnnotation entity
Example:
.. code:: python
annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint(4000.0,3500.0)))
"""
context = ifcopenshell.util.representation.get_context(file,"Model","Annotation","MODEL_VIEW")
shape_representation = file.createIfcShapeRepresentation(ContextOfItems=context,RepresentationIdentifier='Annotation',RepresentationType='Point',Items=[survey_point])
representation = file.createIfcProductDefinitionShape(Representations=[shape_representation])
annotation = file.createIfcAnnotation(ifcopenshell.guid.new(),ObjectPlacement=context.WorldCoordinateSystem,Representation=representation,PredefinedType="SURVEY")
if (site == None):
site = file.by_type("IfcSite")[0]
ifcopenshell.api.spatial.assign_container(file,relating_structure=site,products=[annotation])
return annotation
@@ -0,0 +1,38 @@
# 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
from ifcopenshell import entity_instance
import typing
def assign_survey_point(annotation: entity_instance, survey_point: entity_instance):
"""
Assigns a coordinate point to a survey point annotation
:param annotaton: The survey point annotation
:param survey_point: The survey point
:return: None
Example:
.. code:: python
annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint(4000.0,3500.0)))
ifcopenshell.api.cogo.assign_surve_point(annotation,file.createIfcCartesianPoint(4000.0,3500.0,100.0))
"""
annotation.Representation.Representations[0].Items = [survey_point]
@@ -0,0 +1,106 @@
# 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.util.geolocation
def bearing2dd(bearing: str)->float:
"""
Converts a quadrant bearing string to decimal degrees
The format of the string is "N|S dd (mm (ss.s)) E|W"
where:
N|S is N or S for North or South
dd is degree (required)
mm is minute (optional, but required if second is provided)
ss.s is second (required)
E|W is E or W for East or West
:param str: the bearing string
:return: Angle in radian
"""
error_msg = "Invalid bearing string"
bearing = bearing.strip() # trim external white space
bearing = ' '.join(bearing.split()) # make sure all parts separated by a single space
parts = bearing.split()
nParts = len(parts)
if nParts < 3 or 5 < nParts:
raise ValueError(error_msg)
cY = parts[0]
cY = cY.upper()
if cY != 'N' and cY != 'S':
raise ValueError(error_msg)
cX = parts[-1]
cX = cX.upper()
if cX != 'E' and cX != 'W':
raise ValueError(error_msg)
d = 0
m = 0
s = 0.
ms = 0
if nParts == 3:
d = int(parts[1])
elif nParts == 4:
d = int(parts[1])
m = int(parts[2])
elif nParts == 5:
d = int(parts[1])
m = int(parts[2])
s = float(parts[3])
# s in a decimal number
# need to break it into whole seconds and milliseconds
ms = 100.*(s - int(s))
s = int(s)
if d < 0 or (m < 0 or 60 <= m) or (s < 0 or 60 <= s) or ms < 0:
raise ValueError(error_msg)
if cY == 'N' and cX == 'E':
angle = 90.
sign = -1.
elif cY == 'N' and cX == 'W':
angle = 90.
sign = 1.
elif cY == 'S' and cX == 'E':
angle = 270.
sign = 1.
elif cY == 'S' and cX == 'W':
angle = 270.
sign = -1.
try:
dms = ifcopenshell.util.geolocation.dms2dd(d,m,s,ms)
except ValueError:
raise ValueError(error_msg)
if dms < 0. or 90. < dms:
raise ValueError(error_msg)
angle += sign*dms
# S 90 E will evaluate to 360
if angle == 360.:
angle = 0.
return angle
@@ -0,0 +1,40 @@
# 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
from ifcopenshell import entity_instance
import typing
def edit_survey_point(annotation: entity_instance, x:float,y:float,z:float=0.0):
"""
Edits the location of a previously defined survey point
:param survey_point: The survey point
:return: None
Example:
.. code:: python
annotation = ifcopenshell.api.cogo.add_survey_point(file,file.createIfcCartesianPoint(4000.0,3500.0)))
ifcopenshell.api.cogo.edit_surve_point(annotation,3500.0,2000.0)
"""
if annotation.Representation.Representations[0].Items[0].Dim == 2:
annotation.Representation.Representations[0].Items[0].Coordinates = ((x,y))
else:
annotation.Representation.Representations[0].Items[0].Coordinates = ((x,y,z))