mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
shape_builder - np utils
This commit is contained in:
@@ -91,6 +91,10 @@ def np_normalized(v: VectorType) -> np.ndarray:
|
||||
return np.divide(v, np.linalg.norm(v))
|
||||
|
||||
|
||||
def np_lerp(a: VectorType, b: VectorType, t: float) -> np.ndarray:
|
||||
return a + np.subtract(b, a) * t
|
||||
|
||||
|
||||
def np_to_3d(v: VectorType, z: float = 0.0) -> np.ndarray:
|
||||
"""Convert 2D/4D vector to 3D."""
|
||||
l = len(v)
|
||||
@@ -125,6 +129,16 @@ def np_angle(a: VectorType, b: VectorType) -> float:
|
||||
return np.arccos(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
|
||||
|
||||
|
||||
def np_angle_signed(a: VectorType, b: VectorType) -> float:
|
||||
"""Get signed angle between 2D vectors in radians (clockwise is positive).
|
||||
Designed to work similar to `Vector.angle_signed`.
|
||||
"""
|
||||
assert len(a) == 2 and len(b) == 2, "Only 2D vectors are supported."
|
||||
det = a[1] * b[0] - a[0] * b[1]
|
||||
dot = np.dot(a, b)
|
||||
return np.arctan2(det, dot)
|
||||
|
||||
|
||||
def np_rotation_matrix(
|
||||
angle: float, size: int, axis: Optional[Union[Literal["X", "Y", "Z"], VectorType]] = None
|
||||
) -> np.ndarray:
|
||||
@@ -165,6 +179,52 @@ def np_rotation_matrix(
|
||||
return matrix
|
||||
|
||||
|
||||
def np_normal(vectors: SequenceOfVectors) -> np.ndarray:
|
||||
"""Normal of 3D Polygon.
|
||||
|
||||
Designed to work similar to `mathutils.geometry.normal`.
|
||||
"""
|
||||
assert len(vectors) == 3, "3 vectors required"
|
||||
# TODO: can be optimized?
|
||||
verts_np = np.array(vectors[:3])
|
||||
v0, v1, v2 = verts_np[:3]
|
||||
edge1 = v1 - v0
|
||||
edge2 = v2 - v0
|
||||
normal = np.cross(edge1, edge2)
|
||||
norm = np.linalg.norm(normal)
|
||||
return normal / norm
|
||||
|
||||
|
||||
def np_intersect_line_line(
|
||||
v1: VectorType, v2: VectorType, v3: VectorType, v4: VectorType
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Get 2 closest points on each line.
|
||||
First line - (v1, v2). Second line - (v3, v4).
|
||||
|
||||
Designed to work similar to `mathutils.geometry.intersect_line_line`.
|
||||
"""
|
||||
# TODO: could be optimized?
|
||||
d1 = np.subtract(v2, v1)
|
||||
d2 = np.subtract(v4, v3)
|
||||
|
||||
# Cross product of the directions
|
||||
cross_d1_d2 = np.cross(d1, d2)
|
||||
cross_d1_d2_norm: float = np.linalg.norm(cross_d1_d2)
|
||||
|
||||
# Check if the lines are parallel.
|
||||
if is_x(cross_d1_d2_norm, 0):
|
||||
raise ValueError("Lines are parallel and do not intersect uniquely.")
|
||||
|
||||
r = np.subtract(v3, v1)
|
||||
t = np.dot(np.cross(r, d2), cross_d1_d2) / (cross_d1_d2_norm**2)
|
||||
u = np.dot(np.cross(r, d1), cross_d1_d2) / (cross_d1_d2_norm**2)
|
||||
|
||||
# Closest points on each line
|
||||
point_on_line1 = v1 + t * d1
|
||||
point_on_line2 = v3 + u * d2
|
||||
return point_on_line1, point_on_line2
|
||||
|
||||
|
||||
# Note: using ShapeBuilder try not to reuse IFC elements in the process
|
||||
# otherwise you might run into situation where builder.mirror or other operation
|
||||
# is applied twice during one run to the same element
|
||||
|
||||
@@ -20,13 +20,23 @@ import pytest
|
||||
import test.bootstrap
|
||||
import ifcopenshell.api
|
||||
import numpy as np
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, is_x, np_rotation_matrix, np_to_3d, np_angle, V
|
||||
from ifcopenshell.util.shape_builder import (
|
||||
ShapeBuilder,
|
||||
is_x,
|
||||
np_rotation_matrix,
|
||||
np_to_3d,
|
||||
np_angle,
|
||||
V,
|
||||
np_angle_signed,
|
||||
np_normal,
|
||||
np_intersect_line_line,
|
||||
)
|
||||
from math import degrees, radians
|
||||
from typing import Any, Union
|
||||
|
||||
|
||||
class TestNumpyRotationMatrix(test.bootstrap.IFC4):
|
||||
def test_run(self):
|
||||
class TestMathutilsCompatibleMethods(test.bootstrap.IFC4):
|
||||
def test_np_rotation_matrix(self):
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
# 2D.
|
||||
@@ -47,6 +57,51 @@ class TestNumpyRotationMatrix(test.bootstrap.IFC4):
|
||||
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_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)
|
||||
|
||||
|
||||
class TestRectangle(test.bootstrap.IFC4):
|
||||
def test_get_rectangle_coords(self):
|
||||
|
||||
Reference in New Issue
Block a user