Implements business 2 geometry mapping for Viennese Bend for horizontal and cant layouts

This commit is contained in:
Richard Brice
2025-08-09 14:03:19 -07:00
parent 9d2a79803a
commit 762f4ff975
6 changed files with 1023 additions and 23 deletions
@@ -0,0 +1,77 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.api.geometry
from ifcopenshell import entity_instance
import math
from collections.abc import Sequence
def _get_cant_segment(horizontal_segment: entity_instance) -> entity_instance:
"""
Returns the IfcAlignmentSegment from the cant layout that corresponds to horizontal_segment.
Returns None if the cant segment cannot be found
"""
expected_type = "IfcAlignmentSegment"
if not horizontal_segment.is_a(expected_type):
raise TypeError(f"Expected {expected_type} but got {horizontal_segment.is_a()}")
if not horizontal_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
raise TypeError(f"Expect DesignParameter to be IfcAlignmentHorizontal but got {horizontal_segment.DesignParameters.is_a()}")
# get the index of horizontal_segment in the horizontal_layout
horizontal_layout = horizontal_segment.Nests[0].RelatingObject
index = 0
for segment in horizontal_layout.IsNestedBy[0].RelatedObjects:
if segment == horizontal_segment:
break
else:
index += 1
cant_segment = None
# first check CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant
nests_layouts = horizontal_layout.Nests[0]
for layout in nests_layouts.RelatedObjects:
if layout.is_a("IfcAlignmentCant"):
cant_segment = layout.IsNestedBy[0].RelatedObjects[index]
break
# if a cant_segment wasn't found, check CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout
# Note that nothing forbids multiple child alignments to have cant layouts. However, this would not make
# sense for Viennese Bend because the Viennese Bend cant segment influences the geometry of the horizontal
# Viennese Bend transition curve segment. The horizontal geometry would not be unique if there are
# multiple child alignments with cant layouts.
# For this reason, use the first cant layout found
if cant_segment == None:
alignment = ifcopenshell.api.alignment.get_alignment(horizontal_layout)
for child_alignment in alignment.IsDecomposedBy[0].RelatedObjects:
for layout in child_alignment.Nests[0].RelatedObjects:
if layout.is_a("IfcAlignmentCant"):
cant_segment = layout.IsNestedBy[0].RelatedObjects[index]
break
if cant_segment:
break
return cant_segment
@@ -377,7 +377,66 @@ def _map_sine_curve(
def _map_viennese_bend(
file: ifcopenshell.file, design_parameters: entity_instance, rail_head_distance: float
) -> Sequence[entity_instance]:
raise NotImplementedError("VIENNESEBEND not implemented")
dist_along = design_parameters.StartDistAlong
length = design_parameters.HorizontalLength
Dsl = design_parameters.StartCantLeft
Del = design_parameters.EndCantLeft
Dsr = design_parameters.StartCantRight
Der = design_parameters.EndCantRight
Ds = 0.5 * (Dsl + Dsr)
De = 0.5 * (Del + Der)
f = De - Ds
a0 = Ds # constant term
a1 = 0.0 # linear term
a2 = 0.0 * f # quadratic term
a3 = 0.0 * f # cubic term
a4 = 35. * f # quartic term
a5 = -84.*f # quintic term
a6 = 70.*f # sextic term
a7 = -20.*f # septic term
transition = "DISCONTINUOUS"
A0 = math.pow(length, 2.0 / 1.0) * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0
A1 = math.pow(length, 3.0 / 2.0) * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0
A2 = math.pow(length, 4.0 / 3.0) * math.pow(math.fabs(a2), -1.0 / 3.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0
A3 = math.pow(length, 5.0 / 4.0) * math.pow(math.fabs(a3), -1.0 / 4.0) * (a3 / math.fabs(a3)) if a3 != 0.0 else 0.0
A4 = math.pow(length, 6.0 / 5.0) * math.pow(math.fabs(a4), -1.0 / 5.0) * (a4 / math.fabs(a4)) if a4 != 0.0 else 0.0
A5 = math.pow(length, 7.0 / 6.0) * math.pow(math.fabs(a5), -1.0 / 6.0) * (a5 / math.fabs(a5)) if a5 != 0.0 else 0.0
A6 = math.pow(length, 8.0 / 7.0) * math.pow(math.fabs(a6), -1.0 / 7.0) * (a6 / math.fabs(a6)) if a6 != 0.0 else 0.0
A7 = math.pow(length, 9.0 / 8.0) * math.pow(math.fabs(a7), -1.0 / 8.0) * (a7 / math.fabs(a7)) if a7 != 0.0 else 0.0
parent_curve = file.createIfcSeventhOrderPolynomialSpiral(
Position=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0))
),
SepticTerm=A7,
SexticTerm=A6 if A6 != 0.0 else None,
QuinticTerm=A5 if A5 != 0.0 else None,
QuarticTerm=A4 if A4 != 0.0 else None,
CubicTerm=A3 if A3 != 0.0 else None,
QuadraticTerm=A2 if A2 != 0.0 else None,
LinearTerm=A1 if A1 != 0.0 else None,
ConstantTerm=A0 if A0 != 0.0 else None,
)
start_point = file.createIfcCartesianPoint((dist_along, Ds, 0.0))
start_direction = 0.0
curve_segment = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point,
Axis=_get_axis(file, Ds, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction), 0.0)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(length),
ParentCurve=parent_curve,
)
return (curve_segment, None)
def _map_alignment_cant_segment(
@@ -22,6 +22,8 @@ import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from collections.abc import Sequence
import math
from ifcopenshell.api.alignment._get_cant_segment import _get_cant_segment
def _get_curve_factor(design_parameters: entity_instance) -> float:
start_radius = design_parameters.StartRadiusOfCurvature
@@ -290,22 +292,6 @@ def _map_helmert_curve(file: ifcopenshell.file, design_parameters: entity_instan
ParentCurve=parent_curve2,
)
"""
import numpy as np
settings = ifcopenshell.geom.settings()
prev_segment_fn = ifcopenshell_wrapper.map_shape(settings, curve_segment1.wrapped_data)
prev_segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, prev_segment_fn)
e = prev_segment_evaluator.evaluate(prev_segment_fn.end())
end = np.array(e)
segment_fn = ifcopenshell_wrapper.map_shape(settings, curve_segment2.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
s = segment_evaluator.evaluate(segment_fn.start())
start = np.array(s)
assert(np.allclose(end[:3,3],start[:3,3]))
"""
return curve_segment1, curve_segment2
@@ -444,8 +430,87 @@ def _map_sine_curve(file: ifcopenshell.file, design_parameters: entity_instance)
return (curve_segment, None)
def _map_viennese_bend(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]:
raise NotImplementedError("VIENNESEBEND not implemented")
def _map_viennese_bend(file: ifcopenshell.file, segment: entity_instance) -> Sequence[entity_instance]:
design_parameters = segment.DesignParameters
start_point = design_parameters.StartPoint
start_direction = design_parameters.StartDirection
start_radius = design_parameters.StartRadiusOfCurvature
length = design_parameters.SegmentLength
gravity_centerline_height = design_parameters.GravityCenterLineHeight if design_parameters.GravityCenterLineHeight != None else 0.0
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
transition = "DISCONTINUOUS"
cant_segment = _get_cant_segment(segment)
if cant_segment:
start_cant_left = cant_segment.DesignParameters.StartCantLeft
end_cant_left = cant_segment.DesignParameters.EndCantLeft if cant_segment.DesignParameters.EndCantLeft else 0.0
start_cant_right = cant_segment.DesignParameters.StartCantRight
end_cant_right = cant_segment.DesignParameters.EndCantRight if cant_segment.DesignParameters.EndCantRight else 0.0
cant_layout = cant_segment.Nests[0].RelatingObject
rail_head_distance = cant_layout.RailHeadDistance
else:
start_cant_left = 0.
end_cant_left = 0.
start_cant_right = 0.
end_cant_right = 0.
rail_head_distance = 1.
cant_angle_start = (start_cant_right - start_cant_left)/rail_head_distance if rail_head_distance else 0.
cant_angle_end = (end_cant_right - end_cant_left)/rail_head_distance if rail_head_distance else 0.
cant_factor = -420.*(gravity_centerline_height/length)*(cant_angle_end - cant_angle_start)
f = _get_curve_factor(design_parameters)
a0 = length / start_radius if start_radius != 0.0 else 0.0 # constant term
a1 = 0. # linear term
a2 = 1.*cant_factor # quadratic term
a3 = -4.*cant_factor # cubic term
a4 = 5.*cant_factor + 35.*f # quartic term
a5 = -2.*cant_factor - 84.*f # quintic term
a6 = 70.*f # sextic term
a7 = -20.0*f # septic term
A0 = length*math.pow(math.fabs(a0),-1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0
A1 = length*math.pow(math.fabs(a1),-1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0
A2 = length*math.pow(math.fabs(a2),-1.0 / 3.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0
A3 = length*math.pow(math.fabs(a3),-1.0 / 4.0) * (a3 / math.fabs(a3)) if a3 != 0.0 else 0.0
A4 = length*math.pow(math.fabs(a4),-1.0 / 5.0) * (a4 / math.fabs(a4)) if a4 != 0.0 else 0.0
A5 = length*math.pow(math.fabs(a5),-1.0 / 6.0) * (a5 / math.fabs(a5)) if a5 != 0.0 else 0.0
A6 = length*math.pow(math.fabs(a6),-1.0 / 7.0) * (a6 / math.fabs(a6)) if a6 != 0.0 else 0.0
A7 = length*math.pow(math.fabs(a7),-1.0 / 8.0) * (a7 / math.fabs(a7)) if a7 != 0.0 else 0.0
parent_curve = file.createIfcSeventhOrderPolynomialSpiral(
Position=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0))
),
SepticTerm=A7,
SexticTerm=A6 if A6 != 0.0 else None,
QuinticTerm=A5 if A5 != 0.0 else None,
QuarticTerm=A4 if A4 != 0.0 else None,
CubicTerm=A3 if A3 != 0.0 else None,
QuadraticTerm=A2 if A2 != 0.0 else None,
LinearTerm=A1 if A1 != 0.0 else None,
ConstantTerm=A0 if A0 != 0.0 else None,
)
curve_segment = file.create_entity(
type="IfcCurveSegment",
Transition=transition,
Placement=file.create_entity(
type="IfcAxis2Placement2D",
Location=start_point,
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(length),
ParentCurve=parent_curve,
)
return (curve_segment, None)
def _map_alignment_horizontal_segment(file: ifcopenshell.file, segment: entity_instance) -> Sequence[entity_instance]:
@@ -477,7 +542,7 @@ def _map_alignment_horizontal_segment(file: ifcopenshell.file, segment: entity_i
elif predefined_type == "SINECURVE":
result = _map_sine_curve(file, segment.DesignParameters)
elif predefined_type == "VIENNESEBEND":
result = _map_viennese_bend(file, segment.DesignParameters)
result = _map_viennese_bend(file, segment)
else:
raise TypeError(f"Unexpected predefined type: '{predefined_type}'.")
@@ -46,6 +46,9 @@ def create(
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.
:param file:
:param name: name assigned to IfcAlignment.Name
:param include_vertical: If True, IfcAlignmentVertical and IfcGradientCurve are created