build123d/tests/test_build_line.py
2025-07-31 23:18:18 -04:00

816 lines
34 KiB
Python

"""
build123d BuildLine tests
name: build_line_tests.py
by: Gumyr
date: July 27th 2022
desc: Unit tests for the build123d build_line module
license:
Copyright 2022 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 under the License is 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 unittest
from math import sqrt, pi
from build123d import *
def _assertTupleAlmostEquals(self, expected, actual, places, msg=None):
"""Check Tuples"""
for i, j in zip(actual, expected):
self.assertAlmostEqual(i, j, places, msg=msg)
unittest.TestCase.assertTupleAlmostEquals = _assertTupleAlmostEquals
class BuildLineTests(unittest.TestCase):
"""Test the BuildLine Builder derived class"""
def test_basic_functions(self):
"""Test creating a line and returning properties and methods"""
with BuildLine() as test:
l1 = Line((0, 0), (1, 1))
TangentArc((1, 1), (2, 0), tangent=l1 % 1)
self.assertEqual(len(test.vertices()), 3)
self.assertEqual(len(test.edges()), 2)
self.assertEqual(len(test.vertices(Select.LAST)), 2)
self.assertEqual(len(test.edges(Select.LAST)), 1)
self.assertEqual(len(test.edges(Select.ALL)), 2)
def test_canadian_flag(self):
"""Test many of the features by creating a Canadian flag maple leaf"""
with BuildSketch() as leaf:
with BuildLine() as outline:
l1 = Polyline((0.0000, 0.0771), (0.0187, 0.0771), (0.0094, 0.2569))
l2 = Polyline((0.0325, 0.2773), (0.2115, 0.2458), (0.1873, 0.3125))
RadiusArc(l1 @ 1, l2 @ 0, 0.0271)
l3 = Polyline((0.1915, 0.3277), (0.3875, 0.4865), (0.3433, 0.5071))
TangentArc(l2 @ 1, l3 @ 0, tangent=l2 % 1)
l4 = Polyline((0.3362, 0.5235), (0.375, 0.6427), (0.2621, 0.6188))
SagittaArc(l3 @ 1, l4 @ 0, 0.003)
l5 = Polyline((0.2469, 0.6267), (0.225, 0.6781), (0.1369, 0.5835))
ThreePointArc(
l4 @ 1, (l4 @ 1 + l5 @ 0) * 0.5 + Vector(-0.002, -0.002), l5 @ 0
)
l6 = Polyline((0.1138, 0.5954), (0.1562, 0.8146), (0.0881, 0.7752))
Spline(
l5 @ 1, l6 @ 0, tangents=(l5 % 1, l6 % 0), tangent_scalars=(2, 2)
)
l7 = Line((0.0692, 0.7808), (0.0000, 0.9167))
TangentArc(l6 @ 1, l7 @ 0, tangent=l6 % 1)
mirror(outline.edges(), Plane.YZ)
make_face(leaf.pending_edges)
self.assertAlmostEqual(leaf.sketch.area, 0.2741600685288115, 5)
def test_three_d(self):
"""Test 3D lines with a helix"""
with BuildLine() as roller_coaster:
powerup = Spline(
(0, 0, 0),
(50, 0, 50),
(100, 0, 0),
tangents=((1, 0, 0), (1, 0, 0)),
tangent_scalars=(0.5, 2),
)
corner = RadiusArc(powerup @ 1, (100, 60, 0), -30)
screw = Helix(75, 150, 15, center=(75, 40, 15), direction=(-1, 0, 0))
Spline(corner @ 1, screw @ 0, tangents=(corner % 1, screw % 0))
Spline(
screw @ 1,
(-100, 30, 10),
powerup @ 0,
tangents=(screw % 1, powerup % 0),
)
self.assertAlmostEqual(roller_coaster.wires()[0].length, 678.983628932414, 5)
def test_bezier(self):
pts = [(0, 0), (20, 20), (40, 0), (0, -40), (-60, 0), (0, 100), (100, 0)]
wts = [1.0, 1.0, 2.0, 3.0, 4.0, 2.0, 1.0]
with BuildLine() as bz:
b1 = Bezier(*pts, weights=wts)
self.assertAlmostEqual(bz.wires()[0].length, 225.86389406824566, 5)
self.assertTrue(isinstance(b1, Edge))
def test_double_tangent_arc(self):
l1 = Line((10, 0), (30, 20))
l2 = DoubleTangentArc((0, 5), (1, 0), l1)
_, p1, p2 = l1.distance_to_with_closest_points(l2)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertTupleAlmostEquals(
tuple(l1.tangent_at(p1)), tuple(l2.tangent_at(p2)), 5
)
l3 = Line((10, 0), (20, -10))
l4 = DoubleTangentArc((0, 0), (1, 0), l3)
_, p1, p2 = l3.distance_to_with_closest_points(l4)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertTupleAlmostEquals(
tuple(l3.tangent_at(p1)), tuple(l4.tangent_at(p2)), 5
)
with BuildLine() as test:
l5 = Polyline((20, -10), (10, 0), (20, 10))
l6 = DoubleTangentArc((0, 0), (1, 0), l5, keep=Keep.BOTTOM)
_, p1, p2 = l5.distance_to_with_closest_points(l6)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertTupleAlmostEquals(
tuple(l5.tangent_at(p1)), tuple(l6.tangent_at(p2) * -1), 5
)
# l7 = Spline((15, 5), (5, 0), (15, -5), tangents=[(-1, 0), (1, 0)])
# l8 = DoubleTangentArc((0, 0, 0), (1, 0, 0), l7, keep=Keep.BOTH)
# self.assertEqual(len(l8.edges()), 2)
l9 = EllipticalCenterArc((15, 0), 10, 5, start_angle=90, end_angle=270)
# l10 = DoubleTangentArc((0, 0, 0), (1, 0, 0), l9, keep=Keep.BOTH)
# self.assertEqual(len(l10.edges()), 2)
# self.assertTrue(isinstance(l10, Edge))
with self.assertRaises(ValueError):
l10 = DoubleTangentArc((0, 0, 0), (1, 0, 0), l9, keep=Keep.BOTH)
with self.assertRaises(ValueError):
DoubleTangentArc((0, 0, 0), (0, 0, 1), l9)
l11 = Line((10, 0), (20, 0))
with self.assertRaises(RuntimeError):
DoubleTangentArc((0, 0, 0), (1, 0, 0), l11)
def test_elliptical_start_arc(self):
with self.assertRaises(RuntimeError):
with BuildLine():
EllipticalStartArc((1, 0), (0, 0.5), 1, 0.5, 0)
def test_elliptical_center_arc(self):
with BuildLine() as el:
EllipticalCenterArc((0, 0), 10, 5, 0, 180)
bbox = el.line.bounding_box()
self.assertGreaterEqual(bbox.min.X, -10)
self.assertGreaterEqual(bbox.min.Y, 0)
self.assertLessEqual(bbox.max.X, 10)
self.assertLessEqual(bbox.max.Y, 5)
e1 = EllipticalCenterArc((0, 0), 10, 5, 0, 180)
bbox = e1.bounding_box()
self.assertGreaterEqual(bbox.min.X, -10)
self.assertGreaterEqual(bbox.min.Y, 0)
self.assertLessEqual(bbox.max.X, 10)
self.assertLessEqual(bbox.max.Y, 5)
self.assertTrue(isinstance(e1, Edge))
def test_filletpolyline(self):
with BuildLine(Plane.YZ):
p = FilletPolyline(
(0, 0, 0), (0, 10, 2), (0, 10, 10), (5, 20, 10), radius=2
)
self.assertEqual(len(p.edges()), 5)
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
with BuildLine(Plane.YZ):
p = FilletPolyline(
(0, 0, 0), (0, 0, 10), (10, 2, 10), (10, 0, 0), radius=2, close=True
)
self.assertEqual(len(p.edges()), 8)
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 4)
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 4)
self.assertTrue(isinstance(p, Wire))
with self.assertRaises(ValueError):
FilletPolyline((0, 0), radius=0.1)
with self.assertRaises(ValueError):
FilletPolyline((0, 0), (1, 0), (1, 1), radius=-1)
def test_intersecting_line(self):
with BuildLine():
l1 = Line((0, 0), (10, 0))
l2 = IntersectingLine((5, 10), (0, -1), l1)
self.assertAlmostEqual(l2.length, 10, 5)
l3 = Line((0, 0), (10, 10))
l4 = IntersectingLine((0, 10), (1, -1), l3)
self.assertTupleAlmostEquals(l4 @ 1, (5, 5, 0), 5)
self.assertTrue(isinstance(l4, Edge))
with self.assertRaises(ValueError):
IntersectingLine((0, 10), (1, 1), l3)
def test_jern_arc(self):
with BuildLine() as jern:
j1 = JernArc((1, 0), (0, 1), 1, 90)
self.assertTupleAlmostEquals(jern.line @ 1, (0, 1, 0), 5)
self.assertAlmostEqual(j1.radius, 1)
self.assertAlmostEqual(j1.length, pi / 2)
with BuildLine(Plane.XY.offset(1)) as offset_l:
off1 = JernArc((1, 0), (0, 1), 1, 90)
self.assertTupleAlmostEquals(offset_l.line @ 1, (0, 1, 1), 5)
self.assertAlmostEqual(off1.radius, 1)
self.assertAlmostEqual(off1.length, pi / 2)
plane_iso = Plane(origin=(0, 0, 0), x_dir=(1, 1, 0), z_dir=(1, -1, 1))
with BuildLine(plane_iso) as iso_l:
iso1 = JernArc((0, 0), (0, 1), 1, 180)
self.assertTupleAlmostEquals(iso_l.line @ 1, (-sqrt(2), -sqrt(2), 0), 5)
self.assertAlmostEqual(iso1.radius, 1)
self.assertAlmostEqual(iso1.length, pi)
with BuildLine() as full_l:
l1 = JernArc(start=(0, 0, 0), tangent=(1, 0, 0), radius=1, arc_size=360)
l2 = JernArc(start=(0, 0, 0), tangent=(1, 0, 0), radius=1, arc_size=300)
self.assertTrue(l1.is_closed)
self.assertFalse(l2.is_closed)
circle_face = Face(Wire([l1]))
self.assertAlmostEqual(circle_face.area, pi, 5)
self.assertTupleAlmostEquals(circle_face.center(), (0, 1, 0), 5)
self.assertTupleAlmostEquals(l1.vertex(), l2.start, 5)
l1 = JernArc((0, 0), (1, 0), 1, 90)
self.assertTupleAlmostEquals(l1 @ 1, (1, 1, 0), 5)
self.assertTrue(isinstance(l1, Edge))
def test_polar_line(self):
"""Test 2D and 3D polar lines"""
with BuildLine():
a1 = PolarLine((0, 0), sqrt(2), 45)
d1 = PolarLine((0, 0), sqrt(2), direction=(1, 1))
self.assertTupleAlmostEquals(a1 @ 1, (1, 1, 0), 5)
self.assertTupleAlmostEquals(a1 @ 1, d1 @ 1, 5)
self.assertTrue(isinstance(a1, Edge))
self.assertTrue(isinstance(d1, Edge))
with BuildLine():
a2 = PolarLine((0, 0), 1, 30)
d2 = PolarLine((0, 0), 1, direction=(sqrt(3), 1))
self.assertTupleAlmostEquals(a2 @ 1, (sqrt(3) / 2, 0.5, 0), 5)
self.assertTupleAlmostEquals(a2 @ 1, d2 @ 1, 5)
with BuildLine():
a3 = PolarLine((0, 0), 1, 150)
d3 = PolarLine((0, 0), 1, direction=(-sqrt(3), 1))
self.assertTupleAlmostEquals(a3 @ 1, (-sqrt(3) / 2, 0.5, 0), 5)
self.assertTupleAlmostEquals(a3 @ 1, d3 @ 1, 5)
with BuildLine():
a4 = PolarLine((0, 0), 1, angle=30, length_mode=LengthMode.HORIZONTAL)
d4 = PolarLine(
(0, 0), 1, direction=(sqrt(3), 1), length_mode=LengthMode.HORIZONTAL
)
self.assertTupleAlmostEquals(a4 @ 1, (1, 1 / sqrt(3), 0), 5)
self.assertTupleAlmostEquals(a4 @ 1, d4 @ 1, 5)
with BuildLine(Plane.XZ):
a5 = PolarLine((0, 0), 1, angle=30, length_mode=LengthMode.VERTICAL)
d5 = PolarLine(
(0, 0), 1, direction=(sqrt(3), 1), length_mode=LengthMode.VERTICAL
)
self.assertTupleAlmostEquals(a5 @ 1, (sqrt(3), 0, 1), 5)
self.assertTupleAlmostEquals(a5 @ 1, d5 @ 1, 5)
with self.assertRaises(ValueError):
PolarLine((0, 0), 1)
def test_spline(self):
"""Test spline with no tangents"""
with BuildLine() as test:
s1 = Spline((0, 0), (1, 1), (2, 0))
self.assertTupleAlmostEquals(test.edges()[0] @ 1, (2, 0, 0), 5)
self.assertTrue(isinstance(s1, Edge))
def test_radius_arc(self):
"""Test center arc as arc and circle"""
with BuildSketch() as s:
c = Circle(10)
e = c.edges()[0]
r = e.radius
p1, p2 = e @ 0.3, e @ 0.9
with BuildLine() as l:
arc1 = RadiusArc(p1, p2, r)
self.assertAlmostEqual(arc1.length, 2 * r * pi * 0.4, 6)
self.assertAlmostEqual(arc1.bounding_box().max.X, c.bounding_box().max.X)
arc2 = RadiusArc(p1, p2, r, short_sagitta=False)
self.assertAlmostEqual(arc2.length, 2 * r * pi * 0.6, 6)
self.assertAlmostEqual(arc2.bounding_box().min.X, c.bounding_box().min.X)
arc3 = RadiusArc(p1, p2, -r)
self.assertAlmostEqual(arc3.length, 2 * r * pi * 0.4, 6)
self.assertGreater(arc3.bounding_box().min.X, c.bounding_box().min.X)
self.assertLess(arc3.bounding_box().min.X, c.bounding_box().max.X)
arc4 = RadiusArc(p1, p2, -r, short_sagitta=False)
self.assertAlmostEqual(arc4.length, 2 * r * pi * 0.6, 6)
self.assertGreater(arc4.bounding_box().max.X, c.bounding_box().max.X)
self.assertTrue(isinstance(arc1, Edge))
def test_sagitta_arc(self):
l1 = SagittaArc((0, 0), (1, 0), 0.1)
self.assertAlmostEqual((l1 @ 0.5).Y, 0.1, 5)
self.assertTrue(isinstance(l1, Edge))
def test_center_arc(self):
"""Test center arc as arc and circle"""
with BuildLine() as arc:
CenterArc((0, 0), 10, 0, 180)
self.assertTupleAlmostEquals(arc.edges()[0] @ 1, (-10, 0, 0), 5)
with BuildLine() as arc:
CenterArc((0, 0), 10, 0, 360)
self.assertTupleAlmostEquals(arc.edges()[0] @ 0, arc.edges()[0] @ 1, 5)
with BuildLine(Plane.XZ) as arc:
CenterArc((0, 0), 10, 0, 360)
self.assertTrue(Face(arc.wires()[0]).is_coplanar(Plane.XZ))
with BuildLine(Plane.XZ) as arc:
CenterArc((-100, 0), 100, -45, 90)
self.assertTupleAlmostEquals(arc.edges()[0] @ 0.5, (0, 0, 0), 5)
arc = CenterArc((-100, 0), 100, 0, 360)
self.assertTrue(Face(Wire([arc])).is_coplanar(Plane.XY))
self.assertTupleAlmostEquals(arc.bounding_box().max, (0, 100, 0), 5)
self.assertTrue(isinstance(arc, Edge))
def test_polyline(self):
"""Test edge generation and close"""
with BuildLine() as test:
p1 = Polyline((0, 0), (1, 0), (1, 1), (0, 1), close=True)
self.assertAlmostEqual(
(test.edges()[0] @ 0 - test.edges()[-1] @ 1).length, 0, 5
)
self.assertEqual(len(test.edges()), 4)
self.assertAlmostEqual(test.wires()[0].length, 4)
self.assertTrue(isinstance(p1, Wire))
def test_polyline_with_list(self):
"""Test edge generation and close"""
with BuildLine() as test:
Polyline((0, 0), [(1, 0), (1, 1)], (0, 1), close=True)
self.assertAlmostEqual(
(test.edges()[0] @ 0 - test.edges()[-1] @ 1).length, 0, 5
)
self.assertEqual(len(test.edges()), 4)
self.assertAlmostEqual(test.wires()[0].length, 4)
def test_point_arc_tangent_line(self):
"""Test tangent line between point and arc
Considerations:
- Should produce a GeomType.LINE located on and tangent to arc
- Should start on point
- Lines should always have equal length as long as point is same distance
- LEFT lines should always end on end arc left of midline (angle > 0)
- Arc should be GeomType.CIRCLE
- Point and arc must be coplanar
- Cannot make tangent from point inside arc
"""
# Test line properties in algebra mode
point = (0, 0)
separation = 10
end_point = (0, separation)
end_r = 5
end_arc = CenterArc(end_point, end_r, 0, 360)
lines = []
for side in [Side.LEFT, Side.RIGHT]:
l1 = PointArcTangentLine(point, end_arc, side=side)
self.assertEqual(l1.geom_type, GeomType.LINE)
self.assertTupleAlmostEquals(tuple(point), tuple(l1 @ 0), 5)
_, p1, p2 = end_arc.distance_to_with_closest_points(l1 @ 1)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertAlmostEqual(
end_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5
)
lines.append(l1)
self.assertAlmostEqual(lines[0].length, lines[1].length, 5)
# Test in off-axis builder mode at multiple angles and compare to prev result
workplane = Plane.XY.rotated((45, 45, 45))
with BuildLine(workplane):
end_center = workplane.from_local_coords(end_point)
point_arc = CenterArc(end_center, separation, 0, 360)
end_arc = CenterArc(end_center, end_r, 0, 360)
points = [1, 2, 3, 5, 7, 11, 13]
for point in points:
start_point = point_arc @ (point / 16)
mid_vector = end_center - start_point
mid_perp = mid_vector.cross(workplane.z_dir)
for side in [Side.LEFT, Side.RIGHT]:
l2 = PointArcTangentLine(start_point, end_arc, side=side)
self.assertAlmostEqual(lines[0].length, l2.length, 5)
# Check side
coincident_dir = mid_perp.dot(l2 @ 1 - end_center)
if side == Side.LEFT:
self.assertLess(coincident_dir, 0)
elif side == Side.RIGHT:
self.assertGreater(coincident_dir, 0)
# Error Handling
bad_type = Line((0, 0), (0, 10))
with self.assertRaises(ValueError):
PointArcTangentLine(start_point, bad_type)
with self.assertRaises(ValueError):
PointArcTangentLine(start_point, CenterArc((0, 1, 1), end_r, 0, 360))
with self.assertRaises(ValueError):
PointArcTangentLine(start_point, CenterArc((0, 1), end_r, 0, 360))
def test_point_arc_tangent_arc(self):
"""Test tangent arc between point and arc
Considerations:
- Should produce a GeomType.CIRCLE located on and tangent to arc
- Should start on point tangent to direction
- LEFT lines should always end on end arc left of midline (angle > 0)
- Tangent should be GeomType.CIRCLE
- Point and arc must be coplanar
- Cannot make tangent arc from point/direction already tangent with arc
- (Due to minimizer limit) Cannot make tangent with very large radius
"""
# Test line properties in algebra mode
start_point = (0, 0)
direction = (0, 1)
separation = 10
end_point = (0, separation)
end_r = 5
end_arc = CenterArc(end_point, end_r, 0, 360)
lines = []
for side in [Side.LEFT, Side.RIGHT]:
l1 = PointArcTangentArc(start_point, direction, end_arc, side=side)
self.assertEqual(l1.geom_type, GeomType.CIRCLE)
self.assertTupleAlmostEquals(tuple(start_point), tuple(l1 @ 0), 5)
self.assertAlmostEqual(Vector(direction).cross(l1 % 0).length, 0, 5)
_, p1, p2 = end_arc.distance_to_with_closest_points(l1 @ 1)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertAlmostEqual(
end_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5
)
lines.append(l1)
# Test in off-axis builder mode at multiple angles and compare to prev result
workplane = Plane.XY.rotated((45, 45, 45))
with BuildLine(workplane):
end_center = workplane.from_local_coords(end_point)
end_arc = CenterArc(end_center, end_r, 0, 360)
# Assortment of points in different regimes
flip = separation * 2
value = flip - end_r
points = [
start_point,
(end_r - 0.1, 0),
(-end_r - 0.1, 0),
(end_r + 0.1, flip),
(-end_r + 0.1, flip),
(0, flip),
(flip, flip),
(-flip, -flip),
(value, -value),
(-value, value),
]
for point in points:
mid_vector = end_center - point
mid_perp = mid_vector.cross(workplane.z_dir)
centers = {}
for side in [Side.LEFT, Side.RIGHT]:
l2 = PointArcTangentArc(point, direction, end_arc, side=side)
centers[side] = l2.center()
if point == start_point:
self.assertAlmostEqual(lines[0].length, l2.length, 5)
# Rudimentary side check. Somewhat surprised this works
center_dif = centers[Side.RIGHT] - centers[Side.LEFT]
self.assertGreater(mid_perp.dot(center_dif), 0)
# Error Handling
end_arc = CenterArc(end_point, end_r, 0, 360)
# GeomType
bad_type = Line((0, 0), (0, 10))
with self.assertRaises(ValueError):
PointArcTangentArc(start_point, direction, bad_type)
# Coplanar
with self.assertRaises(ValueError):
arc = CenterArc((0, 1, 1), end_r, 0, 360)
PointArcTangentArc(start_point, direction, arc)
# Positional
with self.assertRaises(ValueError):
PointArcTangentArc((end_r, 0), direction, end_arc, side=Side.RIGHT)
with self.assertRaises(RuntimeError):
PointArcTangentArc(
(end_r - 0.00001, 0), direction, end_arc, side=Side.RIGHT
)
def test_arc_arc_tangent_line(self):
"""Test tangent line between arcs
Considerations:
- Should produce a GeomType.LINE located on and tangent to arcs
- INSIDE arcs cross midline of arc centers
- INSIDE lines should always have equal length as long as arcs are same distance
- OUTSIDE lines should always have equal length as long as arcs are same distance
- LEFT lines should always start on start arc left of midline (angle > 0)
- Tangent should be GeomType.CIRCLE
- Arcs must be coplanar
- Cannot make tangent for concentric arcs
- Cannot make INSIDE tangent from overlapping or tangent arcs
"""
# Test line properties in algebra mode
start_r = 2
end_r = 5
separation = 10
start_point = (0, 0)
end_point = (0, separation)
start_arc = CenterArc(start_point, start_r, 0, 360)
end_arc = CenterArc(end_point, end_r, 0, 360)
lines = []
for keep in [Keep.INSIDE, Keep.OUTSIDE]:
for side in [Side.LEFT, Side.RIGHT]:
l1 = ArcArcTangentLine(start_arc, end_arc, side=side, keep=keep)
self.assertEqual(l1.geom_type, GeomType.LINE)
# Check coincidence, tangency with each arc
_, p1, p2 = start_arc.distance_to_with_closest_points(l1 @ 0)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertAlmostEqual(
start_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5
)
_, p1, p2 = end_arc.distance_to_with_closest_points(l1 @ 1)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertAlmostEqual(
end_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5
)
lines.append(l1)
self.assertAlmostEqual(lines[-2].length, lines[-1].length, 5)
# Test in off-axis builder mode at multiple angles and compare to prev result
workplane = Plane.XY.rotated((45, 45, 45))
with BuildLine(workplane):
end_center = workplane.from_local_coords(end_point)
point_arc = CenterArc(end_center, separation, 0, 360)
end_arc = CenterArc(end_center, end_r, 0, 360)
points = [1, 2, 3, 5, 7, 11, 13]
for point in points:
start_center = point_arc @ (point / 16)
start_arc = CenterArc(start_center, start_r, 0, 360)
midline = Line(start_center, end_center)
mid_vector = end_center - start_center
mid_perp = mid_vector.cross(workplane.z_dir)
for keep in [Keep.INSIDE, Keep.OUTSIDE]:
for side in [Side.LEFT, Side.RIGHT]:
l2 = ArcArcTangentLine(start_arc, end_arc, side=side, keep=keep)
# Check length and cross/does not cross midline
d1 = midline.distance_to(l2)
if keep == Keep.INSIDE:
self.assertAlmostEqual(d1, 0, 5)
self.assertAlmostEqual(lines[0].length, l2.length, 5)
elif keep == Keep.OUTSIDE:
self.assertNotAlmostEqual(d1, 0, 5)
self.assertAlmostEqual(lines[2].length, l2.length, 5)
# Check side of midline
_, _, p2 = start_arc.distance_to_with_closest_points(l2)
coincident_dir = mid_perp.dot(p2 - start_center)
if side == Side.LEFT:
self.assertLess(coincident_dir, 0)
elif side == Side.RIGHT:
self.assertGreater(coincident_dir, 0)
## Error Handling
start_arc = CenterArc(start_point, start_r, 0, 360)
end_arc = CenterArc(end_point, end_r, 0, 360)
# GeomType
bad_type = Line((0, 0), (0, 10))
with self.assertRaises(ValueError):
ArcArcTangentLine(start_arc, bad_type)
with self.assertRaises(ValueError):
ArcArcTangentLine(bad_type, end_arc)
# Coplanar
with self.assertRaises(ValueError):
ArcArcTangentLine(CenterArc((0, 0, 1), 5, 0, 360), end_arc)
# Position conditions
with self.assertRaises(ValueError):
ArcArcTangentLine(CenterArc(end_point, start_r, 0, 360), end_arc)
with self.assertRaises(ValueError):
arc = CenterArc(start_point, separation - end_r, 0, 360)
ArcArcTangentLine(arc, end_arc, keep=Keep.INSIDE)
with self.assertRaises(ValueError):
arc = CenterArc(start_point, separation - end_r + 1, 0, 360)
ArcArcTangentLine(arc, end_arc, keep=Keep.INSIDE)
def test_arc_arc_tangent_arc(self):
"""Test tangent arc between arcs
Considerations:
- Should produce a GeomType.CIRCLE located on and tangent to arcs
- Tangent arcs that share a side have arc centers on the same side of the midline
- LEFT arcs have centers to left of midline (for (INSIDE, *) case, non overlapping))
- Mirrored arcs should always have equal length as long as arcs are same distance
- Tangent should be GeomType.CIRCLE
- Arcs must be coplanar
- Cannot make tangent for concentric arcs
"""
# Test line properties in algebra mode
start_r = 2
end_r = 5
separation = 10
start_point = (0, 0)
end_point = (0, separation)
start_arc = CenterArc(start_point, start_r, 0, 360)
end_arc = CenterArc(end_point, end_r, 0, 360)
radius = 15
lines = []
for keep_placement in [Keep.INSIDE, Keep.OUTSIDE]:
keep = (keep_placement, Keep.OUTSIDE)
for side in [Side.LEFT, Side.RIGHT]:
l1 = ArcArcTangentArc(start_arc, end_arc, radius, side=side, keep=keep)
self.assertEqual(l1.geom_type, GeomType.CIRCLE)
self.assertAlmostEqual(l1.radius, radius)
# Check coincidence, tangency with each arc
_, p1, p2 = start_arc.distance_to_with_closest_points(l1)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertAlmostEqual(
start_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5
)
_, p1, p2 = end_arc.distance_to_with_closest_points(l1)
self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertAlmostEqual(
end_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5
)
lines.append(l1)
self.assertAlmostEqual(lines[-2].length, lines[-1].length, 5)
# Test in off-axis builder mode at multiple angles and compare to prev result
workplane = Plane.XY.rotated((45, 45, 45))
with BuildLine(workplane):
end_center = workplane.from_local_coords(end_point)
point_arc = CenterArc(end_center, separation, 0, 360)
end_arc = CenterArc(end_center, end_r, 0, 360)
points = [1, 2, 3, 5, 7, 11, 13]
for point in points:
start_center = point_arc @ (point / 16)
start_arc = CenterArc(point_arc @ (point / 16), start_r, 0, 360)
mid_vector = end_center - start_center
mid_perp = mid_vector.cross(workplane.z_dir)
for keep_placement in [Keep.INSIDE, Keep.OUTSIDE]:
keep = (keep_placement, Keep.OUTSIDE)
for side in [Side.LEFT, Side.RIGHT]:
l2 = ArcArcTangentArc(
start_arc, end_arc, radius, side=side, keep=keep
)
# Check length against algebraic length
if keep_placement == Keep.OUTSIDE:
self.assertAlmostEqual(lines[2].length, l2.length, 5)
side_sign = 1
elif keep_placement == Keep.INSIDE:
self.assertAlmostEqual(lines[0].length, l2.length, 5)
side_sign = -1
# Check side of midline
_, _, p2 = start_arc.distance_to_with_closest_points(l2)
coincident_dir = mid_perp.dot(p2 - start_center)
center_dir = mid_perp.dot(l2.arc_center - start_center)
if side == Side.LEFT:
self.assertLess(side_sign * coincident_dir, 0)
self.assertLess(center_dir, 0)
elif side == Side.RIGHT:
self.assertGreater(side_sign * coincident_dir, 0)
self.assertGreater(center_dir, 0)
# Verify arc is tangent for a reversed start arc
c1 = CenterArc((0, 80), 40, 0, -180)
c2 = CenterArc((80, 0), 40, 90, 180)
keep = (Keep.OUTSIDE, Keep.OUTSIDE)
arc = ArcArcTangentArc(c1, c2, 25, side=Side.RIGHT, keep=keep)
_, _, point = c1.distance_to_with_closest_points(arc)
self.assertAlmostEqual(
c1.tangent_at(point).cross(arc.tangent_at(point)).length, 0, 5
)
## Error Handling
start_arc = CenterArc(start_point, start_r, 0, 360)
end_arc = CenterArc(end_point, end_r, 0, 360)
# GeomType
bad_type = Line((0, 0), (0, 10))
with self.assertRaises(ValueError):
ArcArcTangentArc(start_arc, bad_type, radius)
with self.assertRaises(ValueError):
ArcArcTangentArc(bad_type, end_arc, radius)
# Keep.BOTH
with self.assertRaises(ValueError):
ArcArcTangentArc(bad_type, end_arc, radius, keep=(Keep.BOTH, Keep.OUTSIDE))
# Coplanar
with self.assertRaises(ValueError):
ArcArcTangentArc(CenterArc((0, 0, 1), 5, 0, 360), end_arc, radius)
# Coincidence (already tangent)
with self.assertRaises(ValueError):
ArcArcTangentArc(start_arc, CenterArc((0, 2 * start_r), start_r, 0, 360), 3)
with self.assertRaises(ValueError):
ArcArcTangentArc(start_arc, CenterArc(start_point, start_r, 0, 360), 3)
with self.assertRaises(ValueError):
ArcArcTangentArc(
start_arc, CenterArc((0, end_r - start_r), end_r, 0, 360), 3
)
# Radius size
with self.assertRaises(ValueError):
r = (separation - (start_r + end_r)) / 2 - 1
ArcArcTangentArc(CenterArc((0, 0, 1), 5, 0, 360), end_arc, r)
def test_line_with_list(self):
"""Test line with a list of points"""
l = Line([(0, 0), (10, 0)])
self.assertAlmostEqual(l.length, 10, 5)
def test_wires_select_last(self):
with BuildLine() as test:
Line((0, 0), (0, 1))
Polyline((1, 0), (1, 1), (0, 1), (0, 0))
self.assertAlmostEqual(test.wires(Select.LAST)[0].length, 3, 5)
def test_error_conditions(self):
"""Test error handling"""
with self.assertRaises(ValueError):
with BuildLine():
Line((0, 0)) # Need two points
with self.assertRaises(ValueError):
with BuildLine():
Polyline((0, 0)) # Need two points
with self.assertRaises(ValueError):
with BuildLine():
RadiusArc((0, 0), (1, 0), 0.1) # Radius too small
with self.assertRaises(ValueError):
with BuildLine():
TangentArc((0, 0), tangent=(1, 1)) # Need two points
with self.assertRaises(ValueError):
with BuildLine():
ThreePointArc((0, 0), (1, 1)) # Need three points
with self.assertRaises(NotImplementedError):
with BuildLine() as bl:
Line((0, 0), (1, 1))
bl.faces()
with self.assertRaises(NotImplementedError):
with BuildLine() as bl:
Line((0, 0), (1, 1))
bl.solids()
def test_obj_name(self):
with BuildLine() as test:
self.assertEqual(test._obj_name, "line")
if __name__ == "__main__":
unittest.main()