From 1fcf2fb53f67c21d11eeb0189fce22109ab94266 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:56:36 -0700 Subject: [PATCH] Fixed zero length segment for vertical alignment. Refactors curve segment transition code --- .../_map_alignment_vertical_segment.py | 7 ++-- .../api/alignment/add_zero_length_segment.py | 26 +++++++++--- .../get_curve_segment_transition_code.py | 41 +++++++++++-------- .../test_map_alignment_vertical_segment.py | 2 + 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/_map_alignment_vertical_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/_map_alignment_vertical_segment.py index 5189231d4a..1f8a1ef5d7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/_map_alignment_vertical_segment.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/_map_alignment_vertical_segment.py @@ -143,6 +143,9 @@ def _map_circular_arc(file: ifcopenshell.file, design_parameters: entity_instanc end_angle = math.atan(end_gradient) dx = math.cos(start_angle) dy = math.sin(start_angle) + + # start and end angles are for the curve tangents + # convert them to be angles of the radii lines if start_angle < end_angle: radius = horizontal_length / (math.sin(end_angle) - math.sin(start_angle)) x = -radius * math.sin(start_angle) @@ -158,14 +161,12 @@ def _map_circular_arc(file: ifcopenshell.file, design_parameters: entity_instanc parent_curve = file.createIfcCircle( Position=file.createIfcAxis2Placement2D( - Location=file.createIfcCartesianPoint((0.0, 0.0)), + Location=file.createIfcCartesianPoint((x, y)), RefDirection=file.createIfcDirection((1.0, 0.0)), ), Radius=radius, ) - segment_curve_length = radius * math.fabs(end_angle - start_angle) - curve_segment = file.create_entity( type="IfcCurveSegment", Transition=transition, diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py index b051a3fdaa..339098fa40 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py @@ -27,6 +27,7 @@ import math from ifcopenshell.api.alignment._get_segment_start_point_label import _get_segment_start_point_label from ifcopenshell.api.alignment._map_alignment_horizontal_segment import _map_alignment_horizontal_segment +from ifcopenshell.api.alignment._map_alignment_vertical_segment import _map_alignment_vertical_segment from ifcopenshell.api.alignment._update_curve_segment_transition_code import _update_curve_segment_transition_code @@ -157,20 +158,35 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in ) elif layout.is_a("IfcAlignmentVertical"): last_segment_dist_along = 0.0 + last_segment_height = 0.0 last_segment_end_gradient = 0.0 + last_segment = None for rel in layout.IsNestedBy: if 0 < len(rel.RelatedObjects): last_segment = rel.RelatedObjects[-1] - last_segment_dist_along = ( - last_segment.DesignParameters.StartDistAlong + last_segment.DesignParameters.HorizontalLength - ) - last_segment_end_gradient = last_segment.DesignParameters.EndGradient break + if last_segment: + file.begin_transaction() + last_segment_dist_along = ( + last_segment.DesignParameters.StartDistAlong + last_segment.DesignParameters.HorizontalLength + ) + last_segment_end_gradient = last_segment.DesignParameters.EndGradient + settings = ifcopenshell.geom.settings() + mapped_segments = _map_alignment_vertical_segment(file, last_segment) + geometry_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1] + fn = wrapper.map_shape(settings, geometry_segment.wrapped_data) + eval = wrapper.function_item_evaluator(settings, fn) + e = np.array(eval.evaluate(fn.end())) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) + last_segment_height = float(e[1, 3]) / unit_scale + + file.discard_transaction() + design_parameters = file.createIfcAlignmentVerticalSegment( StartDistAlong=last_segment_dist_along, HorizontalLength=0.0, - StartHeight=0.0, + StartHeight=last_segment_height, StartGradient=last_segment_end_gradient, EndGradient=last_segment_end_gradient, PredefinedType="CONSTANTGRADIENT", diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve_segment_transition_code.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve_segment_transition_code.py index 5a263d022f..577b14d709 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve_segment_transition_code.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve_segment_transition_code.py @@ -26,47 +26,52 @@ import math def get_curve_segment_transition_code( - prev_segment: entity_instance, segment: entity_instance, tolerance: float = 5.0e-4 + segment: entity_instance, next_segment: entity_instance, position_tolerance: float = 0.001 ) -> str: """ - Returns the IfcCurveSegment.Transition of prev_segment based on a comparison of - the position, ref. direction, and curvature at the end of the prev_segment and the start of segment. + Returns the IfcCurveSegment.Transition of segment based on a comparison of + the position, ref. direction, and curvature at the end of the segment and the start of next_segment. + + :param segment: segment for which the position curve is being being determined + :param next_segment: next segment + :param position_tolerance: tolerance used for evaluation positions. The default is 1mm + :return: the transition code """ expected_type = "IfcCurveSegment" - if not prev_segment.is_a(expected_type): - raise TypeError(f"Expected to see '{expected_type}', instead received '{prev_segment.is_a()}'.") - if not segment.is_a(expected_type): raise TypeError(f"Expected to see '{expected_type}', instead received '{segment.is_a()}'.") - if len(prev_segment.UsingCurves) != 1: - raise TypeError("prev_segment must belong to exactly one curve") + if not next_segment.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{next_segment.is_a()}'.") if len(segment.UsingCurves) != 1: raise TypeError("segment must belong to exactly one curve") - if prev_segment.UsingCurves[0] != segment.UsingCurves[0]: + if len(next_segment.UsingCurves) != 1: + raise TypeError("next_segment must belong to exactly one curve") + + if segment.UsingCurves[0] != next_segment.UsingCurves[0]: raise TypeError("Both segments must belong to the same curve") settings = ifcopenshell.geom.settings() settings.set("COMPUTE_CURVATURE", True) - prev_segment_fn = ifcopenshell_wrapper.map_shape(settings, prev_segment.wrapped_data) - prev_segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, prev_segment_fn) - e = prev_segment_evaluator.evaluate(prev_segment_fn.end()) + segment_fn = ifcopenshell_wrapper.map_shape(settings, segment.wrapped_data) + segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn) + e = segment_evaluator.evaluate(segment_fn.end()) end = np.array(e) # must add the new segment to the container before mapping it, otherwise the segment doesn't # have enough context to know if it is for horizontal, vertical, cant - segment_fn = ifcopenshell_wrapper.map_shape(settings, segment.wrapped_data) - segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn) - s = segment_evaluator.evaluate(segment_fn.start()) + next_segment_fn = ifcopenshell_wrapper.map_shape(settings, next_segment.wrapped_data) + next_segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, next_segment_fn) + s = next_segment_evaluator.evaluate(next_segment_fn.start()) start = np.array(s) - same_position = True if np.allclose(end[:3, 3], start[:3, 3], atol=tolerance) else False - same_gradient = True if np.allclose(end[:3, 0], start[:3, 0], atol=tolerance) else False - same_curvature = True if np.allclose(end[3:, :3], start[3:, :3], atol=tolerance) else False + same_position = True if np.allclose(end[:3, 3], start[:3, 3], atol=position_tolerance) else False + same_gradient = True if np.allclose(end[:3, 0], start[:3, 0]) else False + same_curvature = True if np.allclose(end[3:, :3], start[3:, :3]) else False transition_code = "" if same_position: diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py index c7f4cb3348..7d88d5610a 100644 --- a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py @@ -794,3 +794,5 @@ def test_map_alignment_vertical_segment(): _ParabolicArc_100_0_10_0__1_0__0_5_1_Meter(file) # VERTICAL CLOTHOID NOT IMPLEMENTED + +test_map_alignment_vertical_segment() \ No newline at end of file