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)
@@ -18,6 +18,7 @@
from __future__ import annotations
import inspect
import random
import operator
import warnings
@@ -225,10 +226,24 @@ def serialize_shape(shape):
shapes.SetFormatNb(2)
shapes.Add(shape)
# Check if WriteToString method exists and has the correct signature
# In PythonOCC >= 7.8.0, WriteToString signature changed and requires additional arguments
if hasattr(shapes, "WriteToString"):
return shapes.WriteToString()
else:
return shapes.Write()
try:
# Try to get the method signature
sig = inspect.signature(shapes.WriteToString)
# If WriteToString has no parameters (just self), use it
# This works for PythonOCC < 7.8.0
if len(sig.parameters) == 0:
return shapes.WriteToString()
except (ValueError, TypeError):
# If signature inspection fails, fall through to Write() method
pass
# Fall back to Write() method for newer PythonOCC versions (>= 7.8.0)
# or when WriteToString is not available/compatible
return shapes.Write()
def create_shape_from_serialization(
@@ -68,13 +68,10 @@ def test_add_segment_to_layout():
_add_segment_to_layout(file, horizontal_alignment, alignment_segment)
assert len(horizontal_alignment.IsNestedBy) == 1
assert (
len(horizontal_alignment.IsNestedBy[0].RelatedObjects) == 2
) # The the segment we added and the automatically created zero length segment
assert horizontal_alignment.IsNestedBy[0].RelatedObjects[0] == alignment_segment
assert (
alignment_segment.IsNestedBy[0].RelatedObjects[0].is_a("IfcReferent")
) # a referent is automatically added at the start of the segment
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal_alignment)
assert len(segment_nest.RelatedObjects) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 3
test_add_segment_to_layout()
@@ -39,14 +39,14 @@ def test_add_stationing_to_alignment():
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", start_station=2000.0)
for rel in alignment.IsNestedBy:
for referent in rel.RelatedObjects:
if referent.is_a("IfcReferent"):
assert referent.PredefinedType == "STATION"
assert referent.Name == "2+000.000"
assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing")
assert (
ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing", prop="Station")
== 2000.0
)
assert referent.ObjectPlacement != None
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
referent = referent_nest.RelatedObjects[0]
assert referent.PredefinedType == "STATION"
assert referent.Name == "2+000.000"
assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing")
assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing", prop="Station") == 2000.0
assert referent.ObjectPlacement != None
test_add_stationing_to_alignment()
@@ -32,23 +32,25 @@ def test_add_vertical_alignment():
alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False)
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
assert len(alignment.IsNestedBy) == 1 # one nest
assert len(alignment.IsNestedBy[0].RelatedObjects) == 2 # nesting IfcReferent, IfcAlignmentHorizontal
assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcReferent")
assert alignment.IsNestedBy[0].RelatedObjects[1].is_a("IfcAlignmentHorizontal")
assert len(alignment.IsNestedBy) == 2 # nests for layout and referents
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
assert len(layout_nest.RelatedObjects) == 1
assert layout_nest.RelatedObjects[0].is_a("IfcAlignmentHorizontal")
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 2
assert referent_nest.RelatedObjects[0].is_a("IfcReferent")
curve = ifcopenshell.api.alignment.get_curve(alignment)
assert curve.is_a("IfcCompositeCurve")
vertical_layout = ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
assert len(alignment.IsNestedBy) == 1 # one nest
assert (
len(alignment.IsNestedBy[0].RelatedObjects) == 3
) # nesting IfcReferent, IfcAlignmentHorizontal, IfcAlignmentVertical
assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcReferent")
assert alignment.IsNestedBy[0].RelatedObjects[1].is_a("IfcAlignmentHorizontal")
assert alignment.IsNestedBy[0].RelatedObjects[2].is_a("IfcAlignmentVertical")
assert len(alignment.IsNestedBy) == 2
assert len(layout_nest.RelatedObjects) == 2
assert layout_nest.RelatedObjects[0].is_a("IfcAlignmentHorizontal")
assert layout_nest.RelatedObjects[1].is_a("IfcAlignmentVertical")
curve = ifcopenshell.api.alignment.get_curve(alignment)
assert curve.is_a("IfcGradientCurve")
@@ -56,20 +58,18 @@ def test_add_vertical_alignment():
vertical_layout = ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
assert len(alignment.IsDecomposedBy) == 1 # 1 IfcRelAggreates relationship for the child algiments
assert (
len(alignment.IsDecomposedBy[0].RelatedObjects) == 2
) # two child alignments, one for the first vertical and one for the vertical just added
assert len(alignment.IsDecomposedBy[0].RelatedObjects) == 2
for child_alignment in alignment.IsDecomposedBy[0].RelatedObjects:
assert child_alignment.is_a("IfcAlignment")
assert len(child_alignment.IsNestedBy) == 1 # one nesting relationship for the IfcAlignmentVertical
assert len(child_alignment.IsNestedBy[0].RelatedObjects) == 1 # The IfcAlignmentVertical
assert child_alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentVertical")
assert len(child_alignment.IsNestedBy) == 2
child_layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(child_alignment)
assert len(child_layout_nest.RelatedObjects) == 1 # The IfcAlignmentVertical
assert child_layout_nest.RelatedObjects[0].is_a("IfcAlignmentVertical")
assert len(alignment.IsNestedBy) == 1 # 1 nesting relationsip for the alignments
assert len(alignment.IsNestedBy[0].RelatedObjects) == 2 # nesting IfcReferent and IfcAlignmentHorizontal
assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcReferent")
assert alignment.IsNestedBy[0].RelatedObjects[1].is_a("IfcAlignmentHorizontal")
assert len(alignment.IsNestedBy) == 2
assert len(layout_nest.RelatedObjects) == 1
assert layout_nest.RelatedObjects[0].is_a("IfcAlignmentHorizontal")
test_add_vertical_alignment()
@@ -46,22 +46,21 @@ def test_create_by_pi_method():
)
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
assert len(alignment.IsNestedBy) == 1 # one nest
assert (
len(alignment.IsNestedBy[0].RelatedObjects) == 3
) # nesting IfcReferent, IfcAlignmentHorizontal, IfcAlignmentVertical
assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcReferent")
assert alignment.IsNestedBy[0].RelatedObjects[1].is_a("IfcAlignmentHorizontal")
assert alignment.IsNestedBy[0].RelatedObjects[2].is_a("IfcAlignmentVertical")
assert (
len(alignment.IsNestedBy[0].RelatedObjects[1].IsNestedBy) == 1
) # nesting of segments beneath IfcAlignmentHorizontal
assert (
len(alignment.IsNestedBy[0].RelatedObjects[1].IsNestedBy[0].RelatedObjects) == 8
) # segments in horizontal layout
assert (
len(alignment.IsNestedBy[0].RelatedObjects[2].IsNestedBy[0].RelatedObjects) == 10
) # segments in vertical layout
assert len(alignment.IsNestedBy) == 2
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
assert len(layout_nest.RelatedObjects) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 19
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
horizontal_segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal_layout)
assert len(horizontal_segment_nest.RelatedObjects) == 8
vertical_layout = ifcopenshell.api.alignment.get_vertical_layout(alignment)
vertical_segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(vertical_layout)
assert len(vertical_segment_nest.RelatedObjects) == 10
test_create_by_pi_method()
@@ -0,0 +1,68 @@
# 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 pytest
import ifcopenshell.api.alignment
import ifcopenshell.api.context
import ifcopenshell.api.unit
def test_create_no_geometry():
file = ifcopenshell.file(schema="IFC4X3_ADD2")
project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="Test")
length = ifcopenshell.api.unit.add_si_unit(file, unit_type="LENGTHUNIT")
ifcopenshell.api.unit.assign_unit(file, units=[length])
# creates an IfcAlignment with an IfcAlignmentHorizontal layout containing only the zero length segment
ali = ifcopenshell.api.alignment.create(file, "A1", include_vertical=True, include_geometry=False)
# append a segment to the horizontal layout
horizontal_alignment = ifcopenshell.api.alignment.get_horizontal_layout(ali)
vertical_alignment = ifcopenshell.api.alignment.get_vertical_layout(ali)
curve = ifcopenshell.api.alignment.get_curve(ali)
assert curve == None
design_parameters = file.create_entity(
type="IfcAlignmentHorizontalSegment",
StartTag=None,
EndTag=None,
StartPoint=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))),
StartDirection=0.0,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=100.0,
GravityCenterLineHeight=None,
PredefinedType="LINE",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, horizontal_alignment, design_parameters)
assert end == None
design_parameters = file.createIfcAlignmentVerticalSegment(
StartDistAlong=0.0,
HorizontalLength=50.0,
StartHeight=20.0,
StartGradient=1.0 / 100.0,
EndGradient=1.0 / 100.0,
PredefinedType="CONSTANTGRADIENT",
)
end = ifcopenshell.api.alignment.create_layout_segment(file, vertical_alignment, design_parameters)
assert end == None
test_create_no_geometry()
@@ -45,16 +45,13 @@ def test_horizontal_layout_by_pi_method():
alignment = ifcopenshell.api.alignment.create_by_pi_method(file, "TestAlignment", coordinates, radii)
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
assert len(alignment.IsNestedBy) == 1 # one nest
assert len(alignment.IsNestedBy[0].RelatedObjects) == 2 # nesting IfcReferent, IfcAlignmentHorizontal
assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcReferent")
assert alignment.IsNestedBy[0].RelatedObjects[1].is_a("IfcAlignmentHorizontal")
assert (
len(alignment.IsNestedBy[0].RelatedObjects[1].IsNestedBy) == 1
) # nesting of segments beneath IfcAlignmentHorizontal
assert (
len(alignment.IsNestedBy[0].RelatedObjects[1].IsNestedBy[0].RelatedObjects) == 3
) # segments in horizontal layout
assert len(alignment.IsNestedBy) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
assert referent_nest.RelatedObjects[0].is_a("IfcReferent")
assert layout_nest.RelatedObjects[0].is_a("IfcAlignmentHorizontal")
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout_nest.RelatedObjects[0])
assert len(segment_nest.RelatedObjects) == 3 # segments in horizontal layout
test_horizontal_layout_by_pi_method()
@@ -45,12 +45,15 @@ def test_name_segments():
file, "TestAlignment", coordinates, radii, vpoints, lengths
)
for rel in alignment.IsNestedBy:
for a in rel.RelatedObjects:
if a.is_a("IfcLinearElement"):
ifcopenshell.api.alignment.name_segments("Q", a)
i = 1
for sr in a.IsNestedBy:
for s in sr.RelatedObjects:
assert f"Q{i}" == s.Name
i += 1
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
for layout in layout_nest.RelatedObjects:
assert layout.is_a("IfcLinearElement")
ifcopenshell.api.alignment.name_segments("Q", layout)
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
i = 1
for segment in segment_nest.RelatedObjects:
assert f"Q{i}" == segment.Name
i += 1
test_name_segments()
@@ -97,32 +97,16 @@ def callback_alignment():
def test_with_default_names(default_names_alignment):
hlayout = ifcopenshell.api.alignment.get_horizontal_layout(default_names_alignment)
referent_nest = ifcopenshell.api.alignment.get_referent_nest(None, default_names_alignment)
assert "P.O.B." in hlayout.IsNestedBy[0].RelatedObjects[0].IsNestedBy[0].RelatedObjects[0].Name
assert "P.C." in hlayout.IsNestedBy[0].RelatedObjects[1].IsNestedBy[0].RelatedObjects[0].Name
assert "P.T." in hlayout.IsNestedBy[0].RelatedObjects[2].IsNestedBy[0].RelatedObjects[0].Name
assert "P.O.E." in hlayout.IsNestedBy[0].RelatedObjects[-1].IsNestedBy[0].RelatedObjects[0].Name
vlayout = ifcopenshell.api.alignment.get_vertical_layout(default_names_alignment)
assert "V.P.O.B." in vlayout.IsNestedBy[0].RelatedObjects[0].IsNestedBy[0].RelatedObjects[0].Name
assert "P.V.C." in vlayout.IsNestedBy[0].RelatedObjects[1].IsNestedBy[0].RelatedObjects[0].Name
assert "P.V.T." in vlayout.IsNestedBy[0].RelatedObjects[2].IsNestedBy[0].RelatedObjects[0].Name
assert "V.P.O.E." in vlayout.IsNestedBy[0].RelatedObjects[-1].IsNestedBy[0].RelatedObjects[0].Name
expected = ["P.O.B", "P.C.", "P.T.", "P.O.E.", "V.P.O.B.", "P.V.C.", "P.V.T.", "V.P.O.E"]
for r in referent_nest.RelatedObjects:
assert [x in r.Name for x in expected]
def test_with_callbacks(callback_alignment):
hlayout = ifcopenshell.api.alignment.get_horizontal_layout(callback_alignment)
referent_nest = ifcopenshell.api.alignment.get_referent_nest(None, callback_alignment)
assert "A" in hlayout.IsNestedBy[0].RelatedObjects[0].IsNestedBy[0].RelatedObjects[0].Name
assert "Q" in hlayout.IsNestedBy[0].RelatedObjects[1].IsNestedBy[0].RelatedObjects[0].Name
assert "Q" in hlayout.IsNestedBy[0].RelatedObjects[2].IsNestedBy[0].RelatedObjects[0].Name
assert "Z" in hlayout.IsNestedBy[0].RelatedObjects[-1].IsNestedBy[0].RelatedObjects[0].Name
vlayout = ifcopenshell.api.alignment.get_vertical_layout(callback_alignment)
assert "a" in vlayout.IsNestedBy[0].RelatedObjects[0].IsNestedBy[0].RelatedObjects[0].Name
assert "q" in vlayout.IsNestedBy[0].RelatedObjects[1].IsNestedBy[0].RelatedObjects[0].Name
assert "q" in vlayout.IsNestedBy[0].RelatedObjects[2].IsNestedBy[0].RelatedObjects[0].Name
assert "z" in vlayout.IsNestedBy[0].RelatedObjects[-1].IsNestedBy[0].RelatedObjects[0].Name
expected = ["A", "Q", "Z", "a", "q", "z"]
for r in referent_nest.RelatedObjects:
assert [x in r.Name for x in expected]
@@ -58,22 +58,17 @@ def test_vertical_layout_by_pi_method():
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(file, vlayout, vpoints, lengths)
assert len(alignment.IsDecomposedBy) == 0 # no child alignments
assert len(alignment.IsNestedBy) == 1 # one nest
assert (
len(alignment.IsNestedBy[0].RelatedObjects) == 3
) # nesting IfcReferent, IfcAlignmentHorizontal, IfcAlignmentVertical
assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcReferent")
assert alignment.IsNestedBy[0].RelatedObjects[1].is_a("IfcAlignmentHorizontal")
assert alignment.IsNestedBy[0].RelatedObjects[2].is_a("IfcAlignmentVertical")
assert (
len(alignment.IsNestedBy[0].RelatedObjects[1].IsNestedBy) == 1
) # nesting of segments beneath IfcAlignmentHorizontal
assert (
len(alignment.IsNestedBy[0].RelatedObjects[1].IsNestedBy[0].RelatedObjects) == 2
) # segments in horizontal layout
assert (
len(alignment.IsNestedBy[0].RelatedObjects[2].IsNestedBy[0].RelatedObjects) == 3
) # segments in vertical layout
assert len(alignment.IsNestedBy) == 2
layout_nest = ifcopenshell.api.alignment.get_alignment_layout_nest(alignment)
assert len(layout_nest.RelatedObjects) == 2
referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
assert len(referent_nest.RelatedObjects) == 6
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(vlayout)
assert len(segment_nest.RelatedObjects) == 3
test_vertical_layout_by_pi_method()
+119
View File
@@ -0,0 +1,119 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2025-02-14T16:16:53',(''),(''),'IfcOpenShell 0.8.1-c49ca69','IfcOpenShell 0.8.1-c49ca69','');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'Pravin.Muthukrishnan',$,$,$,$,$);
#2=IFCORGANIZATION($,'BG&E Pty Ltd','BG&E Pty Ltd',$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'15.0C1n','12d Model','12d Model');
#5=IFCOWNERHISTORY(#3,#4,$,.NOCHANGE.,$,$,$,1739508662);
#6=IFCCARTESIANPOINT((0.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCDIRECTION((1.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#6,#7,#8);
#10=IFCDIRECTION((0.,1.,0.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-06,#9,#10);
#12=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#13=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#14=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#15=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#16=IFCUNITASSIGNMENT((#12,#13,#14,#15));
#17=IFCPROJECT('3f4tUCcZz5OBp_RAlF3Yjd',#5,'DRAINAGE DESIGN','No description set',$,$,$,(#11),#16);
#18=IFCPERSON($,$,'Pravin.Muthukrishnan',$,$,$,$,$);
#19=IFCORGANIZATION($,'BG&E Pty Ltd','BG&E Pty Ltd',$,$);
#20=IFCPERSONANDORGANIZATION(#18,#19,$);
#21=IFCAPPLICATION(#19,'15.0C1n','12d Model','12d Model');
#22=IFCOWNERHISTORY(#20,#21,$,.NOCHANGE.,$,$,$,1739508662);
#30=IFCCARTESIANPOINT((0.,0.));
#31=IFCAXIS2PLACEMENT2D(#30,$);
#32=IFCCIRCLEPROFILEDEF(.AREA.,$,#31,0.179);
#33=IFCCARTESIANPOINT((289.080,582.2,118.886));
#34=IFCCARTESIANPOINT((290.081,582.9,118.886));
#35=IFCPOLYLINE((#33,#34));
#36=IFCDIRECTION((1.,0.,0.));
#37=IFCFIXEDREFERENCESWEPTAREASOLID(#32,$,#35,$,$,#36);
#38=IFCSHAPEREPRESENTATION(#11,'Body','AdvancedSweptSolid',(#37));
#39=IFCPRODUCTDEFINITIONSHAPE($,$,(#38));
#40=IFCFLOWSEGMENT('1iJOpv7cX1SBoQLaHDDolV',#108,'5','5-1 to 1-1','EXISTING (RETAINED)',#103,#39,$);
#41=IFCRELDEFINESBYPROPERTIES('10rIwnj8r8RR3PKc1TXqSB',#22,'12d Model','Attributes',(#40),#94);
#42=IFCPROPERTYSINGLEVALUE('calculated critical cover chainage',$,IFCREAL(2.24873590275508),$);
#43=IFCPROPERTYSINGLEVALUE('nominal diameter',$,IFCREAL(0.3),$);
#44=IFCPROPERTYSINGLEVALUE('pipe id',$,IFCINTEGER(35),$);
#45=IFCPROPERTYSINGLEVALUE('colour',$,IFCLABEL('orange'),$);
#46=IFCPROPERTYSINGLEVALUE('group',$,IFCLABEL('EX'),$);
#47=IFCCOMPLEXPROPERTY('pipe sched','12d_attribute_group',$,(#46));
#48=IFCPROPERTYSINGLEVALUE('name',$,IFCLABEL('EX'),$);
#49=IFCCOMPLEXPROPERTY('lplot','12d_attribute_group',$,(#48));
#50=IFCPROPERTYSINGLEVALUE('pipe name',$,IFCLABEL('5-1 to 1-1'),$);
#51=IFCPROPERTYSINGLEVALUE('calculated direct pipe flow total minor',$,IFCREAL(0.),$);
#52=IFCPROPERTYSINGLEVALUE('calculated direct pipe flow total major',$,IFCREAL(0.),$);
#53=IFCPROPERTYSINGLEVALUE('calculated direct pit and pipe flow total minor',$,IFCREAL(0.),$);
#54=IFCPROPERTYSINGLEVALUE('calculated direct pit and pipe flow total major',$,IFCREAL(0.),$);
#55=IFCPROPERTYSINGLEVALUE('pipe type',$,IFCLABEL('EXISTING (RETAINED)'),$);
#56=IFCPROPERTYSINGLEVALUE('roughness text',$,IFCLABEL('n=0.013'),$);
#57=IFCPROPERTYSINGLEVALUE('calculated design grade minimum',$,IFCREAL(1.),$);
#58=IFCPROPERTYSINGLEVALUE('calculated pipe length',$,IFCREAL(7.19890895752825),$);
#59=IFCPROPERTYSINGLEVALUE('invert us',$,IFCREAL(118.736),$);
#60=IFCPROPERTYSINGLEVALUE('invert ds',$,IFCREAL(118.736),$);
#61=IFCPROPERTYSINGLEVALUE('calculated pipe grade',$,IFCREAL(-0.),$);
#62=IFCPROPERTYSINGLEVALUE('calculated pipe vert angle',$,IFCREAL(0.),$);
#63=IFCPROPERTYSINGLEVALUE('calculated pipe grade 1 in',$,IFCREAL(1000000.),$);
#64=IFCPROPERTYSINGLEVALUE('pipe size',$,IFCLABEL('300'),$);
#65=IFCPROPERTYSINGLEVALUE('diameter',$,IFCREAL(0.3),$);
#66=IFCPROPERTYSINGLEVALUE('nominal pipe size',$,IFCLABEL('300'),$);
#67=IFCPROPERTYSINGLEVALUE('calculated us deflection',$,IFCREAL(0.),$);
#68=IFCPROPERTYSINGLEVALUE('calculated ds deflection',$,IFCREAL(89.4496469263205),$);
#69=IFCPROPERTYSINGLEVALUE('calculated design drop',$,IFCREAL(0.02),$);
#70=IFCPROPERTYSINGLEVALUE('calculated drop',$,IFCREAL(0.),$);
#71=IFCPROPERTYSINGLEVALUE('calculated pipe vert ds deflection angle',$,IFCREAL(-4.09980829540645),$);
#72=IFCPROPERTYSINGLEVALUE('calculated design cover',$,IFCREAL(1.1),$);
#73=IFCPROPERTYSINGLEVALUE('minimum cover',$,IFCREAL(1.67561504823626),$);
#74=IFCPROPERTYSINGLEVALUE('pipe colour',$,IFCINTEGER(8),$);
#75=IFCPROPERTYSINGLEVALUE('conduit shape',$,IFCLABEL('Circular'),$);
#76=IFCPROPERTYSINGLEVALUE('calculated invert us',$,IFCREAL(118.736),$);
#77=IFCPROPERTYSINGLEVALUE('calculated invert ds',$,IFCREAL(118.736),$);
#78=IFCPROPERTYSINGLEVALUE('calculated hgl us',$,IFCREAL(118.736),$);
#79=IFCPROPERTYSINGLEVALUE('calculated hgl ds',$,IFCREAL(118.736),$);
#80=IFCPROPERTYSINGLEVALUE('lock us il',$,IFCINTEGER(1),$);
#81=IFCPROPERTYSINGLEVALUE('lock ds il',$,IFCINTEGER(1),$);
#82=IFCPROPERTYSINGLEVALUE('critical barrel',$,IFCINTEGER(0),$);
#83=IFCPROPERTYSINGLEVALUE('critical side',$,IFCINTEGER(0),$);
#84=IFCPROPERTYSINGLEVALUE('Pipe Name',$,IFCLABEL('5-1 to 1-1'),$);
#85=IFCPROPERTYSINGLEVALUE('Pipe Type',$,IFCLABEL('EXISTING (RETAINED)'),$);
#86=IFCPROPERTYSINGLEVALUE('Justification',$,IFCLABEL('Invert'),$);
#87=IFCPROPERTYSINGLEVALUE('Start x',$,IFCREAL(289080.818499209),$);
#88=IFCPROPERTYSINGLEVALUE('Start y',$,IFCREAL(5822851.36621952),$);
#89=IFCPROPERTYSINGLEVALUE('Start z',$,IFCREAL(118.736),$);
#90=IFCPROPERTYSINGLEVALUE('End x',$,IFCREAL(289081.570466963),$);
#91=IFCPROPERTYSINGLEVALUE('End y',$,IFCREAL(5822857.54453541),$);
#92=IFCPROPERTYSINGLEVALUE('End z',$,IFCREAL(118.736),$);
#93=IFCCOMPLEXPROPERTY('Geometry','12d_attribute_group',$,(#86,#87,#88,#89,#90,#91,#92));
#94=IFCPROPERTYSET('0pqqHlNsb54w9VRYcNog0C',#22,'12d Model',$,(#42,#43,#44,#45,#47,#49,#50,#51,#52,#53,#54,#55,#56,#57,#58,#59,#60,#61,#62,#63,#64,#65,#66,#67,#68,#69,#70,#71,#72,#73,#74,#75,#76,#77,#78,#79,#80,#81,#82,#83,#84,#85,#93));
#95=IFCSTYLEDITEM(#37,(#98),$);
#96=IFCCOLOURRGB('orange',1.,0.647058823529412,0.);
#97=IFCSURFACESTYLERENDERING(#96,$,$,$,$,$,IFCNORMALISEDRATIOMEASURE(0.00390625),IFCSPECULAREXPONENT(10.),.NOTDEFINED.);
#98=IFCSURFACESTYLE($,.POSITIVE.,(#97));
#99=IFCCARTESIANPOINT((0.,0.,0.));
#100=IFCDIRECTION((0.,0.,1.));
#101=IFCDIRECTION((1.,0.,0.));
#102=IFCAXIS2PLACEMENT3D(#99,#100,#101);
#103=IFCLOCALPLACEMENT($,#102);
#104=IFCACTORROLE(.USERDEFINED.,'CONTRIBUTOR',$);
#105=IFCTELECOMADDRESS(.USERDEFINED.,$,'WEBPAGE',$,$,$,$,'https://ifcopenshell.org',$);
#106=IFCORGANIZATION('IfcOpenShell','IfcOpenShell','IfcOpenShell is an open source software library that helps users and software developers to work with IFC data.',(#104),(#105));
#107=IFCAPPLICATION(#106,'0.8.1-alpha250208-3832077','Bonsai','Bonsai');
#108=IFCOWNERHISTORY(#20,#21,$,.MODIFIED.,1739510213,#3,#107,1739508662);
#114=IFCSITE('2CTfxIYCTCsRAvUEqFZoH3',#120,'Default Site','Description of Default Site',$,#119,$,$,.ELEMENT.,$,$,$,$,$);
#115=IFCCARTESIANPOINT((0.,0.,0.));
#116=IFCDIRECTION((0.,0.,1.));
#117=IFCDIRECTION((1.,0.,0.));
#118=IFCAXIS2PLACEMENT3D(#115,#116,#117);
#119=IFCLOCALPLACEMENT($,#118);
#120=IFCOWNERHISTORY(#20,#21,$,.MODIFIED.,1739510213,#3,#107,1739508662);
#121=IFCRELCONTAINEDINSPATIALSTRUCTURE('05AVd4pPf1jeS4OU20v7N9',#5,$,$,(#40),#114);
#122=IFCRELAGGREGATES('3PxVuUIG14dQdQEHf8wUca',#5,$,$,#17,(#114));
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,74 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2025-08-27T14:53:55+02:00',('AdaUser'),('AdaOrg'),'IfcOpenShell 0.8.2','IfcOpenShell 0.8.2','Nobody');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPROJECT('2MG118AczBnBRhfOi7rLGC',$,'AdaProject',$,$,$,$,(#11),#6);
#2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#5=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#6=IFCUNITASSIGNMENT((#5,#3,#4,#2));
#7=IFCCARTESIANPOINT((0.,0.,0.));
#8=IFCDIRECTION((0.,0.,1.));
#9=IFCDIRECTION((1.,0.,0.));
#10=IFCAXIS2PLACEMENT3D(#7,#8,#9);
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#10,$);
#12=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#11,$,.MODEL_VIEW.,$);
#13=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#11,$,.GRAPH_VIEW.,$);
#14=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#11,$,.MODEL_VIEW.,$);
#15=IFCACTORROLE(.ENGINEER.,$,$);
#16=IFCPERSON('AdaUser',$,$,$,$,$,(#15),$);
#17=IFCORGANIZATION('ADA','Assembly For Design and Analysis',$,$,$);
#18=IFCPERSONANDORGANIZATION(#16,#17,$);
#19=IFCAPPLICATION(#17,'XXX','ADA','ADA');
#20=IFCOWNERHISTORY(#18,#19,.READWRITE.,$,$,#18,#19,1756299235);
#21=IFCDIRECTION((0.,0.,1.));
#22=IFCDIRECTION((1.,0.,0.));
#23=IFCCARTESIANPOINT((0.,0.,0.));
#24=IFCAXIS2PLACEMENT3D(#23,#21,#22);
#25=IFCLOCALPLACEMENT($,#24);
#26=IFCSITE('2V9fV9zKnAP9J5CfXLkqps',#20,'Ada',$,$,#25,$,$,.ELEMENT.,$,$,$,$,$);
#27=IFCRELAGGREGATES('12FX4aagX3HQdiLOLDvGX_',#20,'Project Container',$,#1,(#26));
#28=IFCPROPERTYSINGLEVALUE('project',$,IFCTEXT('AdaProject'),$);
#29=IFCPROPERTYSINGLEVALUE('schema',$,IFCTEXT('IFC4X3_add2'),$);
#30=IFCPROPERTYSET('2yltt4JCTAOAyhAFFKmDBW',#20,'Properties',$,(#28,#29));
#31=IFCRELDEFINESBYPROPERTIES('3U0CyjG8XALx2oClwZMPg9',#20,'Properties',$,(#26),#30);
#32=IFCMATERIAL('S355',$,'Steel');
#33=IFCPROPERTYSINGLEVALUE('Grade',$,IFCTEXT('S355'),$);
#34=IFCPROPERTYSINGLEVALUE('YieldStress',$,IFCPRESSUREMEASURE(355000000.),$);
#35=IFCPROPERTYSINGLEVALUE('YoungModulus',$,IFCMODULUSOFELASTICITYMEASURE(210000000000.),$);
#36=IFCPROPERTYSINGLEVALUE('PoissonRatio',$,IFCPOSITIVERATIOMEASURE(0.3),$);
#37=IFCPROPERTYSINGLEVALUE('ThermalExpansionCoefficient',$,IFCTHERMALEXPANSIONCOEFFICIENTMEASURE(1.2E-05),$);
#38=IFCPROPERTYSINGLEVALUE('SpecificHeatCapacity',$,IFCSPECIFICHEATCAPACITYMEASURE(0.03),$);
#39=IFCPROPERTYSINGLEVALUE('MassDensity',$,IFCMASSDENSITYMEASURE(7850.),$);
#40=IFCMATERIALPROPERTIES('MaterialMechanical','A Material property description',(#33,#34,#35,#36,#37,#38,#39),#32);
#41=IFCRELASSOCIATESMATERIAL('3__DXGCfr15fk06ugmrrR4',#20,'S355','Objects related to S355',(#64),#32);
#42=IFCDIRECTION((0.,0.,1.));
#43=IFCDIRECTION((1.,0.,0.));
#44=IFCCARTESIANPOINT((0.,0.,0.));
#45=IFCAXIS2PLACEMENT3D(#44,#42,#43);
#46=IFCLOCALPLACEMENT(#25,#45);
#47=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,0.1),(0.1,0.),(0.1,0.1)),$);
#48=IFCINDEXEDPOLYCURVE(#47,(IFCLINEINDEX((3,1)),IFCLINEINDEX((1,2)),IFCLINEINDEX((2,4)),IFCLINEINDEX((4,3))),$);
#49=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#48);
#50=IFCCARTESIANPOINTLIST3D(((-1.,0.,0.8),(-0.14253,0.,0.542759),(-0.039541321421393,0.,0.470579850861542),(0.,0.,0.),(0.,0.,0.351194)),$);
#51=IFCINDEXEDPOLYCURVE(#50,(IFCLINEINDEX((4,5)),IFCARCINDEX((5,3,2)),IFCLINEINDEX((2,1))),$);
#52=IFCDIRECTION((-0.,1.,0.));
#53=IFCCARTESIANPOINT((0.,0.,0.));
#54=IFCDIRECTION((1.,0.,0.));
#55=IFCDIRECTION((0.,0.,-1.));
#56=IFCAXIS2PLACEMENT3D(#53,#54,#55);
#57=IFCFIXEDREFERENCESWEPTAREASOLID(#49,#56,#51,$,$,#52);
#58=IFCSHAPEREPRESENTATION(#12,'Body','AdvancedSweptSolid',(#57));
#59=IFCPRODUCTDEFINITIONSHAPE($,$,(#58));
#60=IFCCOLOURRGB('Color1',0.8,0.8,0.8);
#61=IFCSURFACESTYLESHADING(#60,0.);
#62=IFCSURFACESTYLE('Color1',.BOTH.,(#61));
#63=IFCSTYLEDITEM(#57,(#62),'Color1');
#64=IFCBUILDINGELEMENTPROXY('07ngBzsO5BK8C3rDUqQfSD',#20,'sweep1',$,$,#46,#59,$,$);
#65=IFCRELCONTAINEDINSPATIALSTRUCTURE('1_r$SsBj1C3wgAjnhfmkqs',#20,'Physical model',$,(#64),#26);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,74 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2025-08-28T09:45:32+02:00',('AdaUser'),('AdaOrg'),'IfcOpenShell 0.8.2','IfcOpenShell 0.8.2','Nobody');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPROJECT('3kk2KcD29DrxlrjJ23wiG2',$,'AdaProject',$,$,$,$,(#11),#6);
#2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#5=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#6=IFCUNITASSIGNMENT((#3,#5,#4,#2));
#7=IFCCARTESIANPOINT((0.,0.,0.));
#8=IFCDIRECTION((0.,0.,1.));
#9=IFCDIRECTION((1.,0.,0.));
#10=IFCAXIS2PLACEMENT3D(#7,#8,#9);
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#10,$);
#12=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#11,$,.MODEL_VIEW.,$);
#13=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#11,$,.GRAPH_VIEW.,$);
#14=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#11,$,.MODEL_VIEW.,$);
#15=IFCACTORROLE(.ENGINEER.,$,$);
#16=IFCPERSON('AdaUser',$,$,$,$,$,(#15),$);
#17=IFCORGANIZATION('ADA','Assembly For Design and Analysis',$,$,$);
#18=IFCPERSONANDORGANIZATION(#16,#17,$);
#19=IFCAPPLICATION(#17,'XXX','ADA','ADA');
#20=IFCOWNERHISTORY(#18,#19,.READWRITE.,$,$,#18,#19,1756367132);
#21=IFCDIRECTION((0.,0.,1.));
#22=IFCDIRECTION((1.,0.,0.));
#23=IFCCARTESIANPOINT((0.,0.,0.));
#24=IFCAXIS2PLACEMENT3D(#23,#21,#22);
#25=IFCLOCALPLACEMENT($,#24);
#26=IFCSITE('3192LLwf9Et9NYY6Dp1HMI',#20,'Ada',$,$,#25,$,$,.ELEMENT.,$,$,$,$,$);
#27=IFCRELAGGREGATES('3sB8i$7If0y9iXdBJx7Et7',#20,'Project Container',$,#1,(#26));
#28=IFCPROPERTYSINGLEVALUE('project',$,IFCTEXT('AdaProject'),$);
#29=IFCPROPERTYSINGLEVALUE('schema',$,IFCTEXT('IFC4X3_add2'),$);
#30=IFCPROPERTYSET('16zSs_zL5FTQaH0f6iQohK',#20,'Properties',$,(#28,#29));
#31=IFCRELDEFINESBYPROPERTIES('2hQUoEcor7uf16HkCAa06h',#20,'Properties',$,(#26),#30);
#32=IFCMATERIAL('S355',$,'Steel');
#33=IFCPROPERTYSINGLEVALUE('Grade',$,IFCTEXT('S355'),$);
#34=IFCPROPERTYSINGLEVALUE('YieldStress',$,IFCPRESSUREMEASURE(355000000.),$);
#35=IFCPROPERTYSINGLEVALUE('YoungModulus',$,IFCMODULUSOFELASTICITYMEASURE(210000000000.),$);
#36=IFCPROPERTYSINGLEVALUE('PoissonRatio',$,IFCPOSITIVERATIOMEASURE(0.3),$);
#37=IFCPROPERTYSINGLEVALUE('ThermalExpansionCoefficient',$,IFCTHERMALEXPANSIONCOEFFICIENTMEASURE(1.2E-05),$);
#38=IFCPROPERTYSINGLEVALUE('SpecificHeatCapacity',$,IFCSPECIFICHEATCAPACITYMEASURE(0.03),$);
#39=IFCPROPERTYSINGLEVALUE('MassDensity',$,IFCMASSDENSITYMEASURE(7850.),$);
#40=IFCMATERIALPROPERTIES('MaterialMechanical','A Material property description',(#33,#34,#35,#36,#37,#38,#39),#32);
#41=IFCRELASSOCIATESMATERIAL('3GCQxFEKjCmPtz21j5vEC_',#20,'S355','Objects related to S355',(#64),#32);
#42=IFCDIRECTION((0.,0.,1.));
#43=IFCDIRECTION((1.,0.,0.));
#44=IFCCARTESIANPOINT((0.,0.,0.));
#45=IFCAXIS2PLACEMENT3D(#44,#42,#43);
#46=IFCLOCALPLACEMENT(#25,#45);
#47=IFCCARTESIANPOINTLIST2D(((0.1,0.05),(0.,0.),(0.1,0.),(0.1,0.05)),$);
#48=IFCINDEXEDPOLYCURVE(#47,$,$);
#49=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#48);
#50=IFCCARTESIANPOINTLIST3D(((50.,100.,200.),(50.,100.,200.389146),(50.0003133895072,100.025071208213,200.486102587622),(50.001174980994,100.093998658131,200.558749),(50.0086491903016,100.691934415524,200.932458971772),(50.0310627108963,100.770687414196,201.008805063266),(50.089001837944,100.800000190195,201.100002410475),(50.6760204909973,100.800000000803,201.843063903121),(50.7636872882004,100.858578730855,201.954034522829),(50.7999999397009,101.700000021708,201.999999960467),(50.8000002509973,100.999999960803,202.000000303121)),$);
#51=IFCINDEXEDPOLYCURVE(#50,(IFCLINEINDEX((1,2)),IFCARCINDEX((2,3,4)),IFCLINEINDEX((4,5)),IFCARCINDEX((5,6,7)),IFCLINEINDEX((7,8)),IFCARCINDEX((8,9,11)),IFCLINEINDEX((11,10))),$);
#52=IFCDIRECTION((1.,0.,0.));
#53=IFCCARTESIANPOINT((0.,0.,0.));
#54=IFCDIRECTION((0.,0.,1.));
#55=IFCDIRECTION((1.,0.,0.));
#56=IFCAXIS2PLACEMENT3D(#53,#54,#55);
#57=IFCFIXEDREFERENCESWEPTAREASOLID(#49,#56,#51,$,$,#52);
#58=IFCSHAPEREPRESENTATION(#12,'Body','AdvancedSweptSolid',(#57));
#59=IFCPRODUCTDEFINITIONSHAPE($,$,(#58));
#60=IFCCOLOURRGB('Color1',0.8,0.8,0.8);
#61=IFCSURFACESTYLESHADING(#60,0.);
#62=IFCSURFACESTYLE('Color1',.BOTH.,(#61));
#63=IFCSTYLEDITEM(#57,(#62),'Color1');
#64=IFCBUILDINGELEMENTPROXY('0Yr8J8fnj47xCNdvnijjk4',#20,'sweep2',$,$,#46,#59,$,$);
#65=IFCRELCONTAINEDINSPATIALSTRUCTURE('1Bl6up0hb69elL9bAj0Yd6',#20,'Physical model',$,(#64),#26);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,85 @@
import pathlib
from collections.abc import Sequence
import ifcopenshell
import pytest
def _bbox_from_vertices(verts: list[tuple[float, float, float]]):
if not verts:
return (0, 0, 0), (0, 0, 0)
xs = [v[0] for v in verts]
ys = [v[1] for v in verts]
zs = [v[2] for v in verts]
mn = (min(xs), min(ys), min(zs))
mx = (max(xs), max(ys), max(zs))
return mn, mx
def _size_from_bbox(mn, mx):
return (mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2])
def _triples(flat: Sequence[float]) -> list[tuple[float, float, float]]:
return [(float(flat[i]), float(flat[i + 1]), float(flat[i + 2])) for i in range(0, len(flat), 3)]
def load_ifc_mesh_bbox(ifc_path: str):
"""Load first product's mesh from IFC using ifcopenshell.geom and return bbox and size."""
try:
import ifcopenshell.geom as geom
except Exception as e:
raise RuntimeError("ifcopenshell.geom not available: cannot validate IFC geometry") from e
settings = geom.settings()
settings.set(settings.USE_WORLD_COORDS, True)
f = ifcopenshell.open(ifc_path)
# Prefer the proxy we created, otherwise take any product with representation
products = f.by_type("IfcProduct")
target = None
for p in products:
if p.is_a("IfcBuildingElementProxy") and getattr(p, "Representation", None):
target = p
break
if target is None:
for p in products:
if getattr(p, "Representation", None):
target = p
break
if target is None:
raise RuntimeError("No representable product found in IFC for validation")
shape = geom.create_shape(settings, target)
verts = _triples(shape.geometry.verts)
mn, mx = _bbox_from_vertices(verts)
return mn, mx, _size_from_bbox(mn, mx)
@pytest.fixture
def geom_dir():
return pathlib.Path(__file__).parent.parent.resolve().absolute() / "fixtures/geom"
def test_simple_sweep_1(geom_dir):
ifc_file_path = geom_dir / "simple_sweep_1.ifc"
ifc_mn, ifc_mx, ifc_sz = load_ifc_mesh_bbox(ifc_file_path)
assert ifc_sz == pytest.approx((0.8957825463853046, 0.1, 1.1))
assert ifc_mn == pytest.approx((0.0, 0, -0.1))
assert ifc_mx == pytest.approx((0.8957825463853046, 0.1, 1.0))
def test_simple_sweep_2(geom_dir):
ifc_file_path = geom_dir / "simple_sweep_2.ifc"
ifc_mn, ifc_mx, ifc_sz = load_ifc_mesh_bbox(ifc_file_path)
assert ifc_mn == pytest.approx((50.0, 100.0, 200.0))
assert ifc_mx == pytest.approx((50.89584911299009, 101.70000025609394, 202.0000003710634))
assert ifc_sz == pytest.approx((0.8958491129900921, 1.7000002560939436, 2.0000003710634076))
def test_pipe_12d(geom_dir):
ifc_file_path = geom_dir / "pipe.ifc"
ifc_mn, ifc_mx, ifc_sz = load_ifc_mesh_bbox(ifc_file_path)
assert ifc_sz == pytest.approx((1.205888147422229, 0.9929900508137735, 0.35776115971654576))
assert ifc_mn == pytest.approx((288.9774190979147, 582.0537006391681, 118.70711942014172))
assert ifc_mx == pytest.approx((290.18330724533695, 583.0466906899819, 119.06488057985827))
+1 -1
View File
@@ -21,7 +21,7 @@ import pytest
import ifcopenshell
TEST_FILE_DIR = Path("../../test/input/")
TEST_FILE_DIR = Path(__file__).parent / "../../../test/input/"
class TestOpen:
+1 -1
View File
@@ -22,7 +22,7 @@ import pytest
import ifcopenshell
TEST_FILE_DIR = Path("../../test/input/")
TEST_FILE_DIR = Path(__file__).parent / "../../../test/input/"
class TestWrite: