mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
689 lines
27 KiB
Python
689 lines
27 KiB
Python
"""
|
|
build123d imports
|
|
|
|
name: test_shape.py
|
|
by: Gumyr
|
|
date: January 22, 2025
|
|
|
|
desc:
|
|
This python module contains tests for the build123d project.
|
|
|
|
license:
|
|
|
|
Copyright 2025 Gumyr
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
"""
|
|
|
|
# Always equal to any other object, to test that __eq__ cooperation is working
|
|
import unittest
|
|
from random import uniform
|
|
from unittest.mock import PropertyMock, patch
|
|
|
|
import numpy as np
|
|
from anytree import PreOrderIter
|
|
from build123d.build_enums import CenterOf, GeomType, Keep
|
|
from build123d.geometry import (
|
|
Axis,
|
|
Color,
|
|
Location,
|
|
Matrix,
|
|
Plane,
|
|
Pos,
|
|
Rotation,
|
|
Vector,
|
|
)
|
|
from build123d.objects_part import Box, Cone, Cylinder, Sphere
|
|
from build123d.objects_sketch import Circle
|
|
from build123d.operations_part import extrude
|
|
from build123d.topology import (
|
|
Compound,
|
|
Edge,
|
|
Face,
|
|
Shape,
|
|
ShapeList,
|
|
Shell,
|
|
Solid,
|
|
Vertex,
|
|
Wire,
|
|
)
|
|
|
|
|
|
class AlwaysEqual:
|
|
def __eq__(self, other):
|
|
return True
|
|
|
|
|
|
class TestShape(unittest.TestCase):
|
|
"""Misc Shape tests"""
|
|
|
|
def test_mirror(self):
|
|
box_bb = Solid.make_box(1, 1, 1).mirror(Plane.XZ).bounding_box()
|
|
self.assertAlmostEqual(box_bb.min.X, 0, 5)
|
|
self.assertAlmostEqual(box_bb.max.X, 1, 5)
|
|
self.assertAlmostEqual(box_bb.min.Y, -1, 5)
|
|
self.assertAlmostEqual(box_bb.max.Y, 0, 5)
|
|
|
|
box_bb = Solid.make_box(1, 1, 1).mirror().bounding_box()
|
|
self.assertAlmostEqual(box_bb.min.Z, -1, 5)
|
|
self.assertAlmostEqual(box_bb.max.Z, 0, 5)
|
|
|
|
def test_compute_mass(self):
|
|
with self.assertRaises(NotImplementedError):
|
|
Shape.compute_mass(Vertex())
|
|
|
|
def test_combined_center(self):
|
|
objs = [Solid.make_box(1, 1, 1, Plane((x, 0, 0))) for x in [-2, 1]]
|
|
self.assertAlmostEqual(
|
|
Shape.combined_center(objs, center_of=CenterOf.MASS),
|
|
(0, 0.5, 0.5),
|
|
5,
|
|
)
|
|
|
|
objs = [Solid.make_sphere(1, Plane((x, 0, 0))) for x in [-2, 1]]
|
|
self.assertAlmostEqual(
|
|
Shape.combined_center(objs, center_of=CenterOf.BOUNDING_BOX),
|
|
(-0.5, 0, 0),
|
|
5,
|
|
)
|
|
with self.assertRaises(ValueError):
|
|
Shape.combined_center(objs, center_of=CenterOf.GEOMETRY)
|
|
|
|
def test_shape_type(self):
|
|
self.assertEqual(Vertex().shape_type, "Vertex")
|
|
|
|
def test_scale(self):
|
|
self.assertAlmostEqual(Solid.make_box(1, 1, 1).scale(2).volume, 2**3, 5)
|
|
|
|
def test_fuse(self):
|
|
box1 = Solid.make_box(1, 1, 1)
|
|
box2 = Solid.make_box(1, 1, 1, Plane((1, 0, 0)))
|
|
combined = box1.fuse(box2, glue=True)
|
|
self.assertTrue(combined.is_valid)
|
|
self.assertAlmostEqual(combined.volume, 2, 5)
|
|
fuzzy = box1.fuse(box2, tol=1e-6)
|
|
self.assertTrue(fuzzy.is_valid)
|
|
self.assertAlmostEqual(fuzzy.volume, 2, 5)
|
|
|
|
def test_faces_intersected_by_axis(self):
|
|
box = Solid.make_box(1, 1, 1, Plane((0, 0, 1)))
|
|
intersected_faces = box.faces_intersected_by_axis(Axis.Z)
|
|
self.assertTrue(box.faces().sort_by(sort_by=Axis.Z)[0] in intersected_faces)
|
|
self.assertTrue(box.faces().sort_by(sort_by=Axis.Z)[-1] in intersected_faces)
|
|
|
|
def test_split(self):
|
|
shape = Box(1, 1, 1) - Pos((0, 0, -0.25)) * Box(1, 0.5, 0.5)
|
|
split_shape = shape.split(Plane.XY, keep=Keep.BOTTOM)
|
|
self.assertTrue(isinstance(split_shape, list))
|
|
self.assertEqual(len(split_shape), 2)
|
|
self.assertAlmostEqual(split_shape[0].volume + split_shape[1].volume, 0.25, 5)
|
|
split_shape = shape.split(Plane.XY, keep=Keep.TOP)
|
|
self.assertEqual(len(split_shape.solids()), 1)
|
|
self.assertTrue(isinstance(split_shape, Solid))
|
|
self.assertAlmostEqual(split_shape.volume, 0.5, 5)
|
|
|
|
s = Solid.make_cone(1, 0.5, 2, Plane.YZ.offset(10))
|
|
tool = Solid.make_sphere(11).rotate(Axis.Z, 90).face()
|
|
s2 = s.split(tool, keep=Keep.TOP)
|
|
self.assertLess(s2.volume, s.volume)
|
|
self.assertGreater(s2.volume, 0.0)
|
|
|
|
def test_split_by_non_planar_face(self):
|
|
box = Solid.make_box(1, 1, 1)
|
|
tool = Circle(1).wire()
|
|
tool_shell: Shell = Shell.extrude(tool, Vector(0, 0, 1))
|
|
top, bottom = box.split(tool_shell, keep=Keep.BOTH)
|
|
|
|
self.assertFalse(top is None)
|
|
self.assertFalse(bottom is None)
|
|
self.assertGreater(top.volume, bottom.volume)
|
|
|
|
def test_split_by_shell(self):
|
|
box = Solid.make_box(5, 5, 1)
|
|
tool = Wire.make_rect(4, 4)
|
|
tool_shell: Shell = Shell.extrude(tool, Vector(0, 0, 1))
|
|
split = box.split(tool_shell, keep=Keep.TOP)
|
|
inner_vol = 2 * 2
|
|
outer_vol = 5 * 5
|
|
self.assertAlmostEqual(split.volume, outer_vol - inner_vol)
|
|
|
|
def test_split_keep_all(self):
|
|
shape = Box(1, 1, 1)
|
|
split_shape = shape.split(Plane.XY, keep=Keep.ALL)
|
|
self.assertTrue(isinstance(split_shape, ShapeList))
|
|
self.assertEqual(len(split_shape), 2)
|
|
|
|
def test_split_edge_by_shell(self):
|
|
edge = Edge.make_line((-5, 0, 0), (5, 0, 0))
|
|
tool = Wire.make_rect(4, 4)
|
|
tool_shell: Shell = Shell.extrude(tool, Vector(0, 0, 1))
|
|
top = edge.split(tool_shell, keep=Keep.TOP)
|
|
self.assertEqual(len(top), 2)
|
|
self.assertAlmostEqual(top[0].length, 3, 5)
|
|
|
|
def test_split_invalid_keep(self):
|
|
with self.assertRaises(ValueError):
|
|
Box(1, 1, 1).split(Plane.XY, keep=Keep.INSIDE)
|
|
with self.assertRaises(ValueError):
|
|
Box(1, 1, 1).split(Plane.XY, keep=Keep.OUTSIDE)
|
|
|
|
def test_split_by_perimeter(self):
|
|
# Test 0 - extract a spherical cap
|
|
target0 = Solid.make_sphere(10).rotate(Axis.Z, 90)
|
|
circle = Plane.YZ.offset(15) * Circle(5).face()
|
|
circle_projected = circle.project_to_shape(target0, (-1, 0, 0))[0]
|
|
circle_outerwire = circle_projected.edge()
|
|
inside0, outside0 = target0.split_by_perimeter(circle_outerwire, Keep.BOTH)
|
|
self.assertLess(inside0.area, outside0.area)
|
|
|
|
# Test 1 - extract ring of a sphere
|
|
ring = Pos(Z=15) * (Circle(5) - Circle(3)).face()
|
|
ring_projected = ring.project_to_shape(target0, (0, 0, -1))[0]
|
|
ring_outerwire = ring_projected.outer_wire()
|
|
inside1, outside1 = target0.split_by_perimeter(ring_outerwire, Keep.BOTH)
|
|
if isinstance(inside1, list):
|
|
inside1 = Compound(inside1)
|
|
if isinstance(outside1, list):
|
|
outside1 = Compound(outside1)
|
|
self.assertLess(inside1.area, outside1.area)
|
|
self.assertEqual(len(outside1.faces()), 2)
|
|
|
|
# Test 2 - extract multiple faces
|
|
target2 = Box(1, 10, 10)
|
|
square = Face.make_rect(3, 3, Plane((12, 0, 0), z_dir=(1, 0, 0)))
|
|
square_projected = square.project_to_shape(target2, (-1, 0, 0))[0]
|
|
outside2 = target2.split_by_perimeter(
|
|
square_projected.outer_wire(), Keep.OUTSIDE
|
|
)
|
|
self.assertTrue(isinstance(outside2, Shell))
|
|
inside2 = target2.split_by_perimeter(square_projected.outer_wire(), Keep.INSIDE)
|
|
self.assertTrue(isinstance(inside2, Face))
|
|
|
|
# Test 4 - invalid inputs
|
|
with self.assertRaises(ValueError):
|
|
_, _ = target2.split_by_perimeter(Edge.make_line((0, 0), (1, 0)), Keep.BOTH)
|
|
|
|
with self.assertRaises(ValueError):
|
|
_, _ = target2.split_by_perimeter(Edge.make_circle(1), Keep.TOP)
|
|
|
|
def test_distance(self):
|
|
sphere1 = Solid.make_sphere(1, Plane((-5, 0, 0)))
|
|
sphere2 = Solid.make_sphere(1, Plane((5, 0, 0)))
|
|
self.assertAlmostEqual(sphere1.distance(sphere2), 8, 5)
|
|
|
|
def test_distances(self):
|
|
sphere1 = Solid.make_sphere(1, Plane((-5, 0, 0)))
|
|
sphere2 = Solid.make_sphere(1, Plane((5, 0, 0)))
|
|
sphere3 = Solid.make_sphere(1, Plane((-5, 0, 5)))
|
|
distances = [8, 3]
|
|
for i, distance in enumerate(sphere1.distances(sphere2, sphere3)):
|
|
self.assertAlmostEqual(distances[i], distance, 5)
|
|
|
|
def test_max_fillet(self):
|
|
test_solids = [Solid.make_box(10, 8, 2), Solid.make_cone(5, 3, 8)]
|
|
max_values = [0.96, 3.84]
|
|
for i, test_object in enumerate(test_solids):
|
|
with self.subTest("solids" + str(i)):
|
|
max = test_object.max_fillet(test_object.edges())
|
|
self.assertAlmostEqual(max, max_values[i], 2)
|
|
with self.assertRaises(RuntimeError):
|
|
test_solids[0].max_fillet(
|
|
test_solids[0].edges(), tolerance=1e-6, max_iterations=1
|
|
)
|
|
with self.assertRaises(ValueError):
|
|
box = Solid.make_box(1, 1, 1)
|
|
box.fillet(0.75, box.edges())
|
|
# invalid_object = box.fillet(0.75, box.edges())
|
|
# invalid_object.max_fillet(invalid_object.edges())
|
|
|
|
@patch.object(Shape, "is_valid", new_callable=PropertyMock, return_value=False)
|
|
def test_max_fillet_invalid_shape_raises_error(self, mock_is_valid):
|
|
box = Solid.make_box(1, 1, 1)
|
|
|
|
# Assert that ValueError is raised
|
|
with self.assertRaises(ValueError) as max_fillet_context:
|
|
max = box.max_fillet(box.edges())
|
|
|
|
# Check the error message
|
|
self.assertEqual(str(max_fillet_context.exception), "Invalid Shape")
|
|
|
|
# Verify is_valid was called
|
|
mock_is_valid.assert_called_once()
|
|
|
|
def test_locate_bb(self):
|
|
bounding_box = Solid.make_cone(1, 2, 1).bounding_box()
|
|
relocated_bounding_box = Plane.XZ.from_local_coords(bounding_box)
|
|
self.assertAlmostEqual(relocated_bounding_box.min.X, -2, 5)
|
|
self.assertAlmostEqual(relocated_bounding_box.max.X, 2, 5)
|
|
self.assertAlmostEqual(relocated_bounding_box.min.Y, 0, 5)
|
|
self.assertAlmostEqual(relocated_bounding_box.max.Y, -1, 5)
|
|
self.assertAlmostEqual(relocated_bounding_box.min.Z, -2, 5)
|
|
self.assertAlmostEqual(relocated_bounding_box.max.Z, 2, 5)
|
|
|
|
def test_is_equal(self):
|
|
box = Solid.make_box(1, 1, 1)
|
|
self.assertTrue(box.is_equal(box))
|
|
|
|
def test_equal(self):
|
|
box = Solid.make_box(1, 1, 1)
|
|
self.assertEqual(box, box)
|
|
self.assertEqual(box, AlwaysEqual())
|
|
|
|
def test_not_equal(self):
|
|
box = Solid.make_box(1, 1, 1)
|
|
diff = Solid.make_box(1, 2, 3)
|
|
self.assertNotEqual(box, diff)
|
|
self.assertNotEqual(box, object())
|
|
|
|
def test_tessellate(self):
|
|
box123 = Solid.make_box(1, 2, 3)
|
|
verts, triangles = box123.tessellate(1e-6)
|
|
self.assertEqual(len(verts), 24)
|
|
self.assertEqual(len(triangles), 12)
|
|
|
|
def test_transformed(self):
|
|
"""Validate that transformed works the same as changing location"""
|
|
rotation = (uniform(0, 360), uniform(0, 360), uniform(0, 360))
|
|
offset = (uniform(0, 50), uniform(0, 50), uniform(0, 50))
|
|
shape = Solid.make_box(1, 1, 1).transformed(rotation, offset)
|
|
predicted_location = Location(offset) * Rotation(*rotation)
|
|
located_shape = Solid.make_box(1, 1, 1).locate(predicted_location)
|
|
intersect = shape.intersect(located_shape)
|
|
volume = sum(s.volume for s in intersect.solids())
|
|
self.assertAlmostEqual(volume, 1, 5)
|
|
|
|
def test_position_and_orientation(self):
|
|
box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30)))
|
|
self.assertAlmostEqual(box.position, (1, 2, 3), 5)
|
|
self.assertAlmostEqual(box.orientation, (10, 20, 30), 5)
|
|
|
|
def test_distance_to_with_closest_points(self):
|
|
s0 = Solid.make_sphere(1).locate(Location((0, 2.1, 0)))
|
|
s1 = Solid.make_sphere(1)
|
|
distance, pnt0, pnt1 = s0.distance_to_with_closest_points(s1)
|
|
self.assertAlmostEqual(distance, 0.1, 5)
|
|
self.assertAlmostEqual(pnt0, (0, 1.1, 0), 5)
|
|
self.assertAlmostEqual(pnt1, (0, 1, 0), 5)
|
|
|
|
def test_closest_points(self):
|
|
c0 = Edge.make_circle(1).locate(Location((0, 2.1, 0)))
|
|
c1 = Edge.make_circle(1)
|
|
closest = c0.closest_points(c1)
|
|
self.assertAlmostEqual(closest[0], c0.position_at(0.75), 5)
|
|
self.assertAlmostEqual(closest[1], c1.position_at(0.25), 5)
|
|
|
|
def test_distance_to(self):
|
|
c0 = Edge.make_circle(1).locate(Location((0, 2.1, 0)))
|
|
c1 = Edge.make_circle(1)
|
|
distance = c0.distance_to(c1)
|
|
self.assertAlmostEqual(distance, 0.1, 5)
|
|
|
|
def test_intersection(self):
|
|
box = Solid.make_box(1, 1, 1)
|
|
intersections = (
|
|
box.intersect(Axis((0.5, 0.5, 4), (0, 0, -1))).vertices().sort_by(Axis.Z)
|
|
)
|
|
self.assertAlmostEqual(Vector(intersections[0]), (0.5, 0.5, 0), 5)
|
|
self.assertAlmostEqual(Vector(intersections[1]), (0.5, 0.5, 1), 5)
|
|
|
|
def test_clean_error(self):
|
|
"""Note that this test is here to alert build123d to changes in bad OCCT clean behavior
|
|
with spheres or hemispheres. The extra edge in a sphere seems to be the cause of this.
|
|
"""
|
|
sphere = Solid.make_sphere(1)
|
|
divider = Solid.make_box(0.1, 3, 3, Plane(origin=(-0.05, -1.5, -1.5)))
|
|
positive_half, negative_half = (s.clean() for s in sphere.cut(divider).solids())
|
|
self.assertGreater(abs(positive_half.volume - negative_half.volume), 0, 1)
|
|
|
|
def test_clean_empty(self):
|
|
obj = Solid()
|
|
self.assertIs(obj, obj.clean())
|
|
|
|
# def test_relocate(self):
|
|
# box = Solid.make_box(10, 10, 10).move(Location((20, -5, -5)))
|
|
# cylinder = Solid.make_cylinder(2, 50).move(Location((0, 0, 0), (0, 90, 0)))
|
|
|
|
# box_with_hole = box.cut(cylinder)
|
|
# box_with_hole.relocate(box.location)
|
|
|
|
# self.assertEqual(box.location, box_with_hole.location)
|
|
|
|
# bbox1 = box.bounding_box()
|
|
# bbox2 = box_with_hole.bounding_box()
|
|
# self.assertAlmostEqual(bbox1.min, bbox2.min, 5)
|
|
# self.assertAlmostEqual(bbox1.max, bbox2.max, 5)
|
|
|
|
def test_project_to_viewport(self):
|
|
# Basic test
|
|
box = Solid.make_box(10, 10, 10)
|
|
visible, hidden = box.project_to_viewport((-20, 20, 20))
|
|
self.assertEqual(len(visible), 9)
|
|
self.assertEqual(len(hidden), 3)
|
|
|
|
# Contour edges
|
|
cyl = Solid.make_cylinder(2, 10)
|
|
visible, hidden = cyl.project_to_viewport((-20, 20, 20))
|
|
# Note that some edges are broken into two
|
|
self.assertEqual(len(visible), 6)
|
|
self.assertEqual(len(hidden), 2)
|
|
|
|
# Hidden contour edges
|
|
hole = box - cyl
|
|
visible, hidden = hole.project_to_viewport((-20, 20, 20))
|
|
self.assertEqual(len(visible), 13)
|
|
self.assertEqual(len(hidden), 6)
|
|
|
|
# Outline edges
|
|
sphere = Solid.make_sphere(5)
|
|
visible, hidden = sphere.project_to_viewport((-20, 20, 20))
|
|
self.assertEqual(len(visible), 1)
|
|
self.assertEqual(len(hidden), 0)
|
|
|
|
def test_vertex(self):
|
|
v = Edge.make_circle(1).vertex()
|
|
self.assertTrue(isinstance(v, Vertex))
|
|
with self.assertWarns(UserWarning):
|
|
Wire.make_rect(1, 1).vertex()
|
|
|
|
def test_edge(self):
|
|
e = Edge.make_circle(1).edge()
|
|
self.assertTrue(isinstance(e, Edge))
|
|
with self.assertWarns(UserWarning):
|
|
Wire.make_rect(1, 1).edge()
|
|
|
|
def test_wire(self):
|
|
w = Wire.make_circle(1).wire()
|
|
self.assertTrue(isinstance(w, Wire))
|
|
with self.assertWarns(UserWarning):
|
|
Solid.make_box(1, 1, 1).wire()
|
|
|
|
def test_compound(self):
|
|
c = Compound.make_text("hello", 10)
|
|
self.assertTrue(isinstance(c, Compound))
|
|
c2 = Compound.make_text("world", 10)
|
|
with self.assertWarns(UserWarning):
|
|
Compound(children=[c, c2]).compound()
|
|
|
|
def test_face(self):
|
|
f = Face.make_rect(1, 1)
|
|
self.assertTrue(isinstance(f, Face))
|
|
with self.assertWarns(UserWarning):
|
|
Solid.make_box(1, 1, 1).face()
|
|
|
|
def test_shell(self):
|
|
s = Solid.make_sphere(1).shell()
|
|
self.assertTrue(isinstance(s, Shell))
|
|
with self.assertWarns(UserWarning):
|
|
extrude(Compound.make_text("two", 10), amount=5).shell()
|
|
|
|
def test_solid(self):
|
|
s = Solid.make_sphere(1).solid()
|
|
self.assertTrue(isinstance(s, Solid))
|
|
with self.assertWarns(UserWarning):
|
|
Compound(Solid.make_sphere(1).split(Plane.XY, keep=Keep.BOTH)).solid()
|
|
|
|
def test_manifold(self):
|
|
self.assertTrue(Solid.make_box(1, 1, 1).is_manifold)
|
|
self.assertTrue(Solid.make_box(1, 1, 1).shell().is_manifold)
|
|
self.assertFalse(
|
|
Solid.make_box(1, 1, 1)
|
|
.shell()
|
|
.cut(Solid.make_box(0.5, 0.5, 0.5))
|
|
.is_manifold
|
|
)
|
|
self.assertTrue(
|
|
Compound(
|
|
children=[Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)]
|
|
).is_manifold
|
|
)
|
|
|
|
def test_inherit_color(self):
|
|
# Create some objects and assign colors to them
|
|
b = Box(1, 1, 1).locate(Pos(2, 2, 0))
|
|
b.color = Color("blue") # Blue
|
|
c = Cylinder(1, 1).locate(Pos(-2, 2, 0))
|
|
c.color = "red"
|
|
a = Compound(children=[b, c])
|
|
a.color = Color(0, 1, 0)
|
|
# Check that assigned colors stay and inheritance works
|
|
np.testing.assert_allclose(tuple(a.color), (0, 1, 0, 1), 1e-5)
|
|
np.testing.assert_allclose(tuple(b.color), (0, 0, 1, 1), 1e-5)
|
|
np.testing.assert_allclose(tuple(c.color), (1, 0, 0, 1), 1e-5)
|
|
|
|
def test_ocp_section(self):
|
|
# Vertex
|
|
verts, edges = Vertex(1, 2, 0)._ocp_section(Vertex(1, 2, 0))
|
|
self.assertEqual(len(verts), 1)
|
|
self.assertEqual(len(edges), 0)
|
|
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
|
|
|
verts, edges = Vertex(1, 2, 0)._ocp_section(Edge.make_line((0, 0), (2, 4)))
|
|
self.assertEqual(len(verts), 1)
|
|
self.assertEqual(len(edges), 0)
|
|
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
|
|
|
verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_rect(5, 5))
|
|
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
|
self.assertListEqual(edges, [])
|
|
|
|
verts, edges = Vertex(1, 2, 0)._ocp_section(Face(Plane.XY))
|
|
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
|
self.assertListEqual(edges, [])
|
|
|
|
cylinder = Face.extrude(Edge.make_circle(5, Plane.XY.offset(-10)), (0, 0, 20))
|
|
cylinder2 = Face.extrude(Edge.make_circle(5, Plane.YZ.offset(-10)), (20, 0, 0))
|
|
pln = Plane.XY
|
|
|
|
v_edge = Edge.make_line((-5, 0, -20), (-5, 0, 20))
|
|
vertices1, edges1 = cylinder._ocp_section(v_edge)
|
|
vertices1 = ShapeList(vertices1).sort_by(Axis.Z)
|
|
self.assertEqual(len(vertices1), 2)
|
|
|
|
self.assertAlmostEqual(Vector(vertices1[0]), (-5, 0, -10), 5)
|
|
self.assertAlmostEqual(Vector(vertices1[1]), (-5, 0, 10), 5)
|
|
self.assertEqual(len(edges1), 1)
|
|
self.assertAlmostEqual(edges1[0].length, 20, 5)
|
|
|
|
vertices2, edges2 = cylinder._ocp_section(Face(pln))
|
|
self.assertEqual(len(vertices2), 1)
|
|
self.assertEqual(len(edges2), 1)
|
|
self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5)
|
|
self.assertEqual(edges2[0].geom_type, GeomType.CIRCLE)
|
|
self.assertAlmostEqual(edges2[0].radius, 5, 5)
|
|
|
|
vertices4, edges4 = cylinder2._ocp_section(cylinder)
|
|
self.assertGreaterEqual(len(vertices4), 0)
|
|
self.assertGreaterEqual(len(edges4), 2)
|
|
self.assertTrue(all(e.geom_type == GeomType.ELLIPSE for e in edges4))
|
|
|
|
cylinder3 = Cylinder(5, 20).solid()
|
|
cylinder4 = Rotation(0, 90, 0) * cylinder3
|
|
|
|
vertices5, edges5 = cylinder3._ocp_section(cylinder4)
|
|
self.assertGreaterEqual(len(vertices5), 0)
|
|
self.assertGreaterEqual(len(edges5), 2)
|
|
self.assertTrue(all(e.geom_type == GeomType.ELLIPSE for e in edges5))
|
|
|
|
def test_copy_attributes_to(self):
|
|
box = Box(1, 1, 1)
|
|
box2 = Box(10, 10, 10)
|
|
box.label = "box"
|
|
box.color = Color("Red")
|
|
box.children = [Box(1, 1, 1), Box(2, 2, 2)]
|
|
box.topo_parent = box2
|
|
|
|
blank = Compound()
|
|
box.copy_attributes_to(blank)
|
|
self.assertEqual(blank.label, "box")
|
|
self.assertTrue(all(c1 == c2 for c1, c2 in zip(blank.color, Color("Red"))))
|
|
self.assertTrue(all(c1 == c2 for c1, c2 in zip(blank.children, box.children)))
|
|
self.assertEqual(blank.topo_parent, box2)
|
|
|
|
def test_empty_shape(self):
|
|
empty = Solid()
|
|
box = Solid.make_box(1, 1, 1)
|
|
with self.assertRaises(ValueError):
|
|
empty.location
|
|
with self.assertRaises(ValueError):
|
|
empty.position
|
|
with self.assertRaises(ValueError):
|
|
empty.orientation
|
|
self.assertFalse(empty.is_manifold)
|
|
with self.assertRaises(ValueError):
|
|
empty.geom_type
|
|
self.assertIs(empty, empty.fix())
|
|
self.assertEqual(hash(empty), 0)
|
|
self.assertFalse(empty.is_same(Solid()))
|
|
self.assertFalse(empty.is_equal(Solid()))
|
|
self.assertTrue(empty.is_valid)
|
|
empty_bbox = empty.bounding_box()
|
|
self.assertEqual(tuple(empty_bbox.size), (0, 0, 0))
|
|
self.assertIs(empty, empty.mirror(Plane.XY))
|
|
self.assertEqual(Shape.compute_mass(empty), 0)
|
|
self.assertEqual(empty.entities("Face"), [])
|
|
self.assertEqual(empty.area, 0)
|
|
self.assertIs(empty, empty.rotate(Axis.Z, 90))
|
|
translate_matrix = [
|
|
[1.0, 0.0, 0.0, 1.0],
|
|
[0.0, 1.0, 0.0, 2.0],
|
|
[0.0, 0.0, 1.0, 3.0],
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
]
|
|
self.assertIs(empty, empty.transform_shape(Matrix(translate_matrix)))
|
|
self.assertIs(empty, empty.transform_geometry(Matrix(translate_matrix)))
|
|
with self.assertRaises(ValueError):
|
|
empty.locate(Location())
|
|
empty_loc = Location()
|
|
empty_loc.wrapped = None
|
|
with self.assertRaises(ValueError):
|
|
box.locate(empty_loc)
|
|
with self.assertRaises(ValueError):
|
|
empty.located(Location())
|
|
with self.assertRaises(ValueError):
|
|
box.located(empty_loc)
|
|
with self.assertRaises(ValueError):
|
|
empty.move(Location())
|
|
with self.assertRaises(ValueError):
|
|
box.move(empty_loc)
|
|
with self.assertRaises(ValueError):
|
|
empty.moved(Location())
|
|
with self.assertRaises(ValueError):
|
|
box.moved(empty_loc)
|
|
# with self.assertRaises(ValueError):
|
|
# empty.relocate(Location())
|
|
# with self.assertRaises(ValueError):
|
|
# box.relocate(empty_loc)
|
|
with self.assertRaises(ValueError):
|
|
empty.distance_to(Vector(1, 1, 1))
|
|
with self.assertRaises(ValueError):
|
|
empty.distance_to_with_closest_points(Vector(1, 1, 1))
|
|
with self.assertRaises(ValueError):
|
|
empty.distance_to(Vector(1, 1, 1))
|
|
with self.assertRaises(AttributeError):
|
|
box.intersect(empty_loc)
|
|
self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], []))
|
|
self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList())
|
|
with self.assertRaises(ValueError):
|
|
empty.split_by_perimeter(Circle(1).wire())
|
|
with self.assertRaises(ValueError):
|
|
empty.distance(Vertex(1, 1, 1))
|
|
with self.assertRaises(ValueError):
|
|
list(empty.distances(Vertex(0, 0, 0), Vertex(1, 1, 1)))
|
|
with self.assertRaises(ValueError):
|
|
list(box.distances(empty, Vertex(1, 1, 1)))
|
|
with self.assertRaises(ValueError):
|
|
empty.mesh(0.001)
|
|
with self.assertRaises(ValueError):
|
|
empty.tessellate(0.001)
|
|
with self.assertRaises(ValueError):
|
|
empty.to_splines()
|
|
empty_axis = Axis((0, 0, 0), (1, 0, 0))
|
|
empty_axis.wrapped = None
|
|
with self.assertRaises(ValueError):
|
|
box.vertices().group_by(empty_axis)
|
|
empty_wire = Wire()
|
|
with self.assertRaises(ValueError):
|
|
box.vertices().group_by(empty_wire)
|
|
with self.assertRaises(ValueError):
|
|
box.vertices().sort_by(empty_axis)
|
|
with self.assertRaises(ValueError):
|
|
box.vertices().sort_by(empty_wire)
|
|
|
|
def test_empty_selectors(self):
|
|
self.assertEqual(Vertex(1, 1, 1).edges(), ShapeList())
|
|
self.assertEqual(Vertex(1, 1, 1).wires(), ShapeList())
|
|
self.assertEqual(Vertex(1, 1, 1).faces(), ShapeList())
|
|
self.assertEqual(Vertex(1, 1, 1).shells(), ShapeList())
|
|
self.assertEqual(Vertex(1, 1, 1).solids(), ShapeList())
|
|
self.assertEqual(Vertex(1, 1, 1).compounds(), ShapeList())
|
|
self.assertIsNone(Vertex(1, 1, 1).edge())
|
|
self.assertIsNone(Vertex(1, 1, 1).wire())
|
|
self.assertIsNone(Vertex(1, 1, 1).face())
|
|
self.assertIsNone(Vertex(1, 1, 1).shell())
|
|
self.assertIsNone(Vertex(1, 1, 1).solid())
|
|
self.assertIsNone(Vertex(1, 1, 1).compound())
|
|
|
|
|
|
class TestGlobalLocation(unittest.TestCase):
|
|
def test_global_location_hierarchy(self):
|
|
# Create a hierarchy: root → child → grandchild
|
|
root = Box(1, 1, 1)
|
|
root.location = Location((10, 0, 0))
|
|
|
|
child = Box(1, 1, 1)
|
|
child.location = Location((0, 20, 0))
|
|
child.parent = root
|
|
|
|
grandchild = Box(1, 1, 1)
|
|
grandchild.location = Location((0, 0, 30))
|
|
grandchild.parent = child
|
|
|
|
# Compute expected global location manually
|
|
expected_location = root.location * child.location * grandchild.location
|
|
|
|
self.assertAlmostEqual(
|
|
grandchild.global_location.position, expected_location.position
|
|
)
|
|
self.assertAlmostEqual(
|
|
grandchild.global_location.orientation, expected_location.orientation
|
|
)
|
|
|
|
def test_global_location_in_assembly(self):
|
|
cone = Cone(2, 1, 3)
|
|
cone.label = "Cone"
|
|
box = Box(1, 2, 3)
|
|
box.label = "Box"
|
|
sphere = Sphere(1)
|
|
sphere.label = "Sphere"
|
|
|
|
assembly1 = Compound(label="Assembly1", children=[cone])
|
|
assembly1.move(Location((3, 3, 3), (90, 0, 0)))
|
|
assembly2 = Compound(label="Assembly2", children=[assembly1, box])
|
|
assembly2.move(Location((2, 4, 6), (0, 0, 90)))
|
|
assembly3 = Compound(label="Assembly3", children=[assembly2, sphere])
|
|
assembly3.move(Location((3, 6, 9)))
|
|
deep_shape: Shape = next(
|
|
iter(PreOrderIter(assembly3, filter_=lambda n: n.label in ("Cone")))
|
|
)
|
|
# print(deep_shape.path)
|
|
self.assertAlmostEqual(
|
|
deep_shape.global_location.position, (2, 13, 18), places=6
|
|
)
|
|
self.assertAlmostEqual(
|
|
deep_shape.global_location.orientation, (0, 90, 90), places=6
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|