ifcopenshell-python: move mathutils related tests to Bonsai

Previously tests never ran since workflow targets 3.11 and running tests is gated by Python 3.13.
This commit is contained in:
Andrej730
2026-09-04 11:23:13 +05:00
parent 253448c32a
commit dc69c19aa7
5 changed files with 137 additions and 111 deletions
+129
View File
@@ -0,0 +1,129 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>, @Andrej730
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
"""
These tests actually belong to the `ifcopenshell.util.shape_builder` test suite
(ifcopenshell-python/test/util/test_shape_builder.py) but live here because they need
`mathutils`.
`mathutils` is available from PyPI either as part of `bpy` or `mathutils` package,
but those are not widely used and there were issues in the past with these going out of date.
Testing against them would also require separately building ifcopenshell for targeted Blender's Python version
(usually for tests we use the minimal Python version supported instead).
Therefore we just test it against the real `mathutils` inside Blender.
"""
from math import radians
import numpy as np
from ifcopenshell.util.shape_builder import (
V,
is_x,
np_angle,
np_angle_signed,
np_intersect_line_line,
np_matrix_to_euler,
np_normal,
np_rotation_matrix,
)
from test.bim.bootstrap import NewFile
class TestMathutilsCompatibleMethods(NewFile):
def test_np_rotation_matrix(self):
from mathutils import Matrix, Vector
# 2D.
assert np.allclose(Matrix.Rotation(radians(45), 2), np_rotation_matrix(radians(45), 2))
assert np.allclose(Matrix.Rotation(radians(45), 2, "Z"), np_rotation_matrix(radians(45), 2, "Z"))
# 3D.
assert np.allclose(Matrix.Rotation(radians(45), 3, "X"), np_rotation_matrix(radians(45), 3, "X"))
assert np.allclose(Matrix.Rotation(radians(45), 3, "Y"), np_rotation_matrix(radians(45), 3, "Y"))
assert np.allclose(Matrix.Rotation(radians(45), 3, "Z"), np_rotation_matrix(radians(45), 3, "Z"))
rotation_vector_args = radians(45), 3, Vector((1, 1, 1)).normalized()
assert np.allclose(Matrix.Rotation(*rotation_vector_args), np_rotation_matrix(*rotation_vector_args))
# Size 4.
assert np.allclose(Matrix.Rotation(radians(45), 4, "X"), np_rotation_matrix(radians(45), 4, "X"))
assert np.allclose(Matrix.Rotation(radians(45), 4, "Y"), np_rotation_matrix(radians(45), 4, "Y"))
assert np.allclose(Matrix.Rotation(radians(45), 4, "Z"), np_rotation_matrix(radians(45), 4, "Z"))
rotation_vector_args = radians(45), 4, Vector((1, 1, 1)).normalized()
assert np.allclose(Matrix.Rotation(*rotation_vector_args), np_rotation_matrix(*rotation_vector_args))
def test_np_matrix_to_euler(self):
from mathutils import Euler
# Test 3x3.
rot = Euler((0.5, 0.5, 0.5)).to_matrix()
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
rot = rot.to_4x4()
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
# Ensure support scaled matrices.
rot = Euler((0.5, 0.5, 0.5)).to_matrix()
rot.col[0] *= 2
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
def test_np_angle(self):
from mathutils import Vector
v1, v2 = (1, 0, 0), (0, 1, 0)
angle = np_angle(v1, v2)
assert is_x(angle, Vector(v1).angle(Vector(v2)))
assert is_x(angle, radians(90))
v1, v2 = v1[:2], v2[:2]
angle = np_angle_signed(v1, v2)
assert is_x(angle, Vector(v1).angle_signed(Vector(v2)))
assert is_x(angle, -radians(90))
v1, v2 = (0, 1, 0), (1, 0, 0)
angle = np_angle(v1, v2)
assert is_x(angle, Vector(v1).angle(Vector(v2)))
assert is_x(angle, radians(90))
v1, v2 = v1[:2], v2[:2]
angle = np_angle_signed(v1, v2)
assert is_x(angle, Vector(v1).angle_signed(Vector(v2)))
assert is_x(angle, radians(90))
def test_np_normal(self):
import mathutils.geometry
vectors = (0, 0, 0), (1, 0, 0), (0, 1, 0)
n = mathutils.geometry.normal(vectors)
assert np.allclose(n, np_normal(vectors))
assert np.allclose(n, (0, 0, 1))
vectors = (0, 0, 0), (0, 1, 0), (1, 0, 0)
n = mathutils.geometry.normal(vectors)
assert np.allclose(n, np_normal(vectors))
assert np.allclose(n, (0, 0, -1))
def test_np_intersect_line_line(self):
import mathutils.geometry
p1, p2 = [0, 0, 0], [1, 1, 1]
q1, q2 = [0, 1, 0], [1, 0, 1]
expected = mathutils.geometry.intersect_line_line(tuple(p1), tuple(p2), tuple(q1), tuple(q2))
result = np_intersect_line_line(p1, p2, q1, q2)
assert np.allclose(expected, result)
+2 -6
View File
@@ -71,16 +71,12 @@ build-urls:
.PHONY: test
test:
pytest --import-mode=importlib -p no:pytest-blender test --ignore=test/util/test_shape_builder.py
pytest --import-mode=importlib -p no:pytest-blender test
.PHONY: test-parallel
test-parallel:
@NPROCS=$$(nproc 2>/dev/null || sysctl -n hw.ncpu); \
pytest --import-mode=importlib -p no:pytest-blender -n $$NPROCS test --ignore=test/util/test_shape_builder.py
.PHONY: test-mathutils
test-mathutils:
pytest --import-mode=importlib -p no:pytest-blender test/util/test_shape_builder.py
pytest --import-mode=importlib -p no:pytest-blender -n $$NPROCS test
.PHONY: license
@@ -31,11 +31,10 @@ This runs:
.. code-block:: bash
pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py
pytest -p no:pytest-blender test
The ``pytest-blender`` plugin is disabled because these are IfcOpenShell-Python
tests, not Bonsai Blender tests. The shape builder tests are split into a
separate target because they require Blender's ``mathutils`` package.
tests, not Bonsai Blender tests.
Parallel tests
--------------
@@ -53,12 +52,9 @@ This automatically uses the available CPU count and runs the same tests as
Shape builder tests
-------------------
The shape builder tests require ``mathutils``. Run them separately:
.. code-block:: bash
pip install mathutils
make test-mathutils
Most of ``ifcopenshell.util.shape_builder`` is covered here, but the tests
that check its ``mathutils``-compatible helpers live in the Bonsai test suite instead,
since they need Blender's ``mathutils`` package. See ``src/bonsai/test/tool/test_shape_builder.py``.
Running individual tests
------------------------
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from math import degrees, radians, sqrt
from math import degrees, sqrt
from typing import Any, Union
import numpy as np
@@ -31,11 +31,6 @@ from ifcopenshell.util.shape_builder import (
arc_to_polyline_points,
is_x,
np_angle,
np_angle_signed,
np_intersect_line_line,
np_matrix_to_euler,
np_normal,
np_rotation_matrix,
np_to_3d,
polygonal_face_set_to_faceted_brep,
)
@@ -147,89 +142,6 @@ class TestPolygonalFaceSetToFacetedBrep(test.bootstrap.IFC4):
polygonal_face_set_to_faceted_brep(face_set)
class TestMathutilsCompatibleMethods(test.bootstrap.IFC4):
def test_np_rotation_matrix(self):
from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
# 2D.
assert np.allclose(Matrix.Rotation(radians(45), 2), np_rotation_matrix(radians(45), 2))
assert np.allclose(Matrix.Rotation(radians(45), 2, "Z"), np_rotation_matrix(radians(45), 2, "Z"))
# 3D.
assert np.allclose(Matrix.Rotation(radians(45), 3, "X"), np_rotation_matrix(radians(45), 3, "X"))
assert np.allclose(Matrix.Rotation(radians(45), 3, "Y"), np_rotation_matrix(radians(45), 3, "Y"))
assert np.allclose(Matrix.Rotation(radians(45), 3, "Z"), np_rotation_matrix(radians(45), 3, "Z"))
rotation_vector_args = radians(45), 3, Vector((1, 1, 1)).normalized()
assert np.allclose(Matrix.Rotation(*rotation_vector_args), np_rotation_matrix(*rotation_vector_args))
# Size 4.
assert np.allclose(Matrix.Rotation(radians(45), 4, "X"), np_rotation_matrix(radians(45), 4, "X"))
assert np.allclose(Matrix.Rotation(radians(45), 4, "Y"), np_rotation_matrix(radians(45), 4, "Y"))
assert np.allclose(Matrix.Rotation(radians(45), 4, "Z"), np_rotation_matrix(radians(45), 4, "Z"))
rotation_vector_args = radians(45), 4, Vector((1, 1, 1)).normalized()
assert np.allclose(Matrix.Rotation(*rotation_vector_args), np_rotation_matrix(*rotation_vector_args))
def test_np_matrix_to_euler(self):
from mathutils import Euler # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
# Test 3x3.
rot = Euler((0.5, 0.5, 0.5)).to_matrix()
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
rot = rot.to_4x4()
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
# Ensure support scaled matrices.
rot = Euler((0.5, 0.5, 0.5)).to_matrix()
rot.col[0] *= 2
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
def test_np_angle(self):
from mathutils import Vector # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
v1, v2 = (1, 0, 0), (0, 1, 0)
angle = np_angle(v1, v2)
assert is_x(angle, Vector(v1).angle(Vector(v2)))
assert is_x(angle, radians(90))
v1, v2 = v1[:2], v2[:2]
angle = np_angle_signed(v1, v2)
assert is_x(angle, Vector(v1).angle_signed(Vector(v2)))
assert is_x(angle, -radians(90))
v1, v2 = (0, 1, 0), (1, 0, 0)
angle = np_angle(v1, v2)
assert is_x(angle, Vector(v1).angle(Vector(v2)))
assert is_x(angle, radians(90))
v1, v2 = v1[:2], v2[:2]
angle = np_angle_signed(v1, v2)
assert is_x(angle, Vector(v1).angle_signed(Vector(v2)))
assert is_x(angle, radians(90))
def test_np_normal(self):
import mathutils.geometry # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
vectors = (0, 0, 0), (1, 0, 0), (0, 1, 0)
n = mathutils.geometry.normal(vectors)
assert np.allclose(n, np_normal(vectors))
assert np.allclose(n, (0, 0, 1))
vectors = (0, 0, 0), (0, 1, 0), (1, 0, 0)
n = mathutils.geometry.normal(vectors)
assert np.allclose(n, np_normal(vectors))
assert np.allclose(n, (0, 0, -1))
def test_np_intersect_line_line(self):
import mathutils.geometry # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
p1, p2 = [0, 0, 0], [1, 1, 1]
q1, q2 = [0, 1, 0], [1, 0, 1]
expected = mathutils.geometry.intersect_line_line(tuple(p1), tuple(p2), tuple(q1), tuple(q2))
result = np_intersect_line_line(p1, p2, q1, q2)
assert np.allclose(expected, result)
class TestRectangle(test.bootstrap.IFC4):
def test_get_rectangle_coords(self):
builder = ShapeBuilder(self.file)