From 73b76ba666710e1dcbfc272e3680458ea4c55b4e Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:34:52 -0700 Subject: [PATCH] Adds IfcPolyline representation for alignment. Also some general cleanup --- .../ifcopenshell/api/alignment/__init__.py | 4 +- .../api/alignment/_add_segment_to_layout.py | 2 +- .../api/alignment/_add_zero_length_segment.py | 2 +- .../_create_polyline_representation.py | 57 +++++++++++++ .../api/alignment/add_stationing_referent.py | 79 ++++++++++--------- .../{create_alignment.py => create.py} | 5 +- .../api/alignment/create_as_polyline.py | 68 ++++++++++++++++ .../ifcopenshell/api/alignment/get_curve.py | 1 + ...est_create_alignment.py => test_create.py} | 3 +- .../api/alignment/test_create_as_polyline.py | 50 ++++++++++++ ..._method.py => test_create_by_pi_method.py} | 4 +- 11 files changed, 230 insertions(+), 45 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/_create_polyline_representation.py rename src/ifcopenshell-python/ifcopenshell/api/alignment/{create_alignment.py => create.py} (95%) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py rename src/ifcopenshell-python/test/api/alignment/{test_create_alignment.py => test_create.py} (99%) create mode 100644 src/ifcopenshell-python/test/api/alignment/test_create_as_polyline.py rename src/ifcopenshell-python/test/api/alignment/{test_create_alignment_by_pi_method.py => test_create_by_pi_method.py} (97%) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py index 9018281221..10d9b5f2a3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py @@ -50,7 +50,8 @@ Future versions of this API may support: 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_alignment import create +from .create import create +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 @@ -81,6 +82,7 @@ __all__ = [ "add_vertical_layout", "create_layout_segment", "create", + "create_as_polyline", "create_by_pi_method", "create_from_csv", "create_segment_representations", 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 d0f6552dc6..4bbe28f1dd 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 @@ -171,7 +171,7 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg 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)})" referent = ifcopenshell.api.alignment.add_stationing_referent( - file, segment, basis_curve=basis_curve, distance_along=dist_along, station=station, name=name + file, segment, distance_along=dist_along, station=station, name=name ) if len(curve.Segments) == 2 and layout.is_a("IfcAlignmentHorizontal"): 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 aa8c95e4ef..54fa43bc5b 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 @@ -142,4 +142,4 @@ def _add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance) - ] 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, curve, 0.0, 0.0, name=name) + ifcopenshell.api.alignment.add_stationing_referent(file, segment, 0.0, 0.0, name=name) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_polyline_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_polyline_representation.py new file mode 100644 index 0000000000..5c5b423e66 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_polyline_representation.py @@ -0,0 +1,57 @@ +# 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_polyline_representation(file: ifcopenshell.file, alignment: entity_instance, points: Sequence[Sequence[float]]) -> 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) + + placement = file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=file.createIfcAxis2Placement3D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0, 0.0))), + ) + + curve = file.createIfcPolyLine(Points=points) + + representation = file.createIfcShapeRepresentation( + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(curve,), + ) + + + alignment.ObjectPlacement = placement + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) 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 8751280fa2..0c3af185ac 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_referent.py @@ -31,7 +31,6 @@ import numpy as np def add_stationing_referent( file: ifcopenshell.file, element: entity_instance, - basis_curve: entity_instance, distance_along: float, station: float, name: str, @@ -41,8 +40,7 @@ def add_stationing_referent( If element is an IfcAlignment, IfcReferent.PredefinedType is set to "STATION", otherwise "POSITION" :param element: the element to receive the referent, expected to be an IfcAlignment or IfcAlignmentSegment - :param basis_curve: the basis curve for positining - :param distance_along: distance along the basis curve + :param distance_along: distance along the alignment curve :param station: station value :param name: name to assign to IfcReferent.Name, typically a stringized version of the station value :return: referent @@ -52,32 +50,19 @@ def add_stationing_referent( .. code:: python alignment = model.by_type("IfcAlignment")[0] - basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) - ifcopenshell.api.alignment.add_stationing_referent(model,entity=alignment,basis_curve=basis_curve,distance_along=0.0,station=100.0) + ifcopenshell.api.alignment.add_stationing_referent(model,entity=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) + object_placement = None representation = None if basis_curve: - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) - - settings = ifcopenshell.geom.settings() - fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data) - evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, fn) - p = evaluator.evaluate(distance_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]) - object_placement = file.createIfcLinearPlacement( RelativePlacement=file.createIfcAxis2PlacementLinear( Location=file.createIfcPointByDistanceExpression( @@ -88,12 +73,40 @@ def add_stationing_referent( BasisCurve=basis_curve, ) ), - CartesianPosition=file.createIfcAxis2Placement3D( - Location=file.createIfcCartesianPoint((x, y, z)), - Axis=file.createIfcDirection((ax, ay, az)), - RefDirection=file.createIfcDirection((rx, ry, rz)), - ), ) + + is_valid_curve = True + 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: + is_valid_curve = False + + if is_valid_curve: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) + + settings = ifcopenshell.geom.settings() + fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, fn) + p = evaluator.evaluate(distance_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]) + + 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( @@ -118,12 +131,6 @@ def add_stationing_referent( ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": station}) ifcopenshell.api.nest.assign_object(file, related_objects=[referent], relating_object=element) - alignment = element - - if element.is_a("IfcAlignmentSegment"): - layout = element.Nests[0].RelatingObject - alignment = ifcopenshell.api.alignment.get_alignment(layout) - if len(alignment.Positions) == 0: rel_positions = file.createIfcRelPositions( GlobalId=ifcopenshell.guid.new(), diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create.py similarity index 95% rename from src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment.py rename to src/ifcopenshell-python/ifcopenshell/api/alignment/create.py index 0a4526829c..d54c57a32f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create.py @@ -76,10 +76,9 @@ def create( _add_zero_length_segment(file, layout) # define stationing - basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) name = ifcopenshell.util.stationing.station_as_string(file, start_station) referent = ifcopenshell.api.alignment.add_stationing_referent( - file, alignment, basis_curve, 0.0, start_station, name + file, alignment, 0.0, start_station, name ) ifcopenshell.api.nest.reorder_nesting(file, referent, -1, 0) @@ -88,4 +87,4 @@ def create( if project: ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project) - return alignment + return alignment \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.py new file mode 100644 index 0000000000..feae6afc92 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_as_polyline.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 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_polyline_representation import _create_polyline_representation +from ifcopenshell.api.alignment._add_zero_length_segment import _add_zero_length_segment +from collections.abc import Sequence + + +def create_as_polyline( + file: ifcopenshell.file, + name: str, + points: Sequence[entity_instance], + start_station: float = 0.0, +) -> entity_instance: + """ + Creates a new IfcAlignment with an IfcPolyline representation. + + The IfcAlignment is aggreated to IfcProject + + :param file: + :param name: name assigned to IfcAlignment.Name + :param points: sequence of points defining the polyline + :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_polyline_representation(file, alignment, points) + + # 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 \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py index 7365bbeec8..aeb018976a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py @@ -26,6 +26,7 @@ import ifcopenshell.util.representation def get_curve(alignment: entity_instance) -> entity_instance: """ Returns the geometric representation curve for an alignment. + An alignment without layouts will have a curve of type IfcPolyLine or IfcIndexedPolyCurve A horizontal only will have a curve of type IfcCompositeCurve A horizontal+vertical will have a curve of type IfcGradientCurve A horizontal+vertical+cant will have a curve of tyep IfcSegmentedReferenceCurve diff --git a/src/ifcopenshell-python/test/api/alignment/test_create_alignment.py b/src/ifcopenshell-python/test/api/alignment/test_create.py similarity index 99% rename from src/ifcopenshell-python/test/api/alignment/test_create_alignment.py rename to src/ifcopenshell-python/test/api/alignment/test_create.py index 4281ff529c..8a068e66f8 100644 --- a/src/ifcopenshell-python/test/api/alignment/test_create_alignment.py +++ b/src/ifcopenshell-python/test/api/alignment/test_create.py @@ -22,7 +22,7 @@ import ifcopenshell.api.context import ifcopenshell.api.unit -def test_create_alignment(): +def test_create(): 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") @@ -69,3 +69,4 @@ def test_create_alignment(): assert ifcopenshell.api.alignment.has_zero_length_segment( a ) # there is a check in this function for the geometry curve + diff --git a/src/ifcopenshell-python/test/api/alignment/test_create_as_polyline.py b/src/ifcopenshell-python/test/api/alignment/test_create_as_polyline.py new file mode 100644 index 0000000000..84650c08d1 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_create_as_polyline.py @@ -0,0 +1,50 @@ +# 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 +import ifcopenshell.api.unit + + +def test_create_as_polyline(): + 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]) + + points = [ + file.createIfcCartesianPoint((945.2, 583.6, 50.0)), + file.createIfcCartesianPoint((756.3, 871.7, 66.2)), + file.createIfcCartesianPoint(( 567.4, 1159.7, 78.5)), + file.createIfcCartesianPoint(( 379.4, 1448.3, 86.8)), + file.createIfcCartesianPoint(( 201.7, 1743.3, 91.1)), + file.createIfcCartesianPoint(( 36.8, 2045.7, 91.3)), + file.createIfcCartesianPoint((-118.9, 2353.0, 87.5)), + file.createIfcCartesianPoint((-274.3, 2660.4, 79.7)), + file.createIfcCartesianPoint((-429.6, 2967.8, 68.3)), + file.createIfcCartesianPoint((-585.0, 3275.2, 56.2 )), + ] + + + alignment = ifcopenshell.api.alignment.create_as_polyline(file,"A1",points) + curve = ifcopenshell.api.alignment.get_curve(alignment) + assert curve.is_a("IfcPolyline") + assert len(curve.Points) == 10 + + +test_create_as_polyline() \ No newline at end of file diff --git a/src/ifcopenshell-python/test/api/alignment/test_create_alignment_by_pi_method.py b/src/ifcopenshell-python/test/api/alignment/test_create_by_pi_method.py similarity index 97% rename from src/ifcopenshell-python/test/api/alignment/test_create_alignment_by_pi_method.py rename to src/ifcopenshell-python/test/api/alignment/test_create_by_pi_method.py index db8ed9bb89..0f1bb5fd3a 100644 --- a/src/ifcopenshell-python/test/api/alignment/test_create_alignment_by_pi_method.py +++ b/src/ifcopenshell-python/test/api/alignment/test_create_by_pi_method.py @@ -22,7 +22,7 @@ import ifcopenshell.api.context import ifcopenshell.api.unit -def test_create_alignment_pi_method(): +def test_create_by_pi_method(): 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") @@ -64,4 +64,4 @@ def test_create_alignment_pi_method(): ) # segments in vertical layout -test_create_alignment_pi_method() +test_create_by_pi_method()