mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
258 lines
9.7 KiB
Python
258 lines
9.7 KiB
Python
"""
|
||
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()
|