build123d/tests/test_direct_api/test_solid.py

258 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
build123d imports
name: test_solid.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.
"""
import math
import unittest
from build123d.build_enums import GeomType, Kind, Until
from build123d.geometry import (
Axis,
BoundBox,
Location,
OrientedBoundBox,
Plane,
Pos,
Vector,
)
from build123d.objects_curve import Spline
from build123d.objects_sketch import Circle, Rectangle
from build123d.topology import Compound, Edge, Face, Shell, Solid, Vertex, Wire
class TestSolid(unittest.TestCase):
def test_make_solid(self):
box_faces = Solid.make_box(1, 1, 1).faces()
box_shell = Shell(box_faces)
box = Solid(box_shell)
self.assertAlmostEqual(box.area, 6, 5)
self.assertAlmostEqual(box.volume, 1, 5)
self.assertTrue(box.is_valid())
def test_extrude(self):
v = Edge.extrude(Vertex(1, 1, 1), (0, 0, 1))
self.assertAlmostEqual(v.length, 1, 5)
e = Face.extrude(Edge.make_line((2, 1), (2, 0)), (0, 0, 1))
self.assertAlmostEqual(e.area, 1, 5)
w = Shell.extrude(
Wire([Edge.make_line((1, 1), (0, 2)), Edge.make_line((1, 1), (1, 0))]),
(0, 0, 1),
)
self.assertAlmostEqual(w.area, 1 + math.sqrt(2), 5)
f = Solid.extrude(Face.make_rect(1, 1), (0, 0, 1))
self.assertAlmostEqual(f.volume, 1, 5)
s = Compound.extrude(
Shell(
Solid.make_box(1, 1, 1)
.locate(Location((-2, 1, 0)))
.faces()
.sort_by(Axis((0, 0, 0), (1, 1, 1)))[-2:]
),
(0.1, 0.1, 0.1),
)
self.assertAlmostEqual(s.volume, 0.2, 5)
with self.assertRaises(ValueError):
Solid.extrude(Solid.make_box(1, 1, 1), (0, 0, 1))
def test_extrude_taper(self):
a = 1
rect = Face.make_rect(a, a)
flipped = -rect
for direction in [Vector(0, 0, 2), Vector(0, 0, -2)]:
for taper in [10, -10]:
offset_amt = -direction.length * math.tan(math.radians(taper))
for face in [rect, flipped]:
with self.subTest(
f"{direction=}, {taper=}, flipped={face==flipped}"
):
taper_solid = Solid.extrude_taper(face, direction, taper)
# V = 1/3 × h × (a² + b² + ab)
h = Vector(direction).length
b = a + 2 * offset_amt
v = h * (a**2 + b**2 + a * b) / 3
self.assertAlmostEqual(taper_solid.volume, v, 5)
bbox = taper_solid.bounding_box()
size = max(1, b) / 2
if direction.Z > 0:
self.assertAlmostEqual(bbox.min, (-size, -size, 0), 1)
self.assertAlmostEqual(bbox.max, (size, size, h), 1)
else:
self.assertAlmostEqual(bbox.min, (-size, -size, -h), 1)
self.assertAlmostEqual(bbox.max, (size, size, 0), 1)
def test_extrude_taper_with_hole(self):
rect_hole = Face.make_rect(1, 1).make_holes([Wire.make_circle(0.25)])
direction = Vector(0, 0, 0.5)
taper = 10
taper_solid = Solid.extrude_taper(rect_hole, direction, taper)
offset_amt = -direction.length * math.tan(math.radians(taper))
hole = taper_solid.edges().filter_by(GeomType.CIRCLE).sort_by(Axis.Z)[-1]
self.assertAlmostEqual(hole.radius, 0.25 - offset_amt, 5)
def test_extrude_taper_with_hole_flipped(self):
rect_hole = Face.make_rect(1, 1).make_holes([Wire.make_circle(0.25)])
direction = Vector(0, 0, 1)
taper = 10
taper_solid_t = Solid.extrude_taper(rect_hole, direction, taper, True)
taper_solid_f = Solid.extrude_taper(rect_hole, direction, taper, False)
hole_t = taper_solid_t.edges().filter_by(GeomType.CIRCLE).sort_by(Axis.Z)[-1]
hole_f = taper_solid_f.edges().filter_by(GeomType.CIRCLE).sort_by(Axis.Z)[-1]
self.assertGreater(hole_t.radius, hole_f.radius)
def test_extrude_taper_oblique(self):
rect = Face.make_rect(2, 1)
rect_hole = rect.make_holes([Wire.make_circle(0.25)])
o_rect_hole = rect_hole.moved(Location((0, 0, 0), (1, 0.1, 0), 77))
taper0 = Solid.extrude_taper(rect_hole, (0, 0, 1), 5)
taper1 = Solid.extrude_taper(o_rect_hole, o_rect_hole.normal_at(), 5)
self.assertAlmostEqual(taper0.volume, taper1.volume, 5)
def test_extrude_linear_with_rotation(self):
# Face
base = Face.make_rect(1, 1)
twist = Solid.extrude_linear_with_rotation(
base, center=(0, 0, 0), normal=(0, 0, 1), angle=45
)
self.assertAlmostEqual(twist.volume, 1, 5)
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
bottom = twist.faces().sort_by(Axis.Z)[0]
self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5)
# Wire
base = Wire.make_rect(1, 1)
twist = Solid.extrude_linear_with_rotation(
base, center=(0, 0, 0), normal=(0, 0, 1), angle=45
)
self.assertAlmostEqual(twist.volume, 1, 5)
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
bottom = twist.faces().sort_by(Axis.Z)[0]
self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5)
def test_make_loft(self):
loft = Solid.make_loft(
[Wire.make_rect(2, 2), Wire.make_circle(1, Plane((0, 0, 1)))]
)
self.assertAlmostEqual(loft.volume, (4 + math.pi) / 2, 1)
with self.assertRaises(ValueError):
Solid.make_loft([Wire.make_rect(1, 1)])
def test_make_loft_with_vertices(self):
loft = Solid.make_loft(
[Vertex(0, 0, -1), Wire.make_rect(1, 1.5), Vertex(0, 0, 1)], True
)
self.assertAlmostEqual(loft.volume, 1, 5)
with self.assertRaises(ValueError):
Solid.make_loft(
[Wire.make_rect(1, 1), Vertex(0, 0, 1), Wire.make_rect(1, 1)]
)
with self.assertRaises(ValueError):
Solid.make_loft([Vertex(0, 0, 1), Vertex(0, 0, 2)])
with self.assertRaises(ValueError):
Solid.make_loft(
[
Vertex(0, 0, 1),
Wire.make_rect(1, 1),
Vertex(0, 0, 2),
Vertex(0, 0, 3),
]
)
def test_extrude_until(self):
square = Face.make_rect(1, 1)
box = Solid.make_box(4, 4, 1, Plane((-2, -2, 3)))
extrusion = Solid.extrude_until(square, box, (0, 0, 1), Until.LAST)
self.assertAlmostEqual(extrusion.volume, 4, 5)
square = Face.make_rect(1, 1)
box = Solid.make_box(4, 4, 1, Plane((-2, -2, -3)))
extrusion = Solid.extrude_until(square, box, (0, 0, 1), Until.PREVIOUS)
self.assertAlmostEqual(extrusion.volume, 2, 5)
def test_sweep(self):
path = Edge.make_spline([(0, 0), (3, 5), (7, -2)])
section = Wire.make_circle(1, Plane(path @ 0, z_dir=path % 0))
area = Face(section).area
swept = Solid.sweep(section, path)
self.assertAlmostEqual(swept.volume, path.length * area, 0)
def test_hollow_sweep(self):
path = Edge.make_line((0, 0, 0), (0, 0, 5))
section = (Rectangle(1, 1) - Rectangle(0.1, 0.1)).faces()[0]
swept = Solid.sweep(section, path)
self.assertAlmostEqual(swept.volume, 5 * (1 - 0.1**2), 5)
def test_sweep_multi(self):
f0 = Face.make_rect(1, 1)
f1 = Pos(X=10) * Circle(1).face()
path = Spline((0, 0), (10, 0), tangents=((0, 0, 1), (0, 0, -1)))
binormal = Edge.make_line((0, 1), (10, 1))
swept = Solid.sweep_multi([f0, f1], path, is_frenet=True, binormal=binormal)
self.assertAlmostEqual(swept.volume, 23.78, 2)
path = Spline((0, 0), (10, 0), tangents=((0, 0, 1), (1, 0, 0)))
swept = Solid.sweep_multi(
[f0, f1], path, is_frenet=True, binormal=Vector(5, 0, 1)
)
self.assertAlmostEqual(swept.volume, 20.75, 2)
def test_constructor(self):
with self.assertRaises(TypeError):
Solid(foo="bar")
def test_offset_3d(self):
with self.assertRaises(ValueError):
Solid.make_box(1, 1, 1).offset_3d(None, 0.1, kind=Kind.TANGENT)
def test_revolve(self):
r = Solid.revolve(
Face.make_rect(1, 1, Plane((10, 0, 0))).wire(), 180, axis=Axis.Y
)
self.assertEqual(len(r.faces()), 6)
def test_from_bounding_box(self):
cyl = Solid.make_cylinder(0.001, 10).locate(Location(Plane.isometric))
cyl2 = Solid.make_cylinder(1, 10).locate(Location(Plane.isometric))
rbb = Solid.from_bounding_box(cyl.bounding_box())
obb = Solid.from_bounding_box(cyl.oriented_bounding_box())
obb2 = Solid.from_bounding_box(cyl2.oriented_bounding_box())
self.assertAlmostEqual(rbb.volume, (10**3) * (3**0.5) / 9, 0)
self.assertTrue(rbb.volume > obb.volume)
self.assertAlmostEqual(obb2.volume, 40, 4)
if __name__ == "__main__":
unittest.main()