Merge remote-tracking branch 'origin/v0.8.0' into tfk-rocksdb-storage

This commit is contained in:
Thomas Krijnen
2025-08-31 16:06:07 +02:00
52 changed files with 10039 additions and 450 deletions
@@ -60,7 +60,9 @@ from .create_segment_representations import create_segment_representations
from .create_representation import create_representation
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_alignment_layout_nest import get_alignment_layout_nest
from .get_alignment_segment_nest import get_alignment_segment_nest
from .get_alignment_start_station import get_alignment_start_station
from .get_curve_segment_transition_code import get_curve_segment_transition_code
from .get_layout_segments import get_layout_segments
from .get_horizontal_layout import get_horizontal_layout
@@ -74,6 +76,7 @@ from .get_curve import get_curve
from .get_layout_curve import get_layout_curve
from .get_mapped_segments import get_mapped_segments
from .get_parent_alignment import get_parent_alignment
from .get_referent_nest import get_referent_nest
from .has_zero_length_segment import has_zero_length_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
@@ -97,8 +100,10 @@ __all__ = [
"create_segment_representations",
"distance_along_from_station",
"get_alignment",
"get_alignment_layout_nest",
"get_alignment_layouts",
"get_alignment_station",
"get_alignment_segment_nest",
"get_alignment_start_station",
"get_axis_subcontext",
"get_basis_curve",
"get_cant_layout",
@@ -109,6 +114,7 @@ __all__ = [
"get_layout_curve",
"get_layout_segments",
"get_parent_alignment",
"get_referent_nest",
"get_vertical_layout",
"has_zero_length_segment",
"layout_horizontal_alignment_by_pi_method",
@@ -60,132 +60,93 @@ 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_start_station(file, alignment)
station = start_station + dist_along
# update the zero length layout segment
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
zero_length_segment = segment_nest.RelatedObjects[-1]
# 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.PositionedRelativeTo[0].RelatingPositioningElement
end_referent.Name = f"{_get_segment_start_point_label(zero_length_segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,start_station+start_dist_along)})"
# update the referent's geometric representation's location
end_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue = start_dist_along
settings = ifcopenshell.geom.settings()
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
curve_fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data)
curve_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, curve_fn)
p = curve_evaluator.evaluate(start_dist_along * unit_scale)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
@@ -200,6 +161,48 @@ 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_start_station(file, alignment)
end_referent_station = start_station + start_dist_along
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=end_referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": end_referent_station})
# create the start of segment referent
# get the previous segment. Working from the end of the basis curve, -1 is zero length segment
# -2 is the newly added segment, so -3 is the segment occuring just before the newly added segment
prev_segment = segment_nest.RelatedObjects[-3] if 2 < len(segment_nest.RelatedObjects) else None
name = f"{_get_segment_start_point_label(prev_segment,segment)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, distance_along=dist_along, station=station, name=name, positioned_product=segment
)
if len(curve.Segments) == 2 and layout.is_a("IfcAlignmentHorizontal"):
# this is the first real segment in the horizontal alignment
# update the location of the alignment's stationing referent
alignment = ifcopenshell.api.alignment.get_alignment(layout)
ref_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
stationing_referent = ref_nest.RelatedObjects[0]
p = curve_evaluator.evaluate(
stationing_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue
)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
y = float(p[1, 3]) / unit_scale
z = float(p[2, 3]) / unit_scale
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
stationing_referent.ObjectPlacement.CartesianPosition.Location.Coordinates = (x, y, z)
stationing_referent.ObjectPlacement.CartesianPosition.Axis.DirectionRatios = (ax, ay, az)
stationing_referent.ObjectPlacement.CartesianPosition.RefDirection.DirectionRatios = (rx, ry, rz)
@@ -48,8 +48,9 @@ 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_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
segment = segment_nest.RelatedObjects[-1]
alignment = ifcopenshell.api.alignment.get_alignment(layout)
station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
name = f"{_get_segment_start_point_label(segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(file, alignment, 0.0, station, name, segment)
@@ -30,19 +30,20 @@ import numpy as np
def add_stationing_referent(
file: ifcopenshell.file,
element: entity_instance,
alignment: entity_instance,
distance_along: float,
station: float,
name: str,
positioned_product: entity_instance,
) -> 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"
Adds an IfcReferent to the alignment with the Pset_Stationing property set.
:param element: the element to receive the referent, expected to be an IfcAlignment or IfcAlignmentSegment
:param distance_along: distance along the alignment curve
:param alignment: the alignment to receive the referent
:param distance_along: distance along the alignment basis curve
:param station: station value
:param name: name to assign to IfcReferent.Name, typically a stringized version of the station value
:param positioned_product: the product whose position is informed by the referent
:return: referent
Example:
@@ -50,13 +51,8 @@ def add_stationing_referent(
.. code:: python
alignment = model.by_type("IfcAlignment")[0]
ifcopenshell.api.alignment.add_stationing_referent(model,entity=alignment,distance_along=0.0,station=100.0)
ifcopenshell.api.alignment.add_stationing_referent(model,alignment=alignment,distance_along=0.0,station=100.0)
"""
alignment = element
if element.is_a("IfcAlignmentSegment"):
layout = element.Nests[0].RelatingObject
alignment = ifcopenshell.api.alignment.get_alignment(layout)
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
@@ -76,6 +72,8 @@ def add_stationing_referent(
)
is_valid_curve = True
if basis_curve.is_a("IfcCompositeCurve") and len(basis_curve.Segments) == 0:
is_valid_curve = False
if basis_curve.is_a("IfcPolyline") and len(basis_curve.Points) < 2:
is_valid_curve = False
elif basis_curve.is_a("IfcIndexedPolyCurve") and len(basis_curve.Points.CoordList) < 2:
@@ -91,6 +89,7 @@ def add_stationing_referent(
fn = ifcopenshell_wrapper.convert_loop_to_function_item(fn)
evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, fn)
p = evaluator.evaluate(distance_along * unit_scale)
p = np.array(p)
@@ -105,12 +104,23 @@ def add_stationing_referent(
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
else:
x = 0.0
y = 0.0
z = 0.0
rx = 1.0
ry = 0.0
rz = 0.0
ax = 0.0
ay = 0.0
az = 1.0
object_placement.CartesianPosition = file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint((x, y, z)),
Axis=file.createIfcDirection((ax, ay, az)),
RefDirection=file.createIfcDirection((rx, ry, rz)),
)
object_placement.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(
@@ -120,7 +130,6 @@ def add_stationing_referent(
# )
# create referent for the station
predefined_type = "STATION" if element.is_a("IfcAlignment") else "POSITION"
referent = file.createIfcReferent(
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
@@ -129,21 +138,27 @@ def add_stationing_referent(
ObjectType=None,
ObjectPlacement=object_placement,
Representation=representation,
PredefinedType=predefined_type,
PredefinedType="STATION",
)
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)
if len(alignment.Positions) == 0:
nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
nest.RelatedObjects += (referent,)
nest.RelatedObjects = sorted(
nest.RelatedObjects, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station")
)
if len(referent.Positions) == 0:
rel_positions = file.createIfcRelPositions(
GlobalId=ifcopenshell.guid.new(),
RelatingPositioningElement=alignment,
RelatingPositioningElement=referent,
RelatedProducts=[
referent,
positioned_product,
],
)
else:
alignment.Positions[0].RelatedProducts += (referent,)
referent.Positions[0].RelatedProducts += (positioned_product,)
return referent
@@ -50,9 +50,21 @@ def _move_vertical_layout_to_child_alignment(
# 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
# aggregate the child alignment to the parent alignment
ifcopenshell.api.aggregate.assign_object(file, products=[child_alignment], relating_object=parent_alignment)
# move all referents positioning segments of the vertical layout to the referent nest of the child alignment
child_referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, child_alignment)
parent_referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, parent_alignment)
for referent in parent_referent_nest.RelatedObjects:
for product in referent.Positions[0].RelatedProducts:
if product.is_a("IfcAlignmentSegment") and product.Nests[0].RelatingObject == vertical_layout:
# ifcopenshell.api.nest.change_nest(file,referent,child_alignment) - this doesn't work because referent is assigned to child_alignment.IsNestedBy[0].RelatedObjects
# and it needs to be assigned to child_alignment.IsNestedBy[1].RelatedObjects
# move the referent manually - unassign it and add it to the child alignment's referent nest
ifcopenshell.api.nest.unassign_object(file, [referent])
child_referent_nest.RelatedObjects += (referent,)
# 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:
@@ -231,8 +231,11 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in
if include_referent:
alignment = ifcopenshell.api.alignment.get_alignment(layout)
station = ifcopenshell.api.alignment.get_alignment_station(file, alignment)
station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
name = f"{_get_segment_start_point_label(zero_length_curve_segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
ifcopenshell.api.alignment.add_stationing_referent(file, zero_length_curve_segment, 0.0, station, name=name)
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, 0.0, station, name, zero_length_curve_segment
)
referent.Description = f"Positions zero length segment {zero_length_curve_segment.id()}"
return True
@@ -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,16 +77,17 @@ 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)
name = ifcopenshell.util.alignment.station_as_string(file, start_station)
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, 0.0, start_station, name, alignment
)
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)
# IFC 4.1.4.1.1 Alignment Aggregation To Project
project = file.by_type("IfcProject")[0]
if project:
@@ -53,11 +53,6 @@ def create_as_offset_curve(
_create_offset_curve_representation(file, alignment, offsets)
# 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]
if project:
@@ -137,14 +137,11 @@ def create_as_polyline(
Name=name,
)
# _create_layout(file,alignment,points)
_create_polyline_representation(file, alignment, points)
# 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)
referent = ifcopenshell.api.alignment.add_stationing_referent(file, alignment, 0.0, start_station, name, alignment)
# 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:
@@ -60,25 +61,28 @@ def create_layout_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
if curve:
if layout.is_a("IfcAlignmentHorizontal"):
if curve.is_a("IfcGradientCurve"):
curve = curve.BaseCurve
elif curve.is_a("IfcSegmentedReferenceCurve"):
curve = (
curve.BaseCurve.BaseCurve
) # layout is horizontal and curve is segmented ref ... we want the curve's base curve
elif layout.is_a("IfcAlignmentVertical"):
if curve.is_a("IfcSegmentedReferenceCurve"):
curve = curve.BaseCurve
# the new segment is two from the end... the end segment is zero length
curve_segment = curve.Segments[-2]
# 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
@@ -50,5 +50,6 @@ def create_representation(
layouts = ifcopenshell.api.alignment.get_alignment_layouts(alignment)
for layout in layouts:
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
for segment in layout.IsNestedBy[0].RelatedObjects:
layout_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
for segment in layout_nest.RelatedObjects:
_add_segment_to_curve(file, segment, curve)
@@ -43,6 +43,6 @@ def distance_along_from_station(file: ifcopenshell.file, alignment: entity_insta
print(dist_along) # 100.00
"""
start_station = ifcopenshell.api.alignment.get_alignment_station(file, alignment)
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
dist_along = station - start_station
return dist_along
@@ -16,16 +16,17 @@
# 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 collections.abc import Sequence
import ifcopenshell.util.representation
def get_alignment(layout: entity_instance) -> entity_instance:
"""
Returns the alignment that nests this layout
"""
return layout.Nests[0].RelatingObject if 0 < len(layout.Nests) else None
alignment = None
for nest in layout.Nests:
if nest.RelatingObject.is_a("IfcAlignment"):
alignment = nest.RelatingObject
break
return alignment
@@ -0,0 +1,37 @@
# 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_layout_nest(alignment: entity_instance) -> entity_instance:
"""
Searches for the IfcRelNest that contains IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
:param alignment: the alignment
:return: Returns the IfcRelNests containing the alignment layout
"""
layout_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
for nest in alignment.IsNestedBy:
for related_object in nest.RelatedObjects:
if related_object.is_a() in layout_types:
return nest
return None
@@ -0,0 +1,34 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from ifcopenshell import entity_instance
def get_alignment_segment_nest(layout: entity_instance) -> entity_instance:
"""
Searches for the IfcRelNest that contains IfcAlignmentSegment
:param layout: an alignment layout, expected to be one of IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
:return: Returns the IfcRelNests
"""
for nest in layout.IsNestedBy:
for related_object in nest.RelatedObjects:
if related_object.is_a("IfcAlignmentSegment"):
return nest
return None
@@ -22,11 +22,11 @@ import ifcopenshell.util.element
from ifcopenshell import entity_instance
def get_alignment_station(file: ifcopenshell.file, alignment: entity_instance) -> float:
def get_alignment_start_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.
Returns the start station of the alignment. The starting station is defined by the first nested IfcReferent.
This is interpreted to mean the first IfcReferent with an occurance of Pset_Stationing.Station,
otherwise returns 0.0.
"""
if not alignment.is_a("IfcAlignment"):
@@ -36,12 +36,13 @@ def get_alignment_station(file: ifcopenshell.file, alignment: entity_instance) -
parent_alignment = ifcopenshell.api.alignment.get_parent_alignment(alignment)
if parent_alignment:
start_station = ifcopenshell.api.alignment.get_alignment_station(file, parent_alignment)
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, parent_alignment)
else:
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":
if c.is_a("IfcReferent"):
start_station = ifcopenshell.util.element.get_pset(c, name="Pset_Stationing", prop="Station")
break
if not start_station == None:
break
return start_station
@@ -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_referent_nest(file: ifcopenshell.file, alignment: entity_instance) -> entity_instance:
"""
Searches for the IfcRelNest that contains IfcReferent. If one is not found, a empty IfcRelNests is created.
:param file:
:param alignment: The IfcAlignment which hosts IfcReferent
:return: Returns the IfcRelNests.
"""
if not alignment.is_a("IfcAlignment"):
raise TypeError(f"Expected IfcAlignment, instead received {alignment.is_a()}")
for nest in alignment.IsNestedBy:
for related_object in nest.RelatedObjects:
if related_object.is_a("IfcReferent"):
return nest
nest = file.createIfcRelNests(GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=[])
return nest
@@ -162,3 +162,12 @@ def print_composite_curve_deep(curve):
print(" " * 4, segment.Placement)
print(" " * 4, segment.Placement.Location)
print(" " * 4, segment.Placement.RefDirection)
def print_positioned_products(file: ifcopenshell.file):
referents = file.by_type("IfcReferent")
for referent in referents:
print(referent)
for rel in referent.Positions:
for product in rel.RelatedProducts:
print(" " * 2, product)