2023-08-23 11:32:04 +05:00
|
|
|
# IfcOpenShell - IFC toolkit and geometry engine
|
|
|
|
|
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>, @Andrej730
|
|
|
|
|
#
|
|
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
2025-12-19 18:53:04 +05:00
|
|
|
from math import degrees, radians
|
|
|
|
|
from typing import Any, Union
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
import pytest
|
|
|
|
|
|
2025-09-15 09:52:30 +05:00
|
|
|
import ifcopenshell.geom
|
|
|
|
|
import ifcopenshell.util.shape
|
2023-08-23 11:32:04 +05:00
|
|
|
import test.bootstrap
|
2024-12-18 12:40:11 +05:00
|
|
|
from ifcopenshell.util.shape_builder import (
|
|
|
|
|
ShapeBuilder,
|
2025-12-19 18:53:04 +05:00
|
|
|
V,
|
2024-12-18 12:40:11 +05:00
|
|
|
is_x,
|
|
|
|
|
np_angle,
|
|
|
|
|
np_angle_signed,
|
|
|
|
|
np_intersect_line_line,
|
2025-01-13 13:22:04 +05:00
|
|
|
np_matrix_to_euler,
|
2025-12-19 18:53:04 +05:00
|
|
|
np_normal,
|
|
|
|
|
np_rotation_matrix,
|
|
|
|
|
np_to_3d,
|
2024-12-18 12:40:11 +05:00
|
|
|
)
|
2024-12-16 17:28:54 +05:00
|
|
|
|
|
|
|
|
|
2024-12-18 12:40:11 +05:00
|
|
|
class TestMathutilsCompatibleMethods(test.bootstrap.IFC4):
|
|
|
|
|
def test_np_rotation_matrix(self):
|
2025-06-09 10:59:22 +05:00
|
|
|
from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports]
|
2024-12-17 17:13:56 +05:00
|
|
|
|
2024-12-16 17:28:54 +05:00
|
|
|
# 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))
|
2023-08-23 11:32:04 +05:00
|
|
|
|
2025-01-13 13:22:04 +05:00
|
|
|
def test_np_matrix_to_euler(self):
|
2025-06-09 10:59:22 +05:00
|
|
|
from mathutils import Euler # pyright: ignore[reportMissingImports]
|
2025-01-13 13:22:04 +05:00
|
|
|
|
|
|
|
|
# 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)))
|
|
|
|
|
|
2024-12-18 12:40:11 +05:00
|
|
|
def test_np_angle(self):
|
2025-06-09 10:59:22 +05:00
|
|
|
from mathutils import Vector # pyright: ignore[reportMissingImports]
|
2024-12-18 12:40:11 +05:00
|
|
|
|
|
|
|
|
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):
|
2025-06-09 10:59:22 +05:00
|
|
|
import mathutils.geometry # pyright: ignore[reportMissingImports]
|
2024-12-18 12:40:11 +05:00
|
|
|
|
|
|
|
|
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):
|
2025-06-09 10:59:22 +05:00
|
|
|
import mathutils.geometry # pyright: ignore[reportMissingImports]
|
2024-12-18 12:40:11 +05:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2023-08-23 11:32:04 +05:00
|
|
|
|
2024-12-13 19:07:42 +05:00
|
|
|
class TestRectangle(test.bootstrap.IFC4):
|
|
|
|
|
def test_get_rectangle_coords(self):
|
|
|
|
|
builder = ShapeBuilder(self.file)
|
|
|
|
|
|
|
|
|
|
# 2D.
|
|
|
|
|
coords = builder.get_rectangle_coords((1, 2), (3, 4))
|
|
|
|
|
assert np.allclose(coords, [[3.0, 4.0], [4.0, 4.0], [4.0, 6.0], [3.0, 6.0]])
|
|
|
|
|
|
|
|
|
|
# 3D, XY plane.
|
|
|
|
|
coords = builder.get_rectangle_coords((1, 2, 0), (3, 4, 0))
|
|
|
|
|
assert np.allclose(coords, [[3.0, 4.0, 0.0], [4.0, 4.0, 0.0], [4.0, 6.0, 0.0], [3.0, 6.0, 0.0]])
|
|
|
|
|
|
|
|
|
|
# 3D, XZ plane.
|
|
|
|
|
coords = builder.get_rectangle_coords((1, 0, 2), (3, 0, 4))
|
|
|
|
|
assert np.allclose(coords, [[3.0, 0.0, 4.0], [4.0, 0.0, 4.0], [4.0, 0.0, 6.0], [3.0, 0.0, 6.0]])
|
|
|
|
|
|
|
|
|
|
|
2024-01-25 16:24:43 +05:00
|
|
|
class TestCreatePolyline(test.bootstrap.IFC4):
|
|
|
|
|
def test_simple_polyline(self):
|
|
|
|
|
builder = ShapeBuilder(self.file)
|
|
|
|
|
|
|
|
|
|
# rectangle
|
2024-12-17 17:13:56 +05:00
|
|
|
points = V([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])
|
|
|
|
|
position = (2.0, 0.0)
|
2024-01-25 16:24:43 +05:00
|
|
|
polyline = builder.polyline(points, closed=True, position_offset=position)
|
|
|
|
|
|
2024-12-17 17:13:56 +05:00
|
|
|
points += position
|
|
|
|
|
assert np.allclose(points, polyline.Points.CoordList)
|
2024-01-25 16:24:43 +05:00
|
|
|
# use 1 line index if there are no arcs
|
|
|
|
|
assert len(polyline.Segments) == 1
|
|
|
|
|
segment = polyline.Segments[0]
|
|
|
|
|
assert segment.is_a("IfcLineIndex")
|
|
|
|
|
assert segment.wrappedValue == (1, 2, 3, 4, 1)
|
|
|
|
|
|
|
|
|
|
def test_polyline_with_arc(self):
|
|
|
|
|
builder = ShapeBuilder(self.file)
|
|
|
|
|
|
2024-12-17 17:13:56 +05:00
|
|
|
points = V([(1, 0), (0.707, 0.707), (0, 1), (0, 2)])
|
|
|
|
|
position = (2, 0)
|
2024-01-25 16:24:43 +05:00
|
|
|
arc_points = (1,)
|
2024-12-17 17:13:56 +05:00
|
|
|
# 4=IfcIndexedPolyCurve(# 3,(IfcArcIndex((1,2,3)),IfcLineIndex((3,4,1))),$)
|
2024-01-25 16:24:43 +05:00
|
|
|
polyline = builder.polyline(points, closed=False, position_offset=position, arc_points=arc_points)
|
2024-12-17 17:13:56 +05:00
|
|
|
points += position
|
|
|
|
|
assert np.allclose(points, polyline.Points.CoordList)
|
2024-01-25 16:24:43 +05:00
|
|
|
assert len(polyline.Segments) == 2
|
|
|
|
|
|
|
|
|
|
segment = polyline.Segments[0]
|
|
|
|
|
assert segment.is_a("IfcArcIndex")
|
|
|
|
|
assert segment.wrappedValue == (1, 2, 3)
|
|
|
|
|
|
|
|
|
|
segment = polyline.Segments[1]
|
|
|
|
|
assert segment.is_a("IfcLineIndex")
|
|
|
|
|
assert segment.wrappedValue == (3, 4)
|
|
|
|
|
|
2024-07-12 21:57:23 +10:00
|
|
|
def test_closed_polyline_ending_with_arc(self):
|
|
|
|
|
builder = ShapeBuilder(self.file)
|
|
|
|
|
|
2024-12-17 17:13:56 +05:00
|
|
|
points = V([(0, 0), (1, 0), (0.5, 0.5)])
|
|
|
|
|
position = (2, 0)
|
2024-07-12 21:57:23 +10:00
|
|
|
arc_points = (2,)
|
|
|
|
|
# 4=IfcIndexedPolyCurve(#3,(IfcLineIndex((1,2)),IfcArcIndex((2,3,1))),$)
|
|
|
|
|
polyline = builder.polyline(points, closed=True, position_offset=position, arc_points=arc_points)
|
2024-12-17 17:13:56 +05:00
|
|
|
points += position
|
|
|
|
|
assert np.allclose(points, polyline.Points.CoordList)
|
2024-07-12 21:57:23 +10:00
|
|
|
assert len(polyline.Segments) == 2
|
|
|
|
|
|
|
|
|
|
segment = polyline.Segments[0]
|
|
|
|
|
assert segment.is_a("IfcLineIndex")
|
|
|
|
|
assert segment.wrappedValue == (1, 2)
|
|
|
|
|
|
|
|
|
|
segment = polyline.Segments[1]
|
|
|
|
|
assert segment.is_a("IfcArcIndex")
|
|
|
|
|
assert segment.wrappedValue == (2, 3, 1)
|
|
|
|
|
|
2024-01-25 16:24:43 +05:00
|
|
|
|
2024-12-19 15:45:17 +05:00
|
|
|
class TestMirror(test.bootstrap.IFC4):
|
|
|
|
|
def test_mirror(self):
|
|
|
|
|
builder = ShapeBuilder(self.file)
|
|
|
|
|
rectangle = builder.rectangle(size=(100, 100))
|
|
|
|
|
assert np.allclose(rectangle.Points.CoordList, ((0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)))
|
|
|
|
|
builder.mirror(rectangle, mirror_axes=(1, 0))
|
|
|
|
|
assert np.allclose(rectangle.Points.CoordList, ((0.0, 0.0), (-100.0, 0.0), (-100.0, 100.0), (0.0, 100.0)))
|
|
|
|
|
|
|
|
|
|
|
2025-05-19 17:36:12 +10:00
|
|
|
class TestVertex(test.bootstrap.IFC4):
|
|
|
|
|
def test_run(self):
|
|
|
|
|
builder = ShapeBuilder(self.file)
|
|
|
|
|
vertex = builder.vertex((1, 2, 3))
|
|
|
|
|
assert np.allclose(vertex.VertexGeometry.Coordinates, (1, 2, 3))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestEdge(test.bootstrap.IFC4):
|
|
|
|
|
def test_run(self):
|
|
|
|
|
builder = ShapeBuilder(self.file)
|
|
|
|
|
edge = builder.edge((1, 0, 0), (1, 2, 3))
|
|
|
|
|
assert np.allclose(edge.EdgeStart.VertexGeometry.Coordinates, (1, 0, 0))
|
|
|
|
|
assert np.allclose(edge.EdgeEnd.VertexGeometry.Coordinates, (1, 2, 3))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFace(test.bootstrap.IFC4):
|
|
|
|
|
def test_run(self):
|
|
|
|
|
builder = ShapeBuilder(self.file)
|
|
|
|
|
face = builder.face(((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)))
|
|
|
|
|
assert np.allclose(face.Bounds[0].Bound.Polygon[0], (0, 0, 0))
|
|
|
|
|
assert np.allclose(face.Bounds[0].Bound.Polygon[1], (1, 0, 0))
|
|
|
|
|
assert np.allclose(face.Bounds[0].Bound.Polygon[2], (1, 1, 0))
|
|
|
|
|
assert np.allclose(face.Bounds[0].Bound.Polygon[3], (0, 1, 0))
|
|
|
|
|
|
|
|
|
|
|
2023-08-23 11:32:04 +05:00
|
|
|
class TestCalculateTransitions(test.bootstrap.IFC4):
|
2024-12-17 17:13:56 +05:00
|
|
|
def calculate_and_test(self, params: dict[str, Any], length: Union[float, None]):
|
|
|
|
|
np_X, np_Y = 0, 1
|
|
|
|
|
np_XY = slice(2)
|
|
|
|
|
np_YX = [1, 0]
|
|
|
|
|
|
2023-08-23 11:32:04 +05:00
|
|
|
end_profile = params["end_profile"]
|
2024-12-17 17:13:56 +05:00
|
|
|
start_half_dim: np.ndarray = params["start_half_dim"]
|
|
|
|
|
end_half_dim: np.ndarray = params["end_half_dim"]
|
|
|
|
|
offset: np.ndarray = params["offset"]
|
|
|
|
|
offset = offset if not end_profile else offset[np_YX]
|
2023-08-23 11:32:04 +05:00
|
|
|
angle = params["angle"]
|
|
|
|
|
|
|
|
|
|
calculated_length = self.builder.mep_transition_calculate(**params)
|
2023-08-23 15:06:05 +05:00
|
|
|
if length is None:
|
|
|
|
|
assert calculated_length is None
|
|
|
|
|
return
|
|
|
|
|
|
2024-12-17 17:13:56 +05:00
|
|
|
assert calculated_length is not None and is_x(calculated_length, length)
|
2023-08-23 11:32:04 +05:00
|
|
|
|
|
|
|
|
# angle confirmation methods:
|
|
|
|
|
# A - between two profiles of different dimensions
|
|
|
|
|
# B - between two profiles of same dimensions, no offset by x
|
|
|
|
|
# C - between two profiles of same dimensions, has offset by x
|
2024-12-17 17:13:56 +05:00
|
|
|
diff = np.subtract(start_half_dim[np_XY], end_half_dim[np_XY])
|
|
|
|
|
same_dimension = is_x(diff[np_X] if not end_profile else diff[np_Y], 0)
|
2023-08-23 15:06:05 +05:00
|
|
|
if not same_dimension:
|
2023-08-23 11:32:04 +05:00
|
|
|
confirmation_method = "A"
|
|
|
|
|
else:
|
2024-12-17 17:13:56 +05:00
|
|
|
confirmation_method = "B" if is_x(offset[np_X], 0) else "C"
|
2023-08-23 11:32:04 +05:00
|
|
|
|
|
|
|
|
if confirmation_method == "A":
|
2024-12-17 17:13:56 +05:00
|
|
|
A = (end_half_dim if end_profile else start_half_dim) * (1, 0, 0)
|
|
|
|
|
end_profile_offset = np_to_3d(offset, length)
|
|
|
|
|
D = (start_half_dim if end_profile else end_half_dim) * (1, 0, 0)
|
2023-08-23 11:32:04 +05:00
|
|
|
B, C = -A, -D
|
|
|
|
|
C += end_profile_offset
|
|
|
|
|
D += end_profile_offset
|
2024-12-17 17:13:56 +05:00
|
|
|
tested_angle = degrees(np_angle(A - D, B - C))
|
2023-08-23 11:32:04 +05:00
|
|
|
assert is_x(tested_angle, angle)
|
|
|
|
|
|
|
|
|
|
elif confirmation_method == "B":
|
2024-12-17 17:13:56 +05:00
|
|
|
O = np.zeros(3)
|
|
|
|
|
A = (-start_half_dim[np_X], 0, length) + np_to_3d(offset)
|
|
|
|
|
B = A * (-1, 1, 1)
|
|
|
|
|
tested_angle = degrees(np_angle(A - O, B - O))
|
2023-08-23 11:32:04 +05:00
|
|
|
assert is_x(tested_angle, angle)
|
|
|
|
|
|
|
|
|
|
elif confirmation_method == "C":
|
2024-12-17 17:13:56 +05:00
|
|
|
A = V(-start_half_dim[np_X], 0, 0)
|
|
|
|
|
H = A + (0, 0, length)
|
|
|
|
|
H[np_Y] += offset[np_Y]
|
2023-08-23 11:32:04 +05:00
|
|
|
D = H.copy()
|
2024-12-17 17:13:56 +05:00
|
|
|
D[np_X] += offset[np_X]
|
|
|
|
|
tested_angle = degrees(np_angle(H - A, D - A))
|
2023-08-23 11:32:04 +05:00
|
|
|
assert is_x(tested_angle, angle)
|
|
|
|
|
|
2024-12-17 17:13:56 +05:00
|
|
|
calculated_angle = self.builder.mep_transition_calculate(
|
|
|
|
|
**params | {"angle": None, "length": calculated_length}
|
|
|
|
|
)
|
|
|
|
|
assert calculated_angle is not None
|
|
|
|
|
assert is_x(calculated_angle, angle)
|
2023-08-23 11:32:04 +05:00
|
|
|
|
|
|
|
|
def test_mep_transition_same_dims_no_offset(self):
|
|
|
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
|
params = {
|
|
|
|
|
"start_half_dim": V(100, 50, 0),
|
|
|
|
|
"end_half_dim": V(100, 50, 0),
|
|
|
|
|
"offset": V(0, 0),
|
|
|
|
|
"end_profile": False,
|
|
|
|
|
"angle": 90,
|
|
|
|
|
"verbose": True,
|
|
|
|
|
}
|
|
|
|
|
self.calculate_and_test(params, 100)
|
|
|
|
|
|
|
|
|
|
def test_mep_transition_same_dims_has_x_offset(self):
|
|
|
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
|
params = {
|
|
|
|
|
"start_half_dim": V(100, 50, 0),
|
|
|
|
|
"end_half_dim": V(100, 50, 0),
|
|
|
|
|
"offset": V(50, 50),
|
|
|
|
|
"end_profile": False,
|
|
|
|
|
"angle": 30,
|
|
|
|
|
"verbose": True,
|
|
|
|
|
}
|
|
|
|
|
self.calculate_and_test(params, 70.71068)
|
|
|
|
|
|
|
|
|
|
def test_mep_transition_same_dims_has_y_offset(self):
|
|
|
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
|
params = {
|
|
|
|
|
"start_half_dim": V(100, 50, 0),
|
|
|
|
|
"end_half_dim": V(100, 50, 0),
|
|
|
|
|
"offset": V(0, 50),
|
|
|
|
|
"end_profile": False,
|
|
|
|
|
"angle": 90,
|
|
|
|
|
"verbose": True,
|
|
|
|
|
}
|
|
|
|
|
self.calculate_and_test(params, 86.60254)
|
|
|
|
|
|
|
|
|
|
def test_mep_transition_diff_dims_no_offset(self):
|
|
|
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
|
params = {
|
|
|
|
|
"start_half_dim": V(100, 50, 0),
|
|
|
|
|
"end_half_dim": V(50, 100, 0),
|
|
|
|
|
"offset": V(0, 0),
|
|
|
|
|
"end_profile": False,
|
|
|
|
|
"angle": 30,
|
|
|
|
|
"verbose": True,
|
|
|
|
|
}
|
|
|
|
|
self.calculate_and_test(params, 186.60254)
|
|
|
|
|
|
|
|
|
|
def test_mep_transition_diff_dims_has_x_y_offset(self):
|
|
|
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
|
params = {
|
|
|
|
|
"start_half_dim": V(100, 50, 0),
|
|
|
|
|
"end_half_dim": V(50, 100, 0),
|
|
|
|
|
"offset": V(50, 50),
|
|
|
|
|
"end_profile": False,
|
|
|
|
|
"angle": 30,
|
|
|
|
|
"verbose": True,
|
|
|
|
|
}
|
|
|
|
|
self.calculate_and_test(params, 165.83124)
|
2023-08-23 15:06:05 +05:00
|
|
|
|
|
|
|
|
def test_mep_transition_y_offset_too_big(self):
|
|
|
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
|
|
|
|
|
|
# method A
|
|
|
|
|
params = {
|
|
|
|
|
"start_half_dim": V(100, 50, 0),
|
|
|
|
|
"end_half_dim": V(50, 100, 0),
|
|
|
|
|
# offset.y > h - 190 > 186.6
|
|
|
|
|
"offset": V(0, 190),
|
|
|
|
|
"end_profile": False,
|
|
|
|
|
"angle": 30,
|
|
|
|
|
"verbose": True,
|
|
|
|
|
}
|
|
|
|
|
self.calculate_and_test(params, None)
|
|
|
|
|
|
|
|
|
|
# method B
|
|
|
|
|
params["end_half_dim"] = V(100, 100, 0)
|
|
|
|
|
self.calculate_and_test(params, None)
|
|
|
|
|
|
|
|
|
|
# method C
|
2024-12-17 17:13:56 +05:00
|
|
|
params["offset"][0] = 10.0
|
2023-08-23 15:06:05 +05:00
|
|
|
self.calculate_and_test(params, None)
|
2025-08-16 15:02:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFaceset(test.bootstrap.IFC4):
|
|
|
|
|
@pytest.mark.parametrize("with_inner", [False, True])
|
|
|
|
|
def test_polygonal_face_set_simple_and_with_voids(self, with_inner):
|
|
|
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
|
|
|
|
|
|
v0 = (0.0, 0.0, 0.0)
|
|
|
|
|
v1 = (4.0, 0.0, 0.0)
|
|
|
|
|
v2 = (4.0, 4.0, 0.0)
|
|
|
|
|
v3 = (0.0, 4.0, 0.0)
|
|
|
|
|
|
|
|
|
|
v4 = (1.0, 1.0, 0.0)
|
|
|
|
|
v5 = (3.0, 1.0, 0.0)
|
|
|
|
|
v6 = (3.0, 3.0, 0.0)
|
|
|
|
|
v7 = (1.0, 3.0, 0.0)
|
|
|
|
|
|
|
|
|
|
if with_inner:
|
|
|
|
|
points = [v0, v1, v2, v3, v4, v5, v6, v7]
|
|
|
|
|
|
|
|
|
|
faces = [
|
|
|
|
|
[[0, 1, 2, 3], [4, 5, 6, 7]], # outer loop with inner hole
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
points = [v0, v1, v2, v3]
|
|
|
|
|
|
|
|
|
|
faces = [[0, 1, 2, 3]] # only outer loop
|
|
|
|
|
|
|
|
|
|
result = self.builder.polygonal_face_set(points, faces)
|
|
|
|
|
|
|
|
|
|
assert result.is_a("IfcPolygonalFaceSet")
|
|
|
|
|
assert result.Coordinates.is_a("IfcCartesianPointList3D")
|
|
|
|
|
assert len(result.Faces) == 1
|
|
|
|
|
if with_inner:
|
|
|
|
|
assert result.Faces[0].is_a("IfcIndexedPolygonalFaceWithVoids")
|
|
|
|
|
else:
|
|
|
|
|
assert result.Faces[0].is_a("IfcIndexedPolygonalFace")
|
|
|
|
|
|
|
|
|
|
shp = ifcopenshell.geom.create_shape(ifcopenshell.geom.settings(), result)
|
|
|
|
|
if with_inner:
|
|
|
|
|
assert ifcopenshell.util.shape.get_area(shp) == pytest.approx(12.0)
|
|
|
|
|
else:
|
|
|
|
|
assert ifcopenshell.util.shape.get_area(shp) == pytest.approx(16.0)
|
|
|
|
|
|
|
|
|
|
def test_polygonal_face_set_invalid_face_types(self):
|
|
|
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
|
with pytest.raises(ValueError, match="Expected a sequence of int or sequence of sequence of int"):
|
|
|
|
|
self.builder.polygonal_face_set([], ["123"])
|
|
|
|
|
with pytest.raises(ValueError, match="Expected a sequence of int or sequence of sequence of int"):
|
|
|
|
|
self.builder.polygonal_face_set([], [[1.0, 2.0, 3.0]])
|
|
|
|
|
with pytest.raises(ValueError, match="Expected a sequence of int or sequence of sequence of int"):
|
|
|
|
|
self.builder.polygonal_face_set([], [[[[1, 2], 3], [4, 5, 6]]])
|