Add DoubleTangentArc

This commit is contained in:
gumyr 2024-02-27 16:18:39 -05:00
parent ec495d5aa6
commit 18aafed8e6
7 changed files with 180 additions and 3 deletions

View file

@ -0,0 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<svg width="40.09mm" height="101.09mm" viewBox="-0.01125 -25.01125 10.0225 25.2725" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(1,-1)" stroke-linecap="round">
<g fill="none" stroke="rgb(0,0,0)" stroke-width="0.0225">
<path d="M 6.0,0.0 A 47.000000000000085,47.000000000000085 180.0 0,0 9.615385,18.076923" />
<circle cx="6.0" cy="0.0" r="0.25" />
</g>
<g fill="none" stroke="rgb(0,0,0)" stroke-width="0.0225" id="dashed" stroke-dasharray="0.27 0.405">
<path d="M 0.0,20.0 A 5.0,5.0 180.0 1,0 2.5,15.669873" />
<line x1="6.0" y1="0.0" x2="6.0" y2="5.0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 680 B

View file

@ -17,6 +17,7 @@ Cheat Sheet
| :class:`~objects_curve.Bezier` | :class:`~objects_curve.Bezier`
| :class:`~objects_curve.CenterArc` | :class:`~objects_curve.CenterArc`
| :class:`~objects_curve.DoubleTangentArc`
| :class:`~objects_curve.EllipticalCenterArc` | :class:`~objects_curve.EllipticalCenterArc`
| :class:`~objects_curve.FilletPolyline` | :class:`~objects_curve.FilletPolyline`
| :class:`~objects_curve.Helix` | :class:`~objects_curve.Helix`

View file

@ -90,6 +90,13 @@ The following objects all can be used in BuildLine contexts. Note that
+++ +++
Arc defined by center, radius, & angles Arc defined by center, radius, & angles
.. grid-item-card:: :class:`~objects_curve.DoubleTangentArc`
.. image:: assets/double_tangent_line_example.svg
+++
Arc defined by point/tangent pair & other curve
.. grid-item-card:: :class:`~objects_curve.EllipticalCenterArc` .. grid-item-card:: :class:`~objects_curve.EllipticalCenterArc`
.. image:: assets/elliptical_center_arc_example.svg .. image:: assets/elliptical_center_arc_example.svg
@ -189,6 +196,7 @@ Reference
.. autoclass:: BaseLineObject .. autoclass:: BaseLineObject
.. autoclass:: Bezier .. autoclass:: Bezier
.. autoclass:: CenterArc .. autoclass:: CenterArc
.. autoclass:: DoubleTangentArc
.. autoclass:: EllipticalCenterArc .. autoclass:: EllipticalCenterArc
.. autoclass:: FilletPolyline .. autoclass:: FilletPolyline
.. autoclass:: Helix .. autoclass:: Helix

View file

@ -246,7 +246,19 @@ svg.add_shape(other, "dashed")
svg.add_shape(intersecting_line.line) svg.add_shape(intersecting_line.line)
svg.add_shape(dot.moved(Location(Vector((1, 0))))) svg.add_shape(dot.moved(Location(Vector((1, 0)))))
svg.write("assets/intersecting_line_example.svg") svg.write("assets/intersecting_line_example.svg")
show(other, intersecting_line)
with BuildLine() as double_tangent:
l2 = JernArc(start=(0, 20), tangent=(0, 1), radius=5, arc_size=-300)
l3 = DoubleTangentArc((6, 0), tangent=(0, 1), other=l2)
s = 100 / max(*double_tangent.line.bounding_box().size)
svg = ExportSVG(scale=s)
svg.add_layer("dashed", line_type=LineType.ISO_DASH_SPACE)
svg.add_shape(l2, "dashed")
svg.add_shape(l3)
svg.add_shape(dot.scale(5).moved(Pos(6, 0)))
svg.add_shape(Edge.make_line((6, 0), (6, 5)), "dashed")
svg.write("assets/double_tangent_line_example.svg")
# show_object(example_1.line, name="Ex. 1") # show_object(example_1.line, name="Ex. 1")
# show_object(example_2.line, name="Ex. 2") # show_object(example_2.line, name="Ex. 2")
# show_object(example_3.line, name="Ex. 3") # show_object(example_3.line, name="Ex. 3")

View file

@ -73,6 +73,7 @@ __all__ = [
"BaseLineObject", "BaseLineObject",
"Bezier", "Bezier",
"CenterArc", "CenterArc",
"DoubleTangentArc",
"EllipticalCenterArc", "EllipticalCenterArc",
"EllipticalStartArc", "EllipticalStartArc",
"FilletPolyline", "FilletPolyline",

View file

@ -29,13 +29,15 @@ license:
from __future__ import annotations from __future__ import annotations
import copy import copy
import warnings
from math import copysign, cos, radians, sin, sqrt from math import copysign, cos, radians, sin, sqrt
from scipy.optimize import minimize
from typing import Iterable, Union from typing import Iterable, Union
from build123d.build_common import WorkplaneList, flatten_sequence, validate_inputs from build123d.build_common import WorkplaneList, flatten_sequence, validate_inputs
from build123d.build_enums import AngularDirection, GeomType, LengthMode, Mode from build123d.build_enums import AngularDirection, GeomType, Keep, LengthMode, Mode
from build123d.build_line import BuildLine from build123d.build_line import BuildLine
from build123d.geometry import Axis, Plane, Vector, VectorLike from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE
from build123d.topology import Edge, Face, Wire, Curve from build123d.topology import Edge, Face, Wire, Curve
@ -149,6 +151,105 @@ class CenterArc(BaseLineObject):
super().__init__(arc, mode=mode) super().__init__(arc, mode=mode)
class DoubleTangentArc(BaseLineObject):
"""Line Object: Double Tangent Arc
Create an arc defined by a point/tangent pair and another line which the other end
is tangent to.
Contains a solver.
Args:
pnt (VectorLike): starting point of tangent arc
tangent (VectorLike): tangent at starting point of tangent arc
other (Union[Curve, Edge, Wire]): reference line
keep (Keep, optional): selector for which arc to keep when two arcs are
possible. The arc generated with TOP or BOTTOM depends on the geometry
and isn't necessarily easy to predict. Defaults to Keep.TOP.
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
Raises:
RunTimeError: no double tangent arcs found
"""
_applies_to = [BuildLine._tag]
def __init__(
self,
pnt: VectorLike,
tangent: VectorLike,
other: Union[Curve, Edge, Wire],
keep: Keep = Keep.TOP,
mode: Mode = Mode.ADD,
):
context: BuildLine = BuildLine._get_context(self)
validate_inputs(context, self)
arc_pt = WorkplaneList.localize(pnt)
arc_tangent = WorkplaneList.localize(tangent)
if WorkplaneList._get_context() is not None:
workplane = WorkplaneList._get_context().workplanes[0]
else:
workplane = Edge.make_line(arc_pt, arc_pt + arc_tangent).common_plane(
*other.edges()
)
if workplane is None:
raise ValueError("DoubleTangentArc only works on a single plane")
workplane = -workplane # Flip to help with TOP/BOTTOM
rotation_axis = Axis((0, 0, 0), workplane.z_dir)
# Protect against massive circles that are effectively straight lines
max_size = 2 * other.bounding_box().add(arc_pt).diagonal
# Function to be minimized
def func(radius, perpendicular_bisector):
center = arc_pt + perpendicular_bisector * radius
separation = other.distance_to(center)
return abs(separation - radius)
# Minimize the function using bounds and the tolerance value
arc_centers = []
for angle in [90, -90]:
perpendicular_bisector = arc_tangent.rotate(rotation_axis, angle)
result = minimize(
func,
x0=0.0,
args=perpendicular_bisector,
method="Nelder-Mead",
bounds=[(0.0, max_size)],
tol=TOLERANCE,
)
arc_radius = result.x[0]
arc_center = arc_pt + perpendicular_bisector * arc_radius
# Check for matching tangents
circle = Edge.make_circle(
arc_radius, Plane(arc_center, z_dir=rotation_axis.direction)
)
dist, p1, p2 = other.distance_to_with_closest_points(circle)
if dist > TOLERANCE: # If they aren't touching
continue
other_axis = Axis(p1, other.tangent_at(p1))
circle_axis = Axis(p2, circle.tangent_at(p2))
if other_axis.is_parallel(circle_axis):
arc_centers.append(arc_center)
if len(arc_centers) == 0:
raise RuntimeError("No double tangent arcs found")
# If there are multiple solutions, select the desired one
if keep == Keep.TOP:
arc_centers = arc_centers[0:1]
elif keep == Keep.BOTTOM:
arc_centers = arc_centers[-1:]
with BuildLine() as double:
for center in arc_centers:
_, p1, _ = other.distance_to_with_closest_points(center)
TangentArc(arc_pt, p1, tangent=arc_tangent)
super().__init__(double.line, mode=mode)
class EllipticalStartArc(BaseLineObject): class EllipticalStartArc(BaseLineObject):
"""Line Object: Elliptical Start Arc """Line Object: Elliptical Start Arc

View file

@ -107,6 +107,47 @@ class BuildLineTests(unittest.TestCase):
Bezier(*pts, weights=wts) Bezier(*pts, weights=wts)
self.assertAlmostEqual(bz.wires()[0].length, 225.86389406824566, 5) self.assertAlmostEqual(bz.wires()[0].length, 225.86389406824566, 5)
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)
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): def test_elliptical_start_arc(self):
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
with BuildLine(): with BuildLine():