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!
This commit is contained in:
Dion Moult
2024-06-23 22:22:25 +10:00
parent 9f31815d9e
commit 03ee826e67
2 changed files with 70 additions and 0 deletions
@@ -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)
@@ -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))