From a392a1fbfb1a9f8af104c1128f254902c7605d11 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Thu, 17 Jul 2025 10:47:13 -0700 Subject: [PATCH] Adds alignment as offset curve --- src/ifcgeom/function_item_evaluator.h | 10 ++ .../ifcopenshell/api/alignment/__init__.py | 2 + .../_create_offset_curve_representation.py | 72 +++++++++ .../_create_polyline_representation.py | 26 +++- .../api/alignment/add_stationing_referent.py | 4 + .../api/alignment/create_as_offset_curve.py | 66 +++++++++ .../api/alignment/create_as_polyline.py | 90 +++++++++++- .../api/alignment/get_basis_curve.py | 11 +- .../alignment/test_create_as_offset_curve.py | 138 ++++++++++++++++++ 9 files changed, 407 insertions(+), 12 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_offset_curve.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_create_as_offset_curve.py diff --git a/src/ifcgeom/function_item_evaluator.h b/src/ifcgeom/function_item_evaluator.h index a936bfbe89..642d66a8ef 100644 --- a/src/ifcgeom/function_item_evaluator.h +++ b/src/ifcgeom/function_item_evaluator.h @@ -11,6 +11,16 @@ namespace ifcopenshell { namespace geometry { /// Returns (x,y,theta) at L/2. The results are in a vector so they can be returned to python std::vector helmert_curve_point(double A0, double A1, double A2, double s); +/// @brief Converts a loop to a function item. +/// This is intended to be used from python side. Polylines are mapped to a loop, but when +/// representing an alignment they need to be a function_item so the can be evaluated by function_item_evaluator. +/// On the C++ side, the dcast operator take care of this, but dcast is not accessible on the python side. +/// @param loop +/// @return +static taxonomy::function_item::ptr convert_loop_to_function_item(taxonomy::loop::ptr loop) { + return ifcopenshell::geometry::taxonomy::dcast(loop); +} + /// @brief Abstract class for evaluating a function_item. This class is specialized for each of the function_item types. struct fn_evaluator { fn_evaluator(const ifcopenshell::geometry::Settings& settings) : settings_(settings) { diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py index 10d9b5f2a3..5c2a6b6049 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py @@ -51,6 +51,7 @@ from .add_stationing_referent import add_stationing_referent from .add_vertical_layout import add_vertical_layout from .create_layout_segment import create_layout_segment from .create import create +from .create_as_offset_curve import create_as_offset_curve 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 @@ -82,6 +83,7 @@ __all__ = [ "add_vertical_layout", "create_layout_segment", "create", + "create_as_offset_curve", "create_as_polyline", "create_by_pi_method", "create_from_csv", diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py new file mode 100644 index 0000000000..402a6c3513 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py @@ -0,0 +1,72 @@ +# 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.geometry +from ifcopenshell import entity_instance + +import math +from collections.abc import Sequence + + +def _create_offset_curve_representation( + file: ifcopenshell.file, alignment: entity_instance, offsets: Sequence[entity_instance] +) -> None: + """ + Create geometric representation for the alignment based on an IfcPolyline + + :param alignment: The alignment for which the representation is being created + :return: None + """ + expected_type = "IfcAlignment" + if not alignment.is_a(expected_type): + raise TypeError(f"Expected {expected_type} but got {alignment.is_a()}") + + axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) + + basis_curve = offsets[0].BasisCurve # IfcPointByDistanceExpression.BasisCurve + + if basis_curve.Dim == 3: + placement = file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=file.createIfcAxis2Placement3D( + Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0, 0.0)) + ), + ) + representation_type = "Curve3D" + else: + placement = file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)) + ), + ) + representation_type = "Curve2D" + + curve = file.createIfcOffsetCurveByDistances(BasisCurve=basis_curve, OffsetValues=offsets) + + representation = file.createIfcShapeRepresentation( + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType=representation_type, + Items=(curve,), + ) + + alignment.ObjectPlacement = placement + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_polyline_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_polyline_representation.py index 97b78cfd04..e8329a5b1a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_polyline_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_polyline_representation.py @@ -26,7 +26,7 @@ from collections.abc import Sequence def _create_polyline_representation( - file: ifcopenshell.file, alignment: entity_instance, points: Sequence[Sequence[float]] + file: ifcopenshell.file, alignment: entity_instance, points: Sequence[entity_instance] ) -> None: """ Create geometric representation for the alignment based on an IfcPolyline @@ -40,19 +40,29 @@ def _create_polyline_representation( axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) - placement = file.createIfcLocalPlacement( - PlacementRelTo=None, - RelativePlacement=file.createIfcAxis2Placement3D( - Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0, 0.0)) - ), - ) + if points[0].Dim == 3: + placement = file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=file.createIfcAxis2Placement3D( + Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0, 0.0)) + ), + ) + representation_type = "Curve3D" + else: + placement = file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)) + ), + ) + representation_type = "Curve2D" curve = file.createIfcPolyLine(Points=points) representation = file.createIfcShapeRepresentation( ContextOfItems=axis_geom_subcontext, RepresentationIdentifier="Axis", - RepresentationType="Curve3D", + RepresentationType=representation_type, Items=(curve,), ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py index 74e145044b..eece9ce581 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py @@ -86,6 +86,10 @@ def add_stationing_referent( settings = ifcopenshell.geom.settings() fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data) + + if basis_curve.is_a("IfcPolyline") or basis_curve.is_a("IfcIndexedPolyCurve"): + 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) 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 new file mode 100644 index 0000000000..957de43087 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_offset_curve.py @@ -0,0 +1,66 @@ +# 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.aggregate +import ifcopenshell.api.alignment +import ifcopenshell.api.nest +import ifcopenshell.util.stationing + +from ifcopenshell import entity_instance + +from ifcopenshell.api.alignment._create_offset_curve_representation import _create_offset_curve_representation + +from collections.abc import Sequence + + +def create_as_offset_curve( + file: ifcopenshell.file, + name: str, + offsets: Sequence[entity_instance], + start_station: float = 0.0, +) -> entity_instance: + """ + Creates a new IfcAlignment with an IfcOffsetCurveByDistances representation. + + The IfcAlignment is aggreated to IfcProject + + :param file: + :param name: name assigned to IfcAlignment.Name + :param offsets: offsets from the basis curve that defines the offset curve, expected to be IfcOffsetCurveByDistances. + :param start_station: station value at the start of the alignment + :return: Returns an IfcAlignment + """ + alignment = file.createIfcAlignment( + GlobalId=ifcopenshell.guid.new(), + Name=name, + ) + + _create_offset_curve_representation(file, alignment, offsets) + + # define stationing + name = ifcopenshell.util.stationing.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: + ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project) + + return alignment 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 826e8226dd..22ac769e20 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py @@ -25,10 +25,96 @@ import ifcopenshell.util.stationing from ifcopenshell import entity_instance from ifcopenshell.api.alignment._create_polyline_representation import _create_polyline_representation -from ifcopenshell.api.alignment._add_zero_length_segment import _add_zero_length_segment + +import math from collections.abc import Sequence +def _create_layout(file: ifcopenshell.file, alignment: entity_instance, points: Sequence[entity_instance]): + """ + I don't believe it is required for polylines, but the validation serivce gives an error if the alignment doesn't have a layout + """ + include_vertical = False if points[0].Dim == 2 else True + + alignment_layouts = [] + + alignment_layouts.append(file.createIfcAlignmentHorizontal(GlobalId=ifcopenshell.guid.new())) + + if include_vertical: + alignment_layouts.append(file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new())) + + ifcopenshell.api.nest.assign_object(file, related_objects=alignment_layouts, relating_object=alignment) + + start_dist_along = 0.0 + for p1, p2 in zip(points, points[1:]): + x1, y1, z1 = p1.Coordinates + x2, y2, z2 = p2.Coordinates + dir = math.atan2(y2 - y1, x2 - x1) + gradient = (z2 - z1) / (x2 - x1) + length = math.sqrt(math.pow((x2 - x1), 2.0) + math.pow((y2 - y1), 2.0)) + + hsegment = file.createIfcAlignmentSegment( + ifcopenshell.guid.new(), + DesignParameters=file.createIfcAlignmentHorizontalSegment( + StartPoint=p1, + StartDirection=dir, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=length, + PredefinedType="LINE", + ), + ) + + ifcopenshell.api.nest.assign_object(file, related_objects=[hsegment], relating_object=alignment_layouts[0]) + + if include_vertical: + vsegment = file.createIfcAlignmentSegment( + ifcopenshell.guid.new(), + DesignParameters=file.createIfcAlignmentVerticalSegment( + StartDistAlong=start_dist_along, + HorizontalLength=length, + StartHeight=z1, + StartGradient=gradient, + EndGradient=gradient, + PredefinedType="CONSTANTGRADIENT", + ), + ) + + ifcopenshell.api.nest.assign_object(file, related_objects=[vsegment], relating_object=alignment_layouts[1]) + + start_dist_along += length + + # zero length segment + hsegment = file.createIfcAlignmentSegment( + ifcopenshell.guid.new(), + DesignParameters=file.createIfcAlignmentHorizontalSegment( + StartPoint=points[-1], + StartDirection=dir, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=0.0, + PredefinedType="LINE", + ), + ) + + ifcopenshell.api.nest.assign_object(file, related_objects=[hsegment], relating_object=alignment_layouts[0]) + + if include_vertical: + vsegment = file.createIfcAlignmentSegment( + ifcopenshell.guid.new(), + DesignParameters=file.createIfcAlignmentVerticalSegment( + StartDistAlong=start_dist_along, + HorizontalLength=0.0, + StartHeight=points[-1].Coordinates[-1], + StartGradient=gradient, + EndGradient=gradient, + PredefinedType="CONSTANTGRADIENT", + ), + ) + + ifcopenshell.api.nest.assign_object(file, related_objects=[vsegment], relating_object=alignment_layouts[1]) + + def create_as_polyline( file: ifcopenshell.file, name: str, @@ -51,6 +137,8 @@ def create_as_polyline( Name=name, ) + # _create_layout(file,alignment,points) + _create_polyline_representation(file, alignment, points) # define stationing diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py index d061fa412e..b96c830757 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py @@ -41,9 +41,14 @@ def get_basis_curve(alignment: entity_instance) -> entity_instance: representations = ifcopenshell.util.representation.get_representations_iter(alignment) for representation in representations: - if (representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D") or ( - representation.RepresentationIdentifier == "FootPrint" and representation.RepresentationType == "Curve2D" - ): + if ( + (representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D") + or ( + representation.RepresentationIdentifier == "FootPrint" + and representation.RepresentationType == "Curve2D" + ) + or (representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D") + ): # in the case of IfcPolyline or IfcIndexedPolyCurve with 3D points axis = representation return None if axis.Items == None or len(axis.Items) == 0 else axis.Items[0] diff --git a/src/ifcopenshell-python/test/api/alignment/test_create_as_offset_curve.py b/src/ifcopenshell-python/test/api/alignment/test_create_as_offset_curve.py new file mode 100644 index 0000000000..279b96c4e7 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_create_as_offset_curve.py @@ -0,0 +1,138 @@ +# 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 math +import ifcopenshell.api.alignment +import ifcopenshell.api.unit + + +def test_create_as_offset_curve(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="OCBD Test Alignment") + length = ifcopenshell.api.unit.add_si_unit(file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(file, units=[length]) + + # create main alignment + alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=True) + layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment) + + segment1 = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=500, + PredefinedType="LINE", + ) + + end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment1) + + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) + + x = float(end[0, 3]) / unit_scale + y = float(end[1, 3]) / unit_scale + dx = float(end[0, 0]) + dy = float(end[1, 0]) + dir = math.atan2(dy, dx) + segment2 = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((x, y)), + StartDirection=dir, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100, + PredefinedType="CLOTHOID", + ) + end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment2) + + x = float(end[0, 3]) / unit_scale + y = float(end[1, 3]) / unit_scale + dx = float(end[0, 0]) + dy = float(end[1, 0]) + dir = math.atan2(dy, dx) + segment3 = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((x, y)), + StartDirection=dir, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=1500.0, + PredefinedType="CIRCULARARC", + ) + end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment3) + + x = float(end[0, 3]) / unit_scale + y = float(end[1, 3]) / unit_scale + dx = float(end[0, 0]) + dy = float(end[1, 0]) + dir = math.atan2(dy, dx) + segment4 = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((x, y)), + StartDirection=dir, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100, + PredefinedType="CLOTHOID", + ) + end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment4) + + x = float(end[0, 3]) / unit_scale + y = float(end[1, 3]) / unit_scale + dx = float(end[0, 0]) + dy = float(end[1, 0]) + dir = math.atan2(dy, dx) + segment5 = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((x, y)), + StartDirection=dir, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=800.0, + PredefinedType="LINE", + ) + end = ifcopenshell.api.alignment.create_layout_segment(file, layout, segment5) + + # create vertical for main alignment + vlayout = ifcopenshell.api.alignment.get_vertical_layout(alignment) + + segment1 = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=3000.0, + StartHeight=100.0, + StartGradient=1.75 / 100.0, + EndGradient=1.75 / 100.0, + PredefinedType="CONSTANTGRADIENT", + ) + + end = ifcopenshell.api.alignment.create_layout_segment(file, vlayout, segment1) + + # create the offset alignment + basis_curve = ifcopenshell.api.alignment.get_curve(alignment) # want the IfcGradientCurve + + offsets = [ + file.createIfcPointByDistanceExpression( + DistanceAlong=file.createIfcLengthMeasure(0.0), OffsetLateral=100.0, BasisCurve=basis_curve + ), + ] + + offset_alignment = ifcopenshell.api.alignment.create_as_offset_curve(file, "A2", offsets) + assert offset_alignment.is_a("IfcAlignment") + curve = ifcopenshell.api.alignment.get_curve(offset_alignment) + assert curve.is_a("IfcOffsetCurveByDistances") + assert curve.BasisCurve == basis_curve + + +test_create_as_offset_curve()