mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-16 13:46:54 +00:00
added tests for shape builder transition length calculator
This commit is contained in:
@@ -928,131 +928,7 @@ class ShapeBuilder:
|
|||||||
start_offset = V(0, 0, start_length)
|
start_offset = V(0, 0, start_length)
|
||||||
end_extrusion_offset = start_offset.copy()
|
end_extrusion_offset = start_offset.copy()
|
||||||
|
|
||||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
transition_length = self.mep_transition_length(start_half_dim, end_half_dim, angle, profile_offset)
|
||||||
|
|
||||||
# TODO: move to separate shape_builder method
|
|
||||||
# so we could check transition length without creating representation
|
|
||||||
def get_transition_length(start_half_dim, end_half_dim, angle, profile_offset=None, verbose=True):
|
|
||||||
"""get the final transition length for two profiles dimensions, angle and XY offset between them,
|
|
||||||
|
|
||||||
the difference from `calculate_transition` - `get_transition_length` is making sure
|
|
||||||
that length will fit both sides of the transition
|
|
||||||
"""
|
|
||||||
print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None
|
|
||||||
|
|
||||||
# offsets tend to have bunch of float point garbage
|
|
||||||
# that can result in errors when we're calculating value for square root below
|
|
||||||
offset = V(0, 0) if profile_offset is None else round_vector_to_precision(profile_offset, si_conversion)
|
|
||||||
diff = start_half_dim.xy - end_half_dim.xy
|
|
||||||
diff = Vector([abs(i) for i in diff])
|
|
||||||
|
|
||||||
# TODO: move to separate shape_builder method
|
|
||||||
# so it could be tested later separately
|
|
||||||
def calculate_transition(
|
|
||||||
start_half_dim, end_half_dim, diff, offset, end_profile=False, angle=None, length=None
|
|
||||||
):
|
|
||||||
"""will return transition length based on the profile dimension differences and offset.
|
|
||||||
|
|
||||||
If `length` is provided will return transition angle"""
|
|
||||||
|
|
||||||
if end_profile:
|
|
||||||
diff, offset = diff.yx, offset.yx
|
|
||||||
|
|
||||||
same_dimensions = is_x(diff.length, 0)
|
|
||||||
|
|
||||||
a = diff.x + offset.x
|
|
||||||
b = diff.x - offset.x
|
|
||||||
if length is None:
|
|
||||||
if not same_dimensions:
|
|
||||||
if diff.x == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
t = tan(radians(angle))
|
|
||||||
h = (a + b + sqrt(a**2 + 4 * a * b * t**2 + 2 * a * b + b**2)) / (2 * t)
|
|
||||||
length = sqrt(h**2 - offset.y**2)
|
|
||||||
|
|
||||||
# TODO: move somewhere to tests?
|
|
||||||
if verbose:
|
|
||||||
A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0)
|
|
||||||
end_profile_offset = offset.to_3d() + V(0, 0, length)
|
|
||||||
D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0)
|
|
||||||
B, C = -A, -D
|
|
||||||
C += end_profile_offset
|
|
||||||
D += end_profile_offset
|
|
||||||
tested_angle = degrees((A - D).angle(B - C))
|
|
||||||
print(f"II. length = {length}, requested angle = {angle}, tested angle = {tested_angle}")
|
|
||||||
else:
|
|
||||||
if is_x(offset.x, 0):
|
|
||||||
angle = 90 # NOTE: for now we just hardcode the good value for that case
|
|
||||||
h = start_half_dim.x / tan(radians(angle / 2))
|
|
||||||
length = sqrt(h**2 - offset.y**2)
|
|
||||||
|
|
||||||
if verbose: # TODO: move to tests
|
|
||||||
O = V(0, 0, 0)
|
|
||||||
A = V(-start_half_dim.x, 0, length) + offset.to_3d()
|
|
||||||
B = A * V(-1, 1, 1)
|
|
||||||
tested_angle = degrees((A - O).angle(B - O))
|
|
||||||
print(f"I. length = {length}, requested angle = {angle}, tested angle = {tested_angle}")
|
|
||||||
else:
|
|
||||||
h = offset.x / tan(radians(angle))
|
|
||||||
length = sqrt(h**2 - offset.y**2)
|
|
||||||
|
|
||||||
if verbose: # TODO: move to tests
|
|
||||||
A = V(-start_half_dim.x, 0, 0)
|
|
||||||
H = A + V(0, 0, length)
|
|
||||||
D = H + offset.to_3d()
|
|
||||||
tested_angle = degrees((H - A).angle(D - A))
|
|
||||||
print(
|
|
||||||
f"III. length = {length}, requested angle = {angle}, tested angle = {tested_angle}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return length
|
|
||||||
|
|
||||||
elif angle is None:
|
|
||||||
# TODO: write some tests here too
|
|
||||||
if not same_dimensions:
|
|
||||||
if length == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
h = sqrt(length**2 + offset.y**2)
|
|
||||||
t = -h * (a + b) / (a * b - h**2)
|
|
||||||
angle = degrees(atan(t))
|
|
||||||
else:
|
|
||||||
h = sqrt(length**2 + offset.y**2)
|
|
||||||
if is_x(offset.x, 0):
|
|
||||||
angle = degrees(2 * atan(start_half_dim.x / h))
|
|
||||||
else:
|
|
||||||
angle = degrees(atan(offset.x / length))
|
|
||||||
return angle
|
|
||||||
|
|
||||||
print(f"offset = {profile_offset} / {offset}")
|
|
||||||
print(f"diff = {diff}")
|
|
||||||
|
|
||||||
calculation_arguments = (start_half_dim, end_half_dim, diff, offset)
|
|
||||||
|
|
||||||
def check_transition(end_profile=False):
|
|
||||||
length = calculate_transition(*calculation_arguments, angle=angle, end_profile=end_profile)
|
|
||||||
other_side_angle = calculate_transition(
|
|
||||||
*calculation_arguments, length=length, end_profile=not end_profile
|
|
||||||
)
|
|
||||||
|
|
||||||
# NOTE: for now we just hardcode the good value for that case
|
|
||||||
same_dimensions = is_x(diff.length, 0)
|
|
||||||
if same_dimensions and is_x(offset.y if not end_profile else offset.x, 0):
|
|
||||||
requested_angle = 90
|
|
||||||
else:
|
|
||||||
requested_angle = angle
|
|
||||||
|
|
||||||
print(f"other_side_angle = {other_side_angle}, requested_angle = {requested_angle}")
|
|
||||||
# need to make sure that the worst angle (maximum angle)
|
|
||||||
# for this transition angle is `requested_angle`
|
|
||||||
if other_side_angle < requested_angle or is_x(other_side_angle, requested_angle):
|
|
||||||
print(f"final length = {length}, angle = {requested_angle}, other side angle = {other_side_angle}")
|
|
||||||
return length
|
|
||||||
|
|
||||||
return check_transition() or check_transition(True)
|
|
||||||
|
|
||||||
transition_length = get_transition_length(start_half_dim, end_half_dim, angle, profile_offset)
|
|
||||||
if transition_length is None:
|
if transition_length is None:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
@@ -1199,3 +1075,134 @@ class ShapeBuilder:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return representation, transition_data
|
return representation, transition_data
|
||||||
|
|
||||||
|
# TODO: move to separate shape_builder method
|
||||||
|
# so we could check transition length without creating representation
|
||||||
|
def mep_transition_length(self, start_half_dim, end_half_dim, angle, profile_offset=None, verbose=True):
|
||||||
|
"""get the final transition length for two profiles dimensions, angle and XY offset between them,
|
||||||
|
|
||||||
|
the difference from `calculate_transition` - `get_transition_length` is making sure
|
||||||
|
that length will fit both sides of the transition
|
||||||
|
"""
|
||||||
|
print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None
|
||||||
|
|
||||||
|
# offsets tend to have bunch of float point garbage
|
||||||
|
# that can result in errors when we're calculating value for square root below
|
||||||
|
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||||
|
offset = V(0, 0) if profile_offset is None else round_vector_to_precision(profile_offset, si_conversion)
|
||||||
|
diff = start_half_dim.xy - end_half_dim.xy
|
||||||
|
diff = Vector([abs(i) for i in diff])
|
||||||
|
|
||||||
|
print(f"offset = {profile_offset} / {offset}")
|
||||||
|
print(f"diff = {diff}")
|
||||||
|
|
||||||
|
calculation_arguments = {
|
||||||
|
"start_half_dim": start_half_dim,
|
||||||
|
"end_half_dim": end_half_dim,
|
||||||
|
"diff": diff,
|
||||||
|
"offset": offset,
|
||||||
|
"verbose": verbose,
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_transition(end_profile=False):
|
||||||
|
length = self.mep_transition_calculate(**calculation_arguments, angle=angle, end_profile=end_profile)
|
||||||
|
other_side_angle = self.mep_transition_calculate(
|
||||||
|
**calculation_arguments, length=length, end_profile=not end_profile
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: for now we just hardcode the good value for that case
|
||||||
|
same_dimensions = is_x(diff.length, 0)
|
||||||
|
if same_dimensions and is_x(offset.y if not end_profile else offset.x, 0):
|
||||||
|
requested_angle = 90
|
||||||
|
else:
|
||||||
|
requested_angle = angle
|
||||||
|
|
||||||
|
print(f"other_side_angle = {other_side_angle}, requested_angle = {requested_angle}")
|
||||||
|
# need to make sure that the worst angle (maximum angle)
|
||||||
|
# for this transition angle is `requested_angle`
|
||||||
|
if other_side_angle < requested_angle or is_x(other_side_angle, requested_angle):
|
||||||
|
print(f"final length = {length}, angle = {requested_angle}, other side angle = {other_side_angle}")
|
||||||
|
return length
|
||||||
|
|
||||||
|
return check_transition() or check_transition(True)
|
||||||
|
|
||||||
|
def mep_transition_calculate(
|
||||||
|
self, start_half_dim, end_half_dim, offset, diff=None, end_profile=False, angle=None, length=None, verbose=True
|
||||||
|
):
|
||||||
|
"""will return transition length based on the profile dimension differences and offset.
|
||||||
|
|
||||||
|
If `length` is provided will return transition angle"""
|
||||||
|
|
||||||
|
print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None
|
||||||
|
|
||||||
|
if diff is None:
|
||||||
|
diff = start_half_dim.xy - end_half_dim.xy
|
||||||
|
diff = Vector([abs(i) for i in diff])
|
||||||
|
|
||||||
|
if end_profile:
|
||||||
|
diff, offset = diff.yx, offset.yx
|
||||||
|
|
||||||
|
same_dimensions = is_x(diff.length, 0)
|
||||||
|
|
||||||
|
a = diff.x + offset.x
|
||||||
|
b = diff.x - offset.x
|
||||||
|
if length is None:
|
||||||
|
if not same_dimensions:
|
||||||
|
if diff.x == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
t = tan(radians(angle))
|
||||||
|
h = (a + b + sqrt(a**2 + 4 * a * b * t**2 + 2 * a * b + b**2)) / (2 * t)
|
||||||
|
length = sqrt(h**2 - offset.y**2)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0)
|
||||||
|
end_profile_offset = offset.to_3d() + V(0, 0, length)
|
||||||
|
D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0)
|
||||||
|
B, C = -A, -D
|
||||||
|
C += end_profile_offset
|
||||||
|
D += end_profile_offset
|
||||||
|
tested_angle = degrees((A - D).angle(B - C))
|
||||||
|
print(f"A. length = {length}, requested angle = {angle}, tested angle = {tested_angle}")
|
||||||
|
else:
|
||||||
|
if is_x(offset.x, 0):
|
||||||
|
angle = 90 # NOTE: for now we just hardcode the good value for that case
|
||||||
|
h = start_half_dim.x / tan(radians(angle / 2))
|
||||||
|
length = sqrt(h**2 - offset.y**2)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
O = V(0, 0, 0)
|
||||||
|
A = V(-start_half_dim.x, 0, length) + offset.to_3d()
|
||||||
|
B = A * V(-1, 1, 1)
|
||||||
|
tested_angle = degrees((A - O).angle(B - O))
|
||||||
|
print(f"B. length = {length}, requested angle = {angle}, tested angle = {tested_angle}")
|
||||||
|
else:
|
||||||
|
h = offset.x / tan(radians(angle))
|
||||||
|
length = sqrt(h**2 - offset.y**2)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
A = V(-start_half_dim.x, 0, 0)
|
||||||
|
H = A + V(0, 0, length)
|
||||||
|
H.y += offset.y
|
||||||
|
D = H.copy()
|
||||||
|
D.x += offset.x
|
||||||
|
tested_angle = degrees((H - A).angle(D - A))
|
||||||
|
print(f"C. length = {length}, requested angle = {angle}, tested angle = {tested_angle}")
|
||||||
|
|
||||||
|
return length
|
||||||
|
|
||||||
|
elif angle is None:
|
||||||
|
if not same_dimensions:
|
||||||
|
if length == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
h = sqrt(length**2 + offset.y**2)
|
||||||
|
t = -h * (a + b) / (a * b - h**2)
|
||||||
|
angle = degrees(atan(t))
|
||||||
|
else:
|
||||||
|
h = sqrt(length**2 + offset.y**2)
|
||||||
|
if is_x(offset.x, 0):
|
||||||
|
angle = degrees(2 * atan(start_half_dim.x / h))
|
||||||
|
else:
|
||||||
|
angle = degrees(atan(offset.x / h))
|
||||||
|
return angle
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import test.bootstrap
|
||||||
|
import ifcopenshell.api
|
||||||
|
from ifcopenshell.util.shape_builder import ShapeBuilder, V, is_x
|
||||||
|
from math import degrees, radians, tan
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalculateTransitions(test.bootstrap.IFC4):
|
||||||
|
def calculate_and_test(self, params, length):
|
||||||
|
end_profile = params["end_profile"]
|
||||||
|
start_half_dim = params["start_half_dim"]
|
||||||
|
end_half_dim = params["end_half_dim"]
|
||||||
|
offset = params["offset"]
|
||||||
|
offset = offset if not end_profile else offset.yx
|
||||||
|
angle = params["angle"]
|
||||||
|
|
||||||
|
calculated_length = self.builder.mep_transition_calculate(**params)
|
||||||
|
assert is_x(calculated_length, length)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
same_dimensions = is_x((start_half_dim.xy - end_half_dim.xy).length, 0)
|
||||||
|
if not same_dimensions:
|
||||||
|
confirmation_method = "A"
|
||||||
|
else:
|
||||||
|
confirmation_method = "B" if is_x(offset.x, 0) else "C"
|
||||||
|
|
||||||
|
if confirmation_method == "A":
|
||||||
|
A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0)
|
||||||
|
end_profile_offset = offset.to_3d() + V(0, 0, length)
|
||||||
|
D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0)
|
||||||
|
B, C = -A, -D
|
||||||
|
C += end_profile_offset
|
||||||
|
D += end_profile_offset
|
||||||
|
tested_angle = degrees((A - D).angle(B - C))
|
||||||
|
assert is_x(tested_angle, angle)
|
||||||
|
|
||||||
|
elif confirmation_method == "B":
|
||||||
|
O = V(0, 0, 0)
|
||||||
|
A = V(-start_half_dim.x, 0, length) + offset.to_3d()
|
||||||
|
B = A * V(-1, 1, 1)
|
||||||
|
tested_angle = degrees((A - O).angle(B - O))
|
||||||
|
assert is_x(tested_angle, angle)
|
||||||
|
|
||||||
|
elif confirmation_method == "C":
|
||||||
|
A = V(-start_half_dim.x, 0, 0)
|
||||||
|
H = A + V(0, 0, length)
|
||||||
|
H.y += offset.y
|
||||||
|
D = H.copy()
|
||||||
|
D.x += offset.x
|
||||||
|
tested_angle = degrees((H - A).angle(D - A))
|
||||||
|
assert is_x(tested_angle, angle)
|
||||||
|
|
||||||
|
angle = self.builder.mep_transition_calculate(**params | {"angle": None, "length": calculated_length})
|
||||||
|
assert is_x(angle, angle)
|
||||||
|
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user