From 03ee826e67ceb4f459440f69f745eb112bf41246 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Jun 2024 22:22:25 +1000 Subject: [PATCH] Add tests for xaxis and yaxis angle conversions This tricked me today, the "XAxis" and "YAxis" vectors are from different perspectives (yikes!). XAxis is from the perspective of grid north, and YAxis is from the perspective of project north. I'm following this convention because that's what buildingSMART docs do but just FYI they serve two different purposes! --- .../ifcopenshell/util/geolocation.py | 30 ++++++++++++++ .../test/util/test_geolocation.py | 40 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index 876023fc2e..4d6a7d687e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -22,6 +22,7 @@ import numpy.typing as npt import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.element +import ifcopenshell.util.placement from typing import NamedTuple, Optional, Union @@ -522,6 +523,14 @@ def auto_global2local( def xaxis2angle(x: float, y: float) -> float: """Converts X axis abscissa and ordinates to an angle in decimal degrees + The X axis abscissa and ordinate is how IFC stores grid north. + + This X axis vector indicates "where is project east, if grid north is up + the page?". See the diagram on the IfcGeometricRepresentationContext + documentation for clarification. + + The angle indicates "how do I rotate grid east to get to project east?". + :param x: The X axis abscissa :param y: The X axis ordinate :return: The equivalent angle in decimal degrees from the X axis @@ -534,6 +543,12 @@ def yaxis2angle(x: float, y: float) -> float: The Y axis abscissa and ordinate is how IFC stores true north. + This Y axis vector indicates "where is true north, if project north is up + the page?". See the diagram on the IfcGeometricRepresentationContext + documentation for clarification. + + The angle indicates "how do I rotate project north to get to true north?". + :param x: The Y axis abscissa :param y: The Y axis ordinate :return: The equivalent angle in decimal degrees from the Y axis @@ -617,3 +632,18 @@ def angle2yaxis(angle: float) -> tuple[float, float]: x = -math.sin(angle_rad) y = math.cos(angle_rad) return x, y + + +def get_wcs(ifc_file: ifcopenshell.file) -> Optional[npt.NDArray[np.float64]]: + """Gets the WCS (prioritising 3D contexts) as a matrix + + :param: The IFC file + :return: A 4x4 matrix in project units + """ + wcs = None + for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + wcs = context.WorldCoordinateSystem + if context.ContextType == "Model": + break + if wcs: + return ifcopenshell.util.placement.get_axis2placement(wcs) diff --git a/src/ifcopenshell-python/test/util/test_geolocation.py b/src/ifcopenshell-python/test/util/test_geolocation.py index 1b54973d7a..0d9d5d92c4 100644 --- a/src/ifcopenshell-python/test/util/test_geolocation.py +++ b/src/ifcopenshell-python/test/util/test_geolocation.py @@ -358,3 +358,43 @@ class TestAutoGlobal2Local(test.bootstrap.IFC4): m2[:, 1][0:3] = [-1, 0, 0] m2[:, 3][0:3] = [0, 3, 3] assert np.allclose(subject.auto_global2local(self.file, m2), m) + + +class TestXAxis2Angle(test.bootstrap.IFC4): + def test_run(self): + assert subject.xaxis2angle(1, 0) == 0 + assert subject.xaxis2angle(1, 1) == -45 + assert subject.xaxis2angle(-1, 1) == -135 + assert subject.xaxis2angle(1, -1) == 45 + assert subject.xaxis2angle(-1, -1) == 135 + assert np.isclose(subject.xaxis2angle(0.8660254, -0.5), 30) + + +class TestAngle2XAxis(test.bootstrap.IFC4): + def test_run(self): + a = 0.707106781186 + assert np.allclose(subject.angle2xaxis(0), (1, 0)) + assert np.allclose(subject.angle2xaxis(-45), (a, a)) + assert np.allclose(subject.angle2xaxis(-135), (-a, a)) + assert np.allclose(subject.angle2xaxis(45), (a, -a)) + assert np.allclose(subject.angle2xaxis(135), (-a, -a)) + + +class TestYAxis2Angle(test.bootstrap.IFC4): + def test_run(self): + assert subject.yaxis2angle(0, 1) == 0 + assert np.isclose(subject.yaxis2angle(-0.5, 0.8660254), 30) + assert subject.yaxis2angle(1, 1) == -45 + assert subject.yaxis2angle(-1, 1) == 45 + assert subject.yaxis2angle(1, -1) == -135 + assert subject.yaxis2angle(-1, -1) == 135 + + +class TestAngle2YAxis(test.bootstrap.IFC4): + def test_run(self): + a = 0.707106781186 + assert np.allclose(subject.angle2yaxis(0), (0, 1)) + assert np.allclose(subject.angle2yaxis(-45), (a, a)) + assert np.allclose(subject.angle2yaxis(45), (-a, a)) + assert np.allclose(subject.angle2yaxis(-135), (a, -a)) + assert np.allclose(subject.angle2yaxis(135), (-a, -a))