diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py index e2542360e1..42a5057c2e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py @@ -87,6 +87,7 @@ from .layout_vertical_alignment_by_pi_method import ( layout_vertical_alignment_by_pi_method, ) from .name_segments import name_segments +from .segment_vertices import segment_vertices from .update_fallback_position import update_fallback_position from .util import * @@ -125,6 +126,7 @@ __all__ = [ "layout_horizontal_alignment_by_pi_method", "layout_vertical_alignment_by_pi_method", "name_segments", + "segment_vertices", "register_referent_name_callback", "update_fallback_position", "get_mapped_segments", diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/segment_vertices.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/segment_vertices.py new file mode 100644 index 0000000000..6ca5008644 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/segment_vertices.py @@ -0,0 +1,90 @@ +# 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 numpy as np + +import ifcopenshell +import ifcopenshell.util.unit +from ifcopenshell import entity_instance, ifcopenshell_wrapper + +def _intersect_lines(p1,d1,p2,d2): + x1,y1 = p1 + dx1,dy1 = d1 + x2,y2 = p2 + dx2,dy2 = d2 + + det = dx1*dy2 - dy1*dx2 + if abs(det) < 1e-12: + return None # lines are parallel + + t = ((x2 - x1) * dy2 - (y2 - y1) * dx2)/det + + x = x1 + t*dx1 + y = y1 + t*dy1 + + return (x,y) + + +def segment_vertices(file: ifcopenshell.file, curve_segment: entity_instance): + """ + Genererates segment vertices. Segment vertices are the points where the tangents + at the start and end of the segment intersect (the PI point) and where lines + perpendicular to the start and end of the segment intersect (CC). + + :param curve_segment: A curve segment + :return: tuples for Start, End, PI, CC + """ + supported_segment_types = ["IFCCURVESEGMENT"] + segment_type = curve_segment.is_a().upper() + if not segment_type in supported_segment_types: + raise NotImplementedError( + f"Expected entity type to be one of {[_ for _ in supported_segment_types]}, got '{segment_type}" + ) + + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) + + settings = ifcopenshell.geom.settings() + segment_fn = ifcopenshell_wrapper.map_shape(settings, curve_segment.wrapped_data) + segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn) + + s = segment_evaluator.evaluate(segment_fn.start()) + start = np.array(s) + sx = float(start[0, 3]) / unit_scale + sy = float(start[1, 3]) / unit_scale + sdx = float(start[0, 0]) + sdy = float(start[1, 0]) + + e = segment_evaluator.evaluate(segment_fn.end()) + end = np.array(e) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) + ex = float(end[0, 3]) / unit_scale + ey = float(end[1, 3]) / unit_scale + edx = float(end[0, 0]) + edy = float(end[1, 0]) + + pi = _intersect_lines((sx,sy),(sdx,sdy),(ex,ey),(edx,edy)) + + sdx = float(start[0, 1]) + sdy = float(start[1, 1]) + edx = float(end[0, 1]) + edy = float(end[1, 1]) + + cc = _intersect_lines((sx,sy),(sdx,sdy),(ex,ey),(edx,edy)) + + return (sx,sy), (ex,ey), pi, cc diff --git a/src/ifcopenshell-python/test/api/alignment/test_segment_vertices.py b/src/ifcopenshell-python/test/api/alignment/test_segment_vertices.py new file mode 100644 index 0000000000..bf5f77c8ba --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_segment_vertices.py @@ -0,0 +1,87 @@ +# 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.context +import ifcopenshell.api.unit + + +def test_segment_vertices(): + file = ifcopenshell.file(schema="IFC4X3") + 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]) + 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 + ) + + # test the horizontal alignment geometry segments + expect = [[(500.0, 2500.0), (2142.2379952109395, 1436.01482000418), None, None], + [(2142.2379952109395, 1436.01482000418), (3660.446122847804, 2050.7361731594674), (3340.0, 659.9999999999998), (2685.9792975637306, 2275.267699722618)], + [(3660.4461228478035, 2050.7361731594674), (4084.115884236641, 3889.4629375870213), None, None], + [(4084.115884236641, 3889.4629375870218), (5469.395067206271, 4847.5663099476205), (4340.0, 5000.000000000001), (5302.199415841732, 3608.7985293830834)], + [(5469.395067206271, 4847.56630994762), (7019.971366858418, 4638.286073184753), None, None], + [(7019.971366858417, 4638.286073184753), (7790.932128312586, 4006.7307645487535), (7600.0, 4560.0), (6892.902671821368, 3696.8225599557054)], + [(7790.932128312587, 4006.7307645487535), (8480.0, 2010.0000000000002), None, None], + [(8480.0, 2010.0000000000002), (8480.0, 2010.0000000000002), None, None] + ] + curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + for segment,expected in zip(curve.Segments,expect): + s,e,pi,cc = ifcopenshell.api.alignment.segment_vertices(file,segment) + assert s == pytest.approx(expected[0]) + assert e == pytest.approx(expected[1]) + assert pi == pytest.approx(expected[2]) + assert cc == pytest.approx(expected[3]) + + + # test vertical curve segments + expect = [[(0.0, 100.0), (1200.0, 121.0), None, None], + [(1200.0, 121.0), (2799.99999384661, 127.00000006153391), (1999.9999969233054, 134.99999994615786), (2218.1436363635016, -58058.63636362867)], + [(2800.0, 127.0), (4400.0, 111.0), None, None], + [(4400.0, 111.0), (5599.999994508736, 116.9999998901747), (4999.999997254367, 105.00000002745632), (4800.039999999177, 40114.99999991764)], + [(5600.0, 117.0), (6400.0, 133.0), None, None], + [(6400.0, 133.0), (8399.999995932576, 133.0000000813485), (7399.999997966288, 152.99999995932575), (7399.999999999187, -49866.99999995936)], + [(8400.0, 133.0), (9400.0, 113.0), None, None], + [(9400.0, 113.0), (10199.99999633883, 103.00000001830585), (9799.999998169415, 105.00000003661171), (10466.733333334432, 53449.66666672164)], + [(10200.0, 103.0), (12800.0, 90.0), None, None], + [(12800.0, 90.0), (12800.0, 90.0), None, None] + ] + curve = ifcopenshell.api.alignment.get_curve(alignment) + for segment,expected in zip(curve.Segments,expect): + s,e,pi,cc = ifcopenshell.api.alignment.segment_vertices(file,segment) + assert s == pytest.approx(expected[0]) + assert e == pytest.approx(expected[1]) + assert pi == pytest.approx(expected[2]) + assert cc == pytest.approx(expected[3]) + +test_segment_vertices()