From 3ef7f1c141e7f89118a0ee92988732ff321e31f1 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Sun, 20 Jul 2025 15:32:26 -0700 Subject: [PATCH] Alignment API updates to support new patch recipes --- .../ifcopenshell/api/alignment/__init__.py | 7 + .../api/alignment/_add_segment_to_layout.py | 9 +- .../api/alignment/_add_zero_length_segment.py | 117 ++---------- .../api/alignment/add_zero_length_segment.py | 170 ++++++++++++++++++ .../ifcopenshell/api/alignment/create.py | 4 +- .../api/alignment/create_as_offset_curve.py | 4 +- .../api/alignment/create_as_polyline.py | 4 +- .../api/alignment/create_representation.py | 53 ++++++ .../api/alignment/get_layout_curve.py | 21 +-- ...ped_segments.py => get_mapped_segments.py} | 14 +- .../api/alignment/has_zero_length_segment.py | 28 +-- .../api/alignment/update_fallback_position.py | 63 +++++++ .../ifcopenshell/api/alignment/util.py | 2 +- .../util/{stationing.py => alignment.py} | 38 ++++ .../alignment/test_add_segment_to_layout.py | 2 + .../alignment/test_add_vertical_alignment.py | 7 - .../test/api/alignment/test_create.py | 28 ++- .../test_update_fallback_position.py | 68 +++++++ .../{test_stationing.py => test_alignment.py} | 2 +- 19 files changed, 481 insertions(+), 160 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_representation.py rename src/ifcopenshell-python/ifcopenshell/api/alignment/{_get_mapped_segments.py => get_mapped_segments.py} (81%) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/update_fallback_position.py rename src/ifcopenshell-python/ifcopenshell/util/{stationing.py => alignment.py} (62%) create mode 100644 src/ifcopenshell-python/test/api/alignment/test_update_fallback_position.py rename src/ifcopenshell-python/test/util/{test_stationing.py => test_alignment.py} (98%) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py index 5c2a6b6049..ce4d961491 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py @@ -49,6 +49,7 @@ Future versions of this API may support: from .add_stationing_referent import add_stationing_referent from .add_vertical_layout import add_vertical_layout +from .add_zero_length_segment import add_zero_length_segment from .create_layout_segment import create_layout_segment from .create import create from .create_as_offset_curve import create_as_offset_curve @@ -56,6 +57,7 @@ from .create_as_polyline import create_as_polyline from .create_by_pi_method import create_by_pi_method from .create_from_csv import create_from_csv 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 @@ -69,11 +71,13 @@ from .get_basis_curve import get_basis_curve from .get_child_alignments import get_child_alignments 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 .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 from .name_segments import name_segments +from .update_fallback_position import update_fallback_position from .util import * from ._get_segment_start_point_label import register_referent_name_callback @@ -81,6 +85,7 @@ from ._get_segment_start_point_label import register_referent_name_callback __all__ = [ "add_stationing_referent", "add_vertical_layout", + "add_zero_length_segment", "create_layout_segment", "create", "create_as_offset_curve", @@ -88,6 +93,7 @@ __all__ = [ "create_by_pi_method", "create_from_csv", "create_segment_representations", + "create_representation", "distance_along_from_station", "get_alignment", "get_alignment_station", @@ -105,6 +111,7 @@ __all__ = [ "has_zero_length_segment", "layout_horizontal_alignment_by_pi_method", "layout_vertical_alignment_by_pi_method", + "update_fallback_position", "name_segments", "register_referent_name_callback", ] diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/_add_segment_to_layout.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/_add_segment_to_layout.py index 4bbe28f1dd..3b137a4d74 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/_add_segment_to_layout.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/_add_segment_to_layout.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.api.alignment import ifcopenshell.api.nest import ifcopenshell.geom -import ifcopenshell.util.stationing +import ifcopenshell.util.alignment import ifcopenshell.util.unit from ifcopenshell import ifcopenshell_wrapper import numpy as np @@ -29,7 +29,6 @@ from ifcopenshell import entity_instance from collections.abc import Sequence from ifcopenshell.api.alignment._add_segment_to_curve import _add_segment_to_curve -from ifcopenshell.api.alignment._get_mapped_segments import _get_mapped_segments from ifcopenshell.api.alignment._get_segment_start_point_label import _get_segment_start_point_label @@ -98,7 +97,7 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg "IfcAlignmentVerticalSegment" ) or zero_length_segment.DesignParameters.is_a("IfcAlignmentCantSegment"): # get the geometric representation for the new segment - mapped_segments = _get_mapped_segments(file, 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 @@ -137,7 +136,7 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg 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.stationing.station_as_string(file,start_station+start_dist_along)})" + 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 @@ -169,7 +168,7 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg # 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.stationing.station_as_string(file,station)})" + 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 ) 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 54fa43bc5b..32d9d2b30c 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 @@ -18,128 +18,37 @@ import ifcopenshell import ifcopenshell.api.alignment -import ifcopenshell.api.nest -import ifcopenshell.util.stationing from ifcopenshell import entity_instance from ifcopenshell.api.alignment._get_segment_start_point_label import _get_segment_start_point_label - def _add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance) -> None: """ Adds a zero length segment to the end of a layout. Also adds a zero length segment to the end of the corresponding geometric curve. - If the layout already has a zero length segment, nothing is changed + This function depends on the assumptions made in ifcopenshell.api.alignment.create and is called by that function. + This is not a general purpose function. :param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant :return: None """ + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"] if not layout.is_a() in expected_types: raise TypeError( f"Expected layout type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}" ) - - if ifcopenshell.api.alignment.has_zero_length_segment(layout): - return - - segment = None - if layout.is_a("IfcAlignmentHorizontal"): - design_parameters = file.createIfcAlignmentHorizontalSegment( - StartPoint=file.createIfcCartesianPoint( - (0.0, 0.0) - ), # this is a little problematic. need to know the end point and tangent - StartDirection=0.0, # of the previous segment, which requires geometry mapping - StartRadiusOfCurvature=0.0, - EndRadiusOfCurvature=0.0, - SegmentLength=0.0, - PredefinedType="LINE", - ) - segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters) - elif layout.is_a("IfcAlignmentVertical"): - last_segment_dist_along = 0.0 - last_segment_end_gradient = 0.0 - 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 - - design_parameters = file.createIfcAlignmentVerticalSegment( - StartDistAlong=last_segment_dist_along, - HorizontalLength=0.0, - StartHeight=0.0, - StartGradient=last_segment_end_gradient, - EndGradient=last_segment_end_gradient, - PredefinedType="CONSTANTGRADIENT", - ) - segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters) - elif layout.is_a("IfcAlignmentCant"): - last_segment_dist_along = 0.0 - last_segment_cant_left = 0.0 - last_segment_cant_right = 0.0 - 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_cant_left = ( - last_segment.DesignParameters.EndCantLeft - if last_segment.DesignParameters.EndCantLeft != None - else last_segment.DesignParameters.StartCantLeft - ) - last_segment_cant_right = ( - last_segment.DesignParameters.EndCantRight - if last_segment.DesignParameters.EndCantRight != None - else last_segment.DesignParameters.StartCantRight - ) - - design_parameters = file.createIfcAlignmentCantSegment( - StartDistAlong=last_segment_dist_along, - HorizontalLength=0.0, - StartCantLeft=last_segment_cant_left, - StartCantRight=last_segment_cant_right, - PredefinedType="CONSTANTCANT", - ) - segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters) - - ifcopenshell.api.nest.assign_object(file, related_objects=[segment], relating_object=layout) + + if (not ifcopenshell.api.alignment.add_zero_length_segment(file,layout,include_referent=False)): + return # zero length segment not added, probably because it already exists curve = ifcopenshell.api.alignment.get_layout_curve(layout) - has_zero_length_segment = False - if curve.Segments != None and 0 < len(curve.Segments): - last_segment = curve.Segments[-1] - has_zero_length_segment = ( - True - if last_segment.Transition == "DISCONTINUOUS" and last_segment.SegmentLength.wrappedValue == 0.0 - else False - ) + if curve: + ifcopenshell.api.alignment.add_zero_length_segment(file,curve) - if not has_zero_length_segment: - parent_curve = file.createIfcLine( - Pnt=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))), - Dir=file.createIfcVector( - Orientation=file.createIfcDirection(DirectionRatios=((1.0, 0.0))), - Magnitude=1.0, - ), - ) - curve_segment = file.createIfcCurveSegment( - Transition="DISCONTINUOUS", - Placement=file.createIfcAxis2Placement2D( - Location=file.createIfcCartesianPoint((0.0, 0.0)), - RefDirection=file.createIfcDirection((1.0, 0.0)), - ), - SegmentStart=file.createIfcLengthMeasure(0.0), - SegmentLength=file.createIfcLengthMeasure(0.0), - ParentCurve=parent_curve, - ) - curve.Segments = [ - curve_segment, - ] - - name = f"{_get_segment_start_point_label(segment,None)} {ifcopenshell.util.stationing.station_as_string(file,0.0)}" - ifcopenshell.api.alignment.add_stationing_referent(file, segment, 0.0, 0.0, name=name) + segment = layout.IsNestedBy[0].RelatedObjects[-1] + alignment = ifcopenshell.api.alignment.get_alignment(layout) + station = ifcopenshell.api.alignment.get_alignment_station(file,alignment) + name = f"{_get_segment_start_point_label(segment,None)} {ifcopenshell.util.alignment.station_as_string(file,station)}" + ifcopenshell.api.alignment.add_stationing_referent(file, segment, 0.0, station, name=name) 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 new file mode 100644 index 0000000000..6832c4e158 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py @@ -0,0 +1,170 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import ifcopenshell.api.nest +import ifcopenshell.util.alignment +from ifcopenshell import entity_instance +import ifcopenshell.ifcopenshell_wrapper as wrapper +import numpy as np + +from ifcopenshell.api.alignment._get_segment_start_point_label import _get_segment_start_point_label + + +def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, include_referent : bool = True) -> bool: + """ + Adds a zero length segment to the end of a layout. + + If the layout already has a zero length segment, nothing is changed. + + :param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant, IfcCompositeCurve, IfcGradientCurve, IfcSegmentedReferenceCurve + :param include_referent: If True, an IfcReferent representing the ending point of the layout is included for IfcLinearElement layouts (i.e. business logic) + :return: True if segment is added + """ + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant","IfcCompositeCurve","IfcGradientCurve","IfcSegmentedReferenceCurve"] + if not layout.is_a() in expected_types: + raise TypeError( + f"Expected layout type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}" + ) + + + if ifcopenshell.api.alignment.has_zero_length_segment(layout): + return False + + if layout.is_a("IfcCompositeCurve") or layout.is_a("IfcGradientCurve") or layout.is_a("IfcSegmentedReferenceCurve"): + x = 0. + y = 0. + dx = 1. + dy = 0. + segment_start = 0. + + if layout.Segments and 0 < len(layout.Segments): + # If there are segments, get the last segment and compute the end point and tangent direction + # because this becomes of placement of the zero length segment + last_segment = layout.Segments[-1] + settings = ifcopenshell.geom.settings() + fn = wrapper.map_shape(settings,last_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) + e[:3,3] /= unit_scale + x = float(e[0,3]) + y = float(e[1,3]) + dx = float(e[0,0]) + dy = float(e[1,0]) + segment_start = last_segment.SegmentStart.wrappedValue + last_segment.SegmentLength.wrappedValue + + parent_curve = file.createIfcLine( + Pnt=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))), + Dir=file.createIfcVector( + Orientation=file.createIfcDirection(DirectionRatios=((1.0, 0.0))), + Magnitude=1.0, + ), + ) + curve_segment = file.createIfcCurveSegment( + Transition="DISCONTINUOUS", + Placement=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((x, y)), + RefDirection=file.createIfcDirection((dx, dy)), + ), + SegmentStart=file.createIfcLengthMeasure(segment_start), + SegmentLength=file.createIfcLengthMeasure(0.0), + ParentCurve=parent_curve, + ) + layout.Segments += (curve_segment,) + + # add zero length segments to base curves + if layout.is_a("IfcSegmentedReferenceCurve"): + ifcopenshell.api.alignment.add_zero_length_segment(file,layout.BaseCurve) + elif layout.is_a("IfcGradientCurve"): + ifcopenshell.api.alignment.add_zero_length_segment(file,layout.BaseCurve) + + else: + segment = None + if layout.is_a("IfcAlignmentHorizontal"): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint( + (0.0, 0.0) + ), # this is a little problematic. need to know the end point and tangent + StartDirection=0.0, # of the previous segment, which requires geometry mapping + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=0.0, + PredefinedType="LINE", + ) + segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters) + elif layout.is_a("IfcAlignmentVertical"): + last_segment_dist_along = 0.0 + last_segment_end_gradient = 0.0 + 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 + + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=last_segment_dist_along, + HorizontalLength=0.0, + StartHeight=0.0, + StartGradient=last_segment_end_gradient, + EndGradient=last_segment_end_gradient, + PredefinedType="CONSTANTGRADIENT", + ) + segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters) + elif layout.is_a("IfcAlignmentCant"): + last_segment_dist_along = 0.0 + last_segment_cant_left = 0.0 + last_segment_cant_right = 0.0 + 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_cant_left = ( + last_segment.DesignParameters.EndCantLeft + if last_segment.DesignParameters.EndCantLeft != None + else last_segment.DesignParameters.StartCantLeft + ) + last_segment_cant_right = ( + last_segment.DesignParameters.EndCantRight + if last_segment.DesignParameters.EndCantRight != None + else last_segment.DesignParameters.StartCantRight + ) + + design_parameters = file.createIfcAlignmentCantSegment( + StartDistAlong=last_segment_dist_along, + HorizontalLength=0.0, + StartCantLeft=last_segment_cant_left, + StartCantRight=last_segment_cant_right, + PredefinedType="CONSTANTCANT", + ) + segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters) + + ifcopenshell.api.nest.assign_object(file, related_objects=[segment], relating_object=layout) + + if include_referent: + 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) + + return True \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create.py index e31826ce3b..04a7ebb582 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/create.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.api.aggregate import ifcopenshell.api.alignment import ifcopenshell.api.nest -import ifcopenshell.util.stationing +import ifcopenshell.util.alignment from ifcopenshell import entity_instance @@ -76,7 +76,7 @@ def create( _add_zero_length_segment(file, layout) # define stationing - name = ifcopenshell.util.stationing.station_as_string(file, start_station) + 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) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_offset_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_offset_curve.py index 957de43087..1667a7b352 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_offset_curve.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_offset_curve.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.api.aggregate import ifcopenshell.api.alignment import ifcopenshell.api.nest -import ifcopenshell.util.stationing +import ifcopenshell.util.alignment from ifcopenshell import entity_instance @@ -54,7 +54,7 @@ def create_as_offset_curve( _create_offset_curve_representation(file, alignment, offsets) # define stationing - name = ifcopenshell.util.stationing.station_as_string(file, start_station) + 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) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py index 22ac769e20..cd7ebeb896 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.api.aggregate import ifcopenshell.api.alignment import ifcopenshell.api.nest -import ifcopenshell.util.stationing +import ifcopenshell.util.alignment from ifcopenshell import entity_instance @@ -142,7 +142,7 @@ def create_as_polyline( _create_polyline_representation(file, alignment, points) # define stationing - name = ifcopenshell.util.stationing.station_as_string(file, start_station) + 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) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_representation.py new file mode 100644 index 0000000000..2760a44ad6 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_representation.py @@ -0,0 +1,53 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +from ifcopenshell import entity_instance + +from ifcopenshell.api.alignment._create_geometric_representation import _create_geometric_representation +from ifcopenshell.api.alignment._add_segment_to_curve import _add_segment_to_curve + +def create_representation( + file: ifcopenshell.file, + alignment: entity_instance, +) -> None: + """ + Creates the geometric representation of an alignment if it does not already exist. + This function is intended to be used when a model has only the semantic definition of an alignment + and you want to add the geometric representation. + + If the alignments are complete, it is recommended that add_zero_length_segment is called after this method to ensure + the proper structure of the semantic and geometric definitions of the alignment + + :param alignment: The alignment to create the representation. + """ + expected_type = "IfcAlignment" + if not alignment.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{alignment.is_a()}'.") + + if alignment.Representation: + return + + _create_geometric_representation(file,alignment) + + 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: + _add_segment_to_curve(file, segment, curve) \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_layout_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_layout_curve.py index 033a8919ac..0b5a70b127 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_layout_curve.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_layout_curve.py @@ -39,15 +39,16 @@ def get_layout_curve(layout: entity_instance) -> entity_instance: alignment = ifcopenshell.api.alignment.get_alignment(layout) curve = ifcopenshell.api.alignment.get_curve(alignment) - if layout.is_a("IfcAlignmentHorizontal"): - # Layout is horizontal so get the IfcCompositeCurve - if curve.is_a("IfcGradientCurve"): - curve = curve.BaseCurve - elif curve.is_a("IfcSegmentedReferenceCurve"): - curve = curve.BaseCurve.BaseCurve - elif layout.is_a("IfcAlignmentVertical"): - # Layout is vertical so get the IfcGradientCurve - if curve.is_a("IfcSegmentedReferenceCurve"): - curve = curve.BaseCurve + if curve: + if layout.is_a("IfcAlignmentHorizontal"): + # Layout is horizontal so get the IfcCompositeCurve + if curve.is_a("IfcGradientCurve"): + curve = curve.BaseCurve + elif curve.is_a("IfcSegmentedReferenceCurve"): + curve = curve.BaseCurve.BaseCurve + elif layout.is_a("IfcAlignmentVertical"): + # Layout is vertical so get the IfcGradientCurve + if curve.is_a("IfcSegmentedReferenceCurve"): + curve = curve.BaseCurve return curve diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/_get_mapped_segments.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_mapped_segments.py similarity index 81% rename from src/ifcopenshell-python/ifcopenshell/api/alignment/_get_mapped_segments.py rename to src/ifcopenshell-python/ifcopenshell/api/alignment/get_mapped_segments.py index 3f413a53e7..32d3163b7f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/_get_mapped_segments.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_mapped_segments.py @@ -22,7 +22,7 @@ from ifcopenshell import entity_instance from collections.abc import Sequence -def __get_curve_segment_count(segment: entity_instance) -> int: +def _get_curve_segment_count(segment: entity_instance) -> int: """ returns the number of IfcCurveSegment that an IfcAlignmentSegment maps to. generally this is a 1 to 1 mapping, with helmert curve being the exception @@ -35,25 +35,25 @@ def __get_curve_segment_count(segment: entity_instance) -> int: return 2 if segment.DesignParameters.PredefinedType == "HELMERTCURVE" else 1 -def _get_mapped_segments(file: ifcopenshell.file, layout_segment: entity_instance) -> Sequence[entity_instance]: +def get_mapped_segments(layout_segment: entity_instance) -> Sequence[entity_instance]: """ - Finds the IfcCurveSegment related to segment + From an IfcAlignmentSegment, returns the related IfcCurveSegment. Typically the sequence has one entity, + however there will be two for Helmert curve """ expected_type = "IfcAlignmentSegment" if not layout_segment.is_a(expected_type): raise TypeError(f"Expected to see type '{expected_type}', instead received '{layout_segment.is_a()}'.") layout = layout_segment.Nests[0].RelatingObject - alignment = ifcopenshell.api.alignment.get_alignment(layout) - curve = ifcopenshell.api.alignment.get_curve(alignment) + curve = ifcopenshell.api.alignment.get_layout_curve(layout) index = 0 for seg in layout.IsNestedBy[0].RelatedObjects: - index += __get_curve_segment_count(seg) + index += _get_curve_segment_count(seg) if seg == layout_segment: break - segment_count = __get_curve_segment_count(layout_segment) + segment_count = _get_curve_segment_count(layout_segment) if segment_count == 1: return (curve.Segments[index - segment_count], None) else: diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py index 02609c019f..0871fb12c5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py @@ -26,25 +26,29 @@ def has_zero_length_segment(layout: entity_instance) -> bool: """ Returns true if the layout ends with a zero length segment. - :param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant + :param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant, IfcCompositeCurve, IfcGradientCurve, or IfcSegmentedReferenceCurve :return: True if the zero length segment is present """ - expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"] + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant","IfcCompositeCurve","IfcGradientCurve","IfcSegmentedReferenceCurve"] if not layout.is_a() in expected_types: raise TypeError( f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{layout.is_a()}" ) result = False - for rel in layout.IsNestedBy: - if 0 < len(rel.RelatedObjects): - last_segment = rel.RelatedObjects[-1] - if last_segment.is_a("IfcAlignmentSegment"): - if last_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"): - result = last_segment.DesignParameters.SegmentLength == 0.0 - elif last_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"): - result = last_segment.DesignParameters.HorizontalLength == 0.0 - elif last_segment.DesignParameters.is_a("IfcAlignmentCantSegment"): - result = last_segment.DesignParameters.HorizontalLength == 0.0 + + if layout.is_a("IfcCompositeCurve") or layout.is_a("IfcGradientCurve") or layout.is_a("IfcSegmentedReferenceCurve"): + result = True if layout.Segments and 0 < len(layout.Segments) and layout.Segments[-1].SegmentLength.wrappedValue == 0.0 else False + else: + for rel in layout.IsNestedBy: + if 0 < len(rel.RelatedObjects): + last_segment = rel.RelatedObjects[-1] + if last_segment.is_a("IfcAlignmentSegment"): + if last_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"): + result = last_segment.DesignParameters.SegmentLength == 0.0 + elif last_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"): + result = last_segment.DesignParameters.HorizontalLength == 0.0 + elif last_segment.DesignParameters.is_a("IfcAlignmentCantSegment"): + result = last_segment.DesignParameters.HorizontalLength == 0.0 return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/update_fallback_position.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_fallback_position.py new file mode 100644 index 0000000000..c23921f3ce --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_fallback_position.py @@ -0,0 +1,63 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.util.placement +from ifcopenshell import entity_instance +import numpy as np + + +def update_fallback_position(file: ifcopenshell.file, lp: entity_instance): + """ + Updates the IfcLinearPlacement.CartesianPoint fallback position. + + If the CartesianPosition is not assigned to the IfcLinearPlacement, one will be created + + :param lp: The linear placement + :return: None + """ + + if not lp.CartesianPosition: + lp.CartesianPosition = file.createIfcAxis2Placement3D( + Location = file.createIfcCartesianPoint((0.,0.)) + ) + + p = np.array(ifcopenshell.util.placement.get_axis2placement(lp.RelativePlacement)) + + x = float(p[0, 3]) + y = float(p[1, 3]) + z = float(p[2, 3]) + + 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]) + + lp.CartesianPosition.Location.Coordinates = ((x,y,z)) + + if not lp.CartesianPosition.RefDirection: + lp.CartesianPosition.RefDirection = file.createIfcDirection((1.,0.,0.)) + + if not lp.CartesianPosition.Axis: + lp.CartesianPosition.Axis = file.createIfcDirection((0.,0.,1.)) + + lp.CartesianPosition.RefDirection.DirectionRatios = ((rx,ry,rz)) + lp.CartesianPosition.Axis.DirectionRatios = ((ax,ay,az)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py index e6802705f8..7f24b4eddf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py @@ -29,7 +29,7 @@ import ifcopenshell.template from ifcopenshell import entity_instance from ifcopenshell import ifcopenshell_wrapper import ifcopenshell.util -import ifcopenshell.util.stationing +import ifcopenshell.util.alignment def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray: diff --git a/src/ifcopenshell-python/ifcopenshell/util/stationing.py b/src/ifcopenshell-python/ifcopenshell/util/alignment.py similarity index 62% rename from src/ifcopenshell-python/ifcopenshell/util/stationing.py rename to src/ifcopenshell-python/ifcopenshell/util/alignment.py index 83669631ea..08376939ba 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/stationing.py +++ b/src/ifcopenshell-python/ifcopenshell/util/alignment.py @@ -20,6 +20,44 @@ import math import ifcopenshell import ifcopenshell.util.unit +def add_linear_placement_fallback_position(file:ifcopenshell.file)->ifcopenshell.file: + import ifcopenshell.api.alignment + patched_file = ifcopenshell.file.from_string(file.wrapped_data.to_string()) + + linear_placements = patched_file.by_type("IfcLinearPlacement") + for lp in linear_placements: + ifcopenshell.api.alignment.update_fallback_position(patched_file,lp) + + return patched_file + + +def create_alignment_geometry(file:ifcopenshell.file)->ifcopenshell.file: + import ifcopenshell.api.alignment + patched_file = ifcopenshell.file.from_string(file.wrapped_data.to_string()) + + alignments = patched_file.by_type("IfcAlignment") + for alignment in alignments: + ifcopenshell.api.alignment.create_representation(patched_file,alignment) + + return patched_file + +def append_zero_length_segments(file:ifcopenshell.file)->ifcopenshell.file: + """Appends zero length segments to all alignment layouts and layout geometry, if missing.""" + import ifcopenshell.api.alignment + patched_file = ifcopenshell.file.from_string(file.wrapped_data.to_string()) + + alignments = patched_file.by_type("IfcAlignment") + for alignment in alignments: + layouts = ifcopenshell.api.alignment.get_alignment_layouts(alignment) + for layout in layouts: + ifcopenshell.api.alignment.add_zero_length_segment(patched_file,layout,include_referent=False) + curve = ifcopenshell.api.alignment.get_layout_curve(layout) + if curve: + ifcopenshell.api.alignment.add_zero_length_segment(patched_file,curve) + + return patched_file + + def station_as_string(file: ifcopenshell.file, sta: float): """ diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py index 44dfb3c29b..b109475456 100644 --- a/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py +++ b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py @@ -75,3 +75,5 @@ def test_add_segment_to_layout(): assert ( alignment_segment.IsNestedBy[0].RelatedObjects[0].is_a("IfcReferent") ) # a referent is automatically added at the start of the segment + +test_add_segment_to_layout() \ No newline at end of file diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_vertical_alignment.py b/src/ifcopenshell-python/test/api/alignment/test_add_vertical_alignment.py index a2385c0ef4..256c9eab37 100644 --- a/src/ifcopenshell-python/test/api/alignment/test_add_vertical_alignment.py +++ b/src/ifcopenshell-python/test/api/alignment/test_add_vertical_alignment.py @@ -28,13 +28,6 @@ def test_add_vertical_alignment(): length = ifcopenshell.api.unit.add_si_unit(file, unit_type="LENGTHUNIT") ifcopenshell.api.unit.assign_unit(file, units=[length]) geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") - axis_model_representation_subcontext = ifcopenshell.api.context.add_context( - file, - context_type="Model", - context_identifier="Axis", - target_view="MODEL_VIEW", - parent=geometric_representation_context, - ) alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False) diff --git a/src/ifcopenshell-python/test/api/alignment/test_create.py b/src/ifcopenshell-python/test/api/alignment/test_create.py index d08d486417..7a6334d1b8 100644 --- a/src/ifcopenshell-python/test/api/alignment/test_create.py +++ b/src/ifcopenshell-python/test/api/alignment/test_create.py @@ -61,11 +61,25 @@ def test_create(): assert cant != None # verify each layout has a zero length segment (which also tests if the geometry curve has a zero length segment) - alignments = [horiz, vert, cant] - for a in alignments: - if a != None: - segments = ifcopenshell.api.alignment.get_layout_segments(a) + layouts = [horiz, vert, cant] + for layout in layouts: + if layout != None: + segments = ifcopenshell.api.alignment.get_layout_segments(layout) assert len(segments) == 1 - assert ifcopenshell.api.alignment.has_zero_length_segment( - a - ) # there is a check in this function for the geometry curve + assert ifcopenshell.api.alignment.has_zero_length_segment(layout) + curve = ifcopenshell.api.alignment.get_layout_curve(layout) + assert ifcopenshell.api.alignment.has_zero_length_segment(curve) + + curves = file.by_type("IfcCompositeCurve") + for curve in curves: + assert ifcopenshell.api.alignment.has_zero_length_segment(curve) + + curves = file.by_type("IfcGradientCurve") + for curve in curves: + assert ifcopenshell.api.alignment.has_zero_length_segment(curve) + + curves = file.by_type("IfcSegmentedReferenceCurve") + for curve in curves: + assert ifcopenshell.api.alignment.has_zero_length_segment(curve) + +test_create() \ No newline at end of file diff --git a/src/ifcopenshell-python/test/api/alignment/test_update_fallback_position.py b/src/ifcopenshell-python/test/api/alignment/test_update_fallback_position.py new file mode 100644 index 0000000000..dcb632ac94 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_update_fallback_position.py @@ -0,0 +1,68 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment + + +def test_update_fallback_position(): + file = ifcopenshell.file(schema="IFC4X3") + project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="Test") + length = ifcopenshell.api.unit.add_conversion_based_unit(file, name="foot") + ifcopenshell.api.unit.assign_unit(file, units=[length]) + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + + basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + pde = file.createIfcPointByDistanceExpression( + DistanceAlong = file.createIfcLengthMeasure(4500.), + OffsetLateral = 20.0, + BasisCurve = basis_curve + ) + + placement = file.createIfcAxis2PlacementLinear( + Location = pde, + ) + + lp = file.createIfcLinearPlacement(RelativePlacement = placement) + + assert lp.CartesianPosition == None + + ifcopenshell.api.alignment.update_fallback_position(file,lp) + + assert lp.CartesianPosition != None + assert lp.CartesianPosition.Location.Coordinates == pytest.approx((3781.0625905626425,2663.2859940077856,0.0)) + assert lp.CartesianPosition.RefDirection.DirectionRatios == pytest.approx((0.22453152656315067, 0.9744668252840736, 0.0)) + assert lp.CartesianPosition.Axis.DirectionRatios == pytest.approx((0.0, 0.0, 1.0)) + +test_update_fallback_position() diff --git a/src/ifcopenshell-python/test/util/test_stationing.py b/src/ifcopenshell-python/test/util/test_alignment.py similarity index 98% rename from src/ifcopenshell-python/test/util/test_stationing.py rename to src/ifcopenshell-python/test/util/test_alignment.py index dc0a03ced7..91c6ba707f 100644 --- a/src/ifcopenshell-python/test/util/test_stationing.py +++ b/src/ifcopenshell-python/test/util/test_alignment.py @@ -18,7 +18,7 @@ import ifcopenshell import ifcopenshell.api.unit -import ifcopenshell.util.stationing as sta +import ifcopenshell.util.alignment as sta def _test_si_stations():