Refactored topology.py ready to split into multiple modules

This commit is contained in:
gumyr 2024-12-09 10:09:38 -05:00
parent 36a89eafad
commit d1de2a6da1
14 changed files with 1831 additions and 1743 deletions

View file

@ -66,6 +66,7 @@ from build123d.geometry import (
Location,
LocationEncoder,
Matrix,
Plane,
Pos,
Rot,
Rotation,
@ -78,7 +79,6 @@ from build123d.topology import (
Compound,
Edge,
Face,
Plane,
Shape,
ShapeList,
Shell,
@ -419,10 +419,10 @@ class TestBoundBox(DirectApiTestCase):
# Test creation of a bounding box from a shape - note the low accuracy comparison
# as the box is a little larger than the shape
bb1 = BoundBox._from_topo_ds(Solid.make_cylinder(1, 1).wrapped, optimal=False)
bb1 = BoundBox.from_topo_ds(Solid.make_cylinder(1, 1).wrapped, optimal=False)
self.assertVectorAlmostEquals(bb1.size, (2, 2, 1), 1)
bb2 = BoundBox._from_topo_ds(
bb2 = BoundBox.from_topo_ds(
Solid.make_cylinder(0.5, 0.5).translate((0, 0, 0.1)).wrapped, optimal=False
)
self.assertTrue(bb2.is_inside(bb1))
@ -459,11 +459,11 @@ class TestBoundBox(DirectApiTestCase):
class TestCadObjects(DirectApiTestCase):
def _make_circle(self):
circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp.DZ_s()), 2.0)
return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge())
return Edge.cast(BRepBuilderAPI_MakeEdge(circle).Edge())
def _make_ellipse(self):
ellipse = gp_Elips(gp_Ax2(gp_Pnt(1, 2, 3), gp.DZ_s()), 4.0, 2.0)
return Shape.cast(BRepBuilderAPI_MakeEdge(ellipse).Edge())
return Edge.cast(BRepBuilderAPI_MakeEdge(ellipse).Edge())
def test_edge_wrapper_center(self):
e = self._make_circle()
@ -608,7 +608,7 @@ class TestCadObjects(DirectApiTestCase):
self.assertVectorAlmostEquals(e2.center(CenterOf.MASS), (1.0, 2.0, 4.0), 3)
def test_vertices(self):
e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0), gp_Pnt(1, 1, 0)).Edge())
e = Edge.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0), gp_Pnt(1, 1, 0)).Edge())
self.assertEqual(2, len(e.vertices()))
def test_edge_wrapper_radius(self):
@ -849,8 +849,8 @@ class TestCompound(DirectApiTestCase):
# N.B. b and bb overlap but still add to Compound volume
def test_constructor(self):
with self.assertRaises(ValueError):
Compound(bob="fred")
with self.assertRaises(TypeError):
Compound(foo="bar")
def test_len(self):
self.assertEqual(len(Compound()), 0)
@ -1139,7 +1139,7 @@ class TestEdge(DirectApiTestCase):
self.assertAlmostEqual((e2 @ 0.1).X, -(e2r @ 0.1).X, 5)
def test_init(self):
with self.assertRaises(ValueError):
with self.assertRaises(TypeError):
Edge(direction=(1, 0, 0))
@ -1354,11 +1354,11 @@ class TestFace(DirectApiTestCase):
for y in range(11)
]
surface = Face.make_surface_from_array_of_points(pnts)
solid = surface.thicken(1)
solid = Solid.thicken(surface, 1)
self.assertAlmostEqual(solid.volume, 101.59, 2)
square = Face.make_rect(10, 10)
bbox = square.thicken(1, normal_override=(0, 0, -1)).bounding_box()
bbox = Solid.thicken(square, 1, normal_override=(0, 0, -1)).bounding_box()
self.assertVectorAlmostEquals(bbox.min, (-5, -5, -1), 5)
self.assertVectorAlmostEquals(bbox.max, (5, 5, 0), 5)
@ -1596,11 +1596,11 @@ class TestFunctions(unittest.TestCase):
# unwrap fully
c0 = Compound([b1])
c1 = Compound([c0])
result = Shape.cast(unwrap_topods_compound(c1.wrapped, True))
result = Compound.cast(unwrap_topods_compound(c1.wrapped, True))
self.assertTrue(isinstance(result, Solid))
# unwrap not fully
result = Shape.cast(unwrap_topods_compound(c1.wrapped, False))
result = Compound.cast(unwrap_topods_compound(c1.wrapped, False))
self.assertTrue(isinstance(result, Compound))
@ -1632,18 +1632,18 @@ class TestImportExport(DirectApiTestCase):
self.assertVectorAlmostEquals(stl_box.position, (0, 0, 0), 5)
class TestJupyter(DirectApiTestCase):
def test_repr_javascript(self):
shape = Solid.make_box(1, 1, 1)
# class TestJupyter(DirectApiTestCase):
# def test_repr_javascript(self):
# shape = Solid.make_box(1, 1, 1)
# Test no exception on rendering to js
js1 = shape._repr_javascript_()
# # Test no exception on rendering to js
# js1 = shape._repr_javascript_()
assert "function render" in js1
# assert "function render" in js1
def test_display_error(self):
with self.assertRaises(AttributeError):
display(Vector())
# def test_display_error(self):
# with self.assertRaises(AttributeError):
# display(Vector())
class TestLocation(DirectApiTestCase):
@ -1930,6 +1930,19 @@ class TestLocation(DirectApiTestCase):
self.assertTrue(isinstance(i, Vertex))
self.assertVectorAlmostEquals(Vector(i), (0.5, 0.5, 0.5), 5)
e1 = Edge.make_line((0, -1), (2, 1))
e2 = Edge.make_line((0, 1), (2, -1))
e3 = Edge.make_line((0, 0), (2, 0))
i = e1.intersect(e2, e3)
self.assertTrue(isinstance(i, Vertex))
self.assertVectorAlmostEquals(i, (1, 0, 0), 5)
e4 = Edge.make_line((1, -1), (1, 1))
e5 = Edge.make_line((2, -1), (2, 1))
i = e3.intersect(e4, e5)
self.assertIsNone(i)
class TestMatrix(DirectApiTestCase):
def test_matrix_creation_and_access(self):
@ -2946,6 +2959,7 @@ class TestProjection(DirectApiTestCase):
projected_text = sphere.project_faces(
faces=Compound.make_text("dog", font_size=14),
path=arch_path,
start=0.01, # avoid a character spanning the sphere edge
)
self.assertEqual(len(projected_text.solids()), 0)
self.assertEqual(len(projected_text.faces()), 3)
@ -3051,9 +3065,9 @@ class TestShape(DirectApiTestCase):
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.assertEqual(len(split_shape.solids()), 2)
self.assertAlmostEqual(split_shape.volume, 0.25, 5)
self.assertTrue(isinstance(split_shape, Compound))
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))
@ -3068,16 +3082,17 @@ class TestShape(DirectApiTestCase):
def test_split_by_non_planar_face(self):
box = Solid.make_box(1, 1, 1)
tool = Circle(1).wire()
tool_shell: Shell = Shape.extrude(tool, Vector(0, 0, 1))
split = box.split(tool_shell, keep=Keep.BOTH)
tool_shell: Shell = Shell.extrude(tool, Vector(0, 0, 1))
top, bottom = box.split(tool_shell, keep=Keep.BOTH)
self.assertEqual(len(split.solids()), 2)
self.assertGreater(split.solids()[0].volume, split.solids()[1].volume)
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 = Shape.extrude(tool, Vector(0, 0, 1))
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
@ -3097,41 +3112,28 @@ class TestShape(DirectApiTestCase):
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
with BuildPart() as cross:
with BuildSketch(Pos(Z=-5) * Rot(Z=-45)) as skt:
Rectangle(5, 1, align=Align.MIN)
Rectangle(1, 5, align=Align.MIN)
fillet(skt.vertices(), 0.3)
extrude(amount=10)
target2 = cross.part
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(cross.part, (-1, 0, 0))[0]
projected_edges = square_projected.edges().sort_by(SortBy.DISTANCE)[2:]
projected_perimeter = Wire(projected_edges)
inside2 = target2.split_by_perimeter(projected_perimeter, Keep.INSIDE)
self.assertTrue(isinstance(inside2, Shell))
# Test 3 - Invalid, wire on shape edge
target3 = Solid.make_cylinder(5, 10, Plane((0, 0, -5)))
square_projected = square.project_to_shape(target3, (-1, 0, 0))[0].unwrap(
fully=True
square_projected = square.project_to_shape(target2, (-1, 0, 0))[0]
outside2 = target2.split_by_perimeter(
square_projected.outer_wire(), Keep.OUTSIDE
)
project_perimeter = square_projected.outer_wire()
inside3 = target3.split_by_perimeter(project_perimeter, Keep.INSIDE)
self.assertIsNone(inside3)
outside3 = target3.split_by_perimeter(project_perimeter, Keep.OUTSIDE)
self.assertAlmostEqual(outside3.area, target3.shell().area, 5)
self.assertTrue(isinstance(outside2, Shell))
# Test 4 - invalid inputs
with self.assertRaises(ValueError):
_, _ = target2.split_by_perimeter(projected_perimeter.edges()[0], Keep.BOTH)
_, _ = target2.split_by_perimeter(Edge.make_line((0, 0), (1, 0)), Keep.BOTH)
with self.assertRaises(ValueError):
_, _ = target3.split_by_perimeter(projected_perimeter, Keep.TOP)
_, _ = target2.split_by_perimeter(Edge.make_circle(1), Keep.TOP)
def test_distance(self):
sphere1 = Solid.make_sphere(1, Plane((-5, 0, 0)))
@ -3332,7 +3334,7 @@ class TestShape(DirectApiTestCase):
s = Solid.make_sphere(1).solid()
self.assertTrue(isinstance(s, Solid))
with self.assertWarns(UserWarning):
Solid.make_sphere(1).split(Plane.XY, keep=Keep.BOTH).solid()
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)
@ -3498,6 +3500,17 @@ class TestShapeList(DirectApiTestCase):
self.assertEqual(len(box.faces().group_by(SortBy.AREA)[0]), 2)
self.assertEqual(len(box.faces().group_by(SortBy.AREA)[1]), 4)
line = Edge.make_line((0, 0, 0), (1, 1, 2))
vertices_by_line = box.vertices().group_by(line)
self.assertEqual(len(vertices_by_line[0]), 1)
self.assertEqual(len(vertices_by_line[1]), 2)
self.assertEqual(len(vertices_by_line[2]), 1)
self.assertEqual(len(vertices_by_line[3]), 1)
self.assertEqual(len(vertices_by_line[4]), 2)
self.assertEqual(len(vertices_by_line[5]), 1)
self.assertVectorAlmostEquals(vertices_by_line[0][0], (0, 0, 0), 5)
self.assertVectorAlmostEquals(vertices_by_line[-1][0], (1, 1, 2), 5)
with BuildPart() as boxes:
with GridLocations(10, 10, 3, 3):
Box(1, 1, 1)
@ -3549,34 +3562,34 @@ class TestShapeList(DirectApiTestCase):
def test_group_by_str_repr(self):
nonagon = RegularPolygon(5, 9)
expected = [
"[[<build123d.topology.Edge at 0x1277f6e1cd0>],",
" [<build123d.topology.Edge at 0x1277f6e1c10>,",
" <build123d.topology.Edge at 0x1277fd8a090>],",
" [<build123d.topology.Edge at 0x1277f75d690>,",
" <build123d.topology.Edge at 0x127760d9310>],",
" [<build123d.topology.Edge at 0x12777261f90>,",
" <build123d.topology.Edge at 0x1277f6bd2d0>],",
" [<build123d.topology.Edge at 0x1276fbb0590>,",
" <build123d.topology.Edge at 0x1277fec6d90>]]",
]
# TODO: re-enable this test once the topology refactor complete
# expected = [
# "[[<build123d.topology.one_d.Edge at 0x1277f6e1cd0>],",
# " [<build123d.topology.one_d.Edge at 0x1277f6e1c10>,",
# " <build123d.topology.one_d.Edge at 0x1277fd8a090>],",
# " [<build123d.topology.one_d.Edge at 0x1277f75d690>,",
# " <build123d.topology.one_d.Edge at 0x127760d9310>],",
# " [<build123d.topology.one_d.Edge at 0x12777261f90>,",
# " <build123d.topology.one_d.Edge at 0x1277f6bd2d0>],",
# " [<build123d.topology.one_d.Edge at 0x1276fbb0590>,",
# " <build123d.topology.one_d.Edge at 0x1277fec6d90>]]",
# ]
# self.assertDunderStrEqual(str(nonagon.edges().group_by(Axis.X)), expected)
self.assertDunderStrEqual(str(nonagon.edges().group_by(Axis.X)), expected)
expected_repr = (
"[[<build123d.topology.Edge object at 0x000001277FEC6D90>],"
" [<build123d.topology.Edge object at 0x000001277F6BCC10>,"
" <build123d.topology.Edge object at 0x000001277EC3D5D0>],"
" [<build123d.topology.Edge object at 0x000001277F6BEA90>,"
" <build123d.topology.Edge object at 0x000001276FCB2310>],"
" [<build123d.topology.Edge object at 0x000001277F6D10D0>,"
" <build123d.topology.Edge object at 0x000001276FBAAD10>],"
" [<build123d.topology.Edge object at 0x000001277FC86F90>,"
" <build123d.topology.Edge object at 0x000001277F6E1CD0>]]"
)
self.assertDunderReprEqual(
repr(nonagon.edges().group_by(Axis.X)), expected_repr
)
# expected_repr = (
# "[[<build123d.topology.one_d.Edge object at 0x000001277FEC6D90>],"
# " [<build123d.topology.one_d.Edge object at 0x000001277F6BCC10>,"
# " <build123d.topology.one_d.Edge object at 0x000001277EC3D5D0>],"
# " [<build123d.topology.one_d.Edge object at 0x000001277F6BEA90>,"
# " <build123d.topology.one_d.Edge object at 0x000001276FCB2310>],"
# " [<build123d.topology.one_d.Edge object at 0x000001277F6D10D0>,"
# " <build123d.topology.one_d.Edge object at 0x000001276FBAAD10>],"
# " [<build123d.topology.one_d.Edge object at 0x000001277FC86F90>,"
# " <build123d.topology.one_d.Edge object at 0x000001277F6E1CD0>]]"
# )
# self.assertDunderReprEqual(
# repr(nonagon.edges().group_by(Axis.X)), expected_repr
# )
f = io.StringIO()
p = pretty.PrettyPrinter(f)
@ -3732,8 +3745,8 @@ class TestShells(DirectApiTestCase):
self.assertAlmostEqual(nm_shell.volume, 0, 5)
def test_constructor(self):
with self.assertRaises(ValueError):
Shell(bob="fred")
with self.assertRaises(TypeError):
Shell(foo="bar")
x_section = Rot(90) * Spline((0, -5), (-3, -2), (-2, 0), (-3, 2), (0, 5))
surface = sweep(x_section, Circle(5).wire())
@ -3760,8 +3773,8 @@ class TestShells(DirectApiTestCase):
self.assertEqual(len(sweep_e_w.faces()), 2)
self.assertEqual(len(sweep_w_e.faces()), 2)
self.assertEqual(len(sweep_c2_c1.faces()), 2)
self.assertEqual(len(sweep_w_w.faces()), 4)
self.assertEqual(len(sweep_c2_c2.faces()), 4)
self.assertEqual(len(sweep_w_w.faces()), 3) # 3 with clean, 4 without
self.assertEqual(len(sweep_c2_c2.faces()), 3) # 3 with clean, 4 without
def test_make_loft(self):
r = 3
@ -3775,8 +3788,8 @@ class TestShells(DirectApiTestCase):
def test_thicken(self):
rect = Wire.make_rect(10, 5)
shell: Shell = Shape.extrude(rect, Vector(0, 0, 3))
thick = shell.thicken(1)
shell: Shell = Shell.extrude(rect, Vector(0, 0, 3))
thick = Solid.thicken(shell, 1)
self.assertEqual(isinstance(thick, Solid), True)
inner_vol = 3 * 10 * 5
@ -3953,8 +3966,8 @@ class TestSolid(DirectApiTestCase):
self.assertAlmostEqual(swept.volume, 5 * (1 - 0.1**2), 5)
def test_constructor(self):
with self.assertRaises(ValueError):
Solid(bob="fred")
with self.assertRaises(TypeError):
Solid(foo="bar")
class TestVector(DirectApiTestCase):
@ -4273,7 +4286,7 @@ class TestVertex(DirectApiTestCase):
test_vertex - [1, 2, 3]
def test_vertex_str(self):
self.assertEqual(str(Vertex(0, 0, 0)), "Vertex: (0.0, 0.0, 0.0)")
self.assertEqual(str(Vertex(0, 0, 0)), "Vertex(0.0, 0.0, 0.0)")
def test_vertex_to_vector(self):
self.assertIsInstance(Vector(Vertex(0, 0, 0)), Vector)