Alignments can now be created with or without geometric representations

This commit is contained in:
Richard Brice
2025-08-25 16:05:47 -07:00
parent 8903d6a85c
commit 9331e89d15
6 changed files with 248 additions and 169 deletions
@@ -60,132 +60,92 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg
# 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)
if curve:
# 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
# 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
# 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 = ifcopenshell.api.alignment.get_mapped_segments(segment)
mapped_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
# compute the end point matrix
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, mapped_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
# update the zero length segment semantic representation parameters
if zero_length_segment.DesignParameters.is_a("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
# the length of the current segment is in dist_along, so subtract it out
dist_along -= segment.DesignParameters.SegmentLength
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
dist_along = segment.DesignParameters.StartDistAlong
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.alignment.station_as_string(file,start_station+start_dist_along)})"
# update the referent's geometric representation's location
end_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue = start_dist_along
settings = ifcopenshell.geom.settings()
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
curve_fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data)
curve_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, curve_fn)
p = curve_evaluator.evaluate(start_dist_along * unit_scale)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
y = float(p[1, 3]) / unit_scale
z = float(p[2, 3]) / unit_scale
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
end_referent.ObjectPlacement.CartesianPosition.Location.Coordinates = (x, y, z)
end_referent.ObjectPlacement.CartesianPosition.Axis.DirectionRatios = (ax, ay, az)
end_referent.ObjectPlacement.CartesianPosition.RefDirection.DirectionRatios = (rx, ry, rz)
start_station = ifcopenshell.api.alignment.get_alignment_station(file, alignment)
end_referent_station = start_station + start_dist_along
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=end_referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": end_referent_station})
# create the start of segment referent
# get the previous segment. Working from the end of the basis curve, -1 is zero length segment
# -2 is the newly added segment, so -3 is the segment occuring just before the newly added segment
prev_segment = 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.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, segment, 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
# get the station of the start of the segment
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
)
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 = ifcopenshell.api.alignment.get_mapped_segments(segment)
mapped_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
# compute the end point matrix
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, mapped_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
# update the zero length segment semantic representation parameters
if zero_length_segment.DesignParameters.is_a("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.alignment.station_as_string(file,start_station+start_dist_along)})"
# update the referent's geometric representation's location
end_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue = start_dist_along
settings = ifcopenshell.geom.settings()
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
curve_fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data)
curve_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, curve_fn)
p = curve_evaluator.evaluate(start_dist_along * unit_scale)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
@@ -200,6 +160,47 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg
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)
end_referent.ObjectPlacement.CartesianPosition.Location.Coordinates = (x, y, z)
end_referent.ObjectPlacement.CartesianPosition.Axis.DirectionRatios = (ax, ay, az)
end_referent.ObjectPlacement.CartesianPosition.RefDirection.DirectionRatios = (rx, ry, rz)
start_station = ifcopenshell.api.alignment.get_alignment_station(file, alignment)
end_referent_station = start_station + start_dist_along
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=end_referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": end_referent_station})
# create the start of segment referent
# get the previous segment. Working from the end of the basis curve, -1 is zero length segment
# -2 is the newly added segment, so -3 is the segment occuring just before the newly added segment
prev_segment = 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.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, segment, 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)
@@ -48,8 +48,8 @@ def _add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance) -
if curve:
ifcopenshell.api.alignment.add_zero_length_segment(file, curve)
segment = layout.IsNestedBy[0].RelatedObjects[-1]
alignment = ifcopenshell.api.alignment.get_alignment(layout)
station = ifcopenshell.api.alignment.get_alignment_station(file, alignment)
name = f"{_get_segment_start_point_label(segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
ifcopenshell.api.alignment.add_stationing_referent(file, segment, 0.0, station, name=name)
segment = layout.IsNestedBy[0].RelatedObjects[-1]
alignment = ifcopenshell.api.alignment.get_alignment(layout)
station = ifcopenshell.api.alignment.get_alignment_station(file, alignment)
name = f"{_get_segment_start_point_label(segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
ifcopenshell.api.alignment.add_stationing_referent(file, segment, 0.0, station, name=name)
@@ -33,27 +33,31 @@ def create(
name: str,
include_vertical: bool = False,
include_cant: bool = False,
include_geometry: bool = True,
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.
Creates a new alignment with a horizontal layout. Optionally, vertical and cant layouts can be created as well.
The geometric representations are created as well, unless they are explicitly excluded.
Zero length segments are added at the end of the layouts and geometric representations.
The alignment is automatically aggreated to the project if it exists.
Zero length segments are added at the end.
Use get_horizontal_layout(alignment), get_vertical_layout(alignment) and get_cant_layout(alignment) to get the
corresponding IfcAlignmentHorizontal, IfcAlignmentVertical, and IfcAlignmentCant layout entities.
The IfcAlignment is aggreated to IfcProject
If the alignment has Viennese Bend transition curves, create the segments in the cant layout before the horizontal layout using create_layout_segment().
The horizontal geometry in the Viennese Bend transition curves depends on the Viennese Bend cant parameters. create_layout_segment() automatically creates
the geometric representation from the semantic definition. The horizontal segment geometric representation will fail if the cant segment is not defined.
Use get_horizontal_layout(alignment) to get the IfcAlignmentHorizontal layout.
If the alignment has Viennese Bend transition curves, create the cant layout before the horizontal layout. This is because the horizontal layout
in the Viennese Bend transition curves depends on the Viennese Bend cant parameters.
If geometric representations are created, the alignment stationing referent is also created using the start_station value. IfcReferent.ObjectPlacement
is required for linear positiion elements and IfcLinearPlacement is defined relative to alignment curve geometry.
: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
:param include_vertical: If True, IfcAlignmentVertical is created. IfcGradientCurve is created if include_geometry is True
:param include_cant: If True, IfcAlignmentCant is created. IfcSegmentedReferenceCurve is created if include_geometry is True
:param include_geometry: If True, the geometric representations are added
:param start_station: station value at the start of the alignment.
:return: Returns an IfcAlignment
"""
alignment = file.createIfcAlignment(
@@ -73,15 +77,18 @@ def create(
ifcopenshell.api.nest.assign_object(file, related_objects=alignment_layouts, relating_object=alignment)
_create_geometric_representation(file, alignment)
if include_geometry:
_create_geometric_representation(file, alignment)
for layout in alignment_layouts:
_add_zero_length_segment(file, layout)
# define stationing
name = ifcopenshell.util.alignment.station_as_string(file, start_station)
referent = ifcopenshell.api.alignment.add_stationing_referent(file, alignment, 0.0, start_station, name)
ifcopenshell.api.nest.reorder_nesting(file, referent, -1, 0)
if include_geometry:
# define stationing
name = ifcopenshell.util.alignment.station_as_string(file, start_station)
referent = ifcopenshell.api.alignment.add_stationing_referent(file, alignment, 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]
@@ -23,20 +23,21 @@ from ifcopenshell import entity_instance
import math
from ifcopenshell import ifcopenshell_wrapper
import numpy as np
from typing import Union
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:
) -> Union[np.array, None]:
"""
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
The new segment is appended to the layout alignment and the corresponding IfcCurveSegment is created in the geometric representation if it exists.
: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.
:return: 4x4 matrix at end of segment as np.array intended to be used as the start point geometry for the next segment or None if there is the geometric representation is not defined.
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
@@ -59,26 +60,29 @@ def create_layout_segment(
# returned and used when defining the next segment
alignment = ifcopenshell.api.alignment.get_alignment(layout)
curve = ifcopenshell.api.alignment.get_curve(alignment)
if curve:
if layout.is_a("IfcAlignmentHorizontal"):
if curve.is_a("IfcGradientCurve"):
curve = curve.BaseCurve
elif curve.is_a("IfcSegmentedReferenceCurve"):
curve = (
curve.BaseCurve.BaseCurve
) # layout is horizontal and curve is segmented ref ... we want the curve's base curve
elif layout.is_a("IfcAlignmentVertical"):
if curve.is_a("IfcSegmentedReferenceCurve"):
curve = curve.BaseCurve
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]
# the new segment is two from the end... the end segment is zero length
curve_segment = curve.Segments[-2]
settings = ifcopenshell.geom.settings()
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)
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
return end
else:
return None
@@ -21,12 +21,11 @@ import ifcopenshell.api.alignment
import ifcopenshell.util.element
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.
otherwise returns 0.0.
"""
if not alignment.is_a("IfcAlignment"):