""" 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.9785865257071, 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.98661946375782, 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 self.assertRaises(ValueError): p = FilletPolyline( (0, 0), (10, 0), (10, 10), (0, 10), radius=(1, 2, 3, 0), close=True, ) with self.assertRaises(ValueError): p = FilletPolyline( (0, 0), (10, 0), (10, 10), (0, 10), radius=(1, 2, 3, 4), close=False, ) with self.assertRaises(ValueError): p = FilletPolyline( (0, 0), (10, 0), (10, 10), (0, 10), radius=-1, close=True, ) with self.assertRaises(ValueError): p = FilletPolyline( (0, 0), (10, 0), (10, 10), (0, 10), radius=(1, 2), close=True, ) with BuildLine(Plane.YZ): p = FilletPolyline( (0, 0), (10, 0), (10, 10), (0, 10), radius=(1, 2, 3, 4), 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) 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 ) ## Spot check all conditions r1, r2 = 3, 8 start_center = (0, 0) start_arc = CenterArc(start_center, r1, 0, 360) end_y = { "no_overlap": (r1 + r2) * 1.1, "partial_overlap": (r1 + r2) / 2, "full_overlap": (r2 - r1) * 0.9, } # Test matrix: # (separation, keep pair, [min_limit, max_limit]) # actual limit will be (separation + min_limit) / 2 cases = [ (end_y["no_overlap"], (Keep.INSIDE, Keep.INSIDE), [r1 - r2, None]), (end_y["no_overlap"], (Keep.OUTSIDE, Keep.INSIDE), [-r1 + r2, None]), (end_y["no_overlap"], (Keep.INSIDE, Keep.OUTSIDE), [r1 + r2, None]), (end_y["no_overlap"], (Keep.OUTSIDE, Keep.OUTSIDE), [-r1 - r2, None]), (end_y["partial_overlap"], (Keep.INSIDE, Keep.INSIDE), [None, r1 - r2]), (end_y["partial_overlap"], (Keep.OUTSIDE, Keep.INSIDE), [None, -r1 + r2]), (end_y["partial_overlap"], (Keep.BOTH, Keep.INSIDE), [None, r1 + r2]), (end_y["partial_overlap"], (Keep.INSIDE, Keep.OUTSIDE), [r1 + r2, None]), (end_y["partial_overlap"], (Keep.OUTSIDE, Keep.OUTSIDE), [None, None]), (end_y["full_overlap"], (Keep.INSIDE, Keep.INSIDE), [r1 + r2, r1 + r2]), (end_y["full_overlap"], (Keep.OUTSIDE, Keep.INSIDE), [-r1 + r2, -r1 + r2]), ] # Check min and max radii, tangency for case in cases: end_center = (0, case[0]) end_arc = CenterArc(end_center, r2, 0, 360) flip_max = -1 if case[1] == (Keep.BOTH, Keep.INSIDE) else 1 flip_min = -1 if case[0] == end_y["full_overlap"] else 1 min_r = 0 if case[2][0] is None else (flip_min * case[0] + case[2][0]) / 2 max_r = 1e6 if case[2][1] is None else (flip_max * case[0] + case[2][1]) / 2 print(case[1], min_r, max_r, case[0]) print(min_r + 0.01, min_r * 0.99, max_r - 0.01, max_r + 0.01) print((case[0] - 1 * (r1 + r2)) / 2) # Greater than min l1 = ArcArcTangentArc(start_arc, end_arc, min_r + 0.01, keep=case[1]) _, 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 ) # Less than max l1 = ArcArcTangentArc(start_arc, end_arc, max_r - 0.01, keep=case[1]) _, 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 ) # Less than min with self.assertRaises(ValueError): ArcArcTangentArc(start_arc, end_arc, min_r * 0.99, keep=case[1]) # Greater than max if max_r != 1e6: with self.assertRaises(ValueError): ArcArcTangentArc(start_arc, end_arc, max_r + 0.01, keep=case[1]) 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()