Skip to content

Commit

Permalink
Add DoubleTangentArc
Browse files Browse the repository at this point in the history
  • Loading branch information
gumyr committed Feb 27, 2024
1 parent ec495d5 commit 18aafed
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 3 deletions.
13 changes: 13 additions & 0 deletions docs/assets/double_tangent_line_example.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/cheat_sheet.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Cheat Sheet

| :class:`~objects_curve.Bezier`
| :class:`~objects_curve.CenterArc`
| :class:`~objects_curve.DoubleTangentArc`
| :class:`~objects_curve.EllipticalCenterArc`
| :class:`~objects_curve.FilletPolyline`
| :class:`~objects_curve.Helix`
Expand Down
8 changes: 8 additions & 0 deletions docs/objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ The following objects all can be used in BuildLine contexts. Note that
+++
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`

.. image:: assets/elliptical_center_arc_example.svg
Expand Down Expand Up @@ -189,6 +196,7 @@ Reference
.. autoclass:: BaseLineObject
.. autoclass:: Bezier
.. autoclass:: CenterArc
.. autoclass:: DoubleTangentArc
.. autoclass:: EllipticalCenterArc
.. autoclass:: FilletPolyline
.. autoclass:: Helix
Expand Down
14 changes: 13 additions & 1 deletion docs/objects_1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,19 @@
svg.add_shape(intersecting_line.line)
svg.add_shape(dot.moved(Location(Vector((1, 0)))))
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_2.line, name="Ex. 2")
# show_object(example_3.line, name="Ex. 3")
Expand Down
1 change: 1 addition & 0 deletions src/build123d/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"BaseLineObject",
"Bezier",
"CenterArc",
"DoubleTangentArc",
"EllipticalCenterArc",
"EllipticalStartArc",
"FilletPolyline",
Expand Down
105 changes: 103 additions & 2 deletions src/build123d/objects_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@
from __future__ import annotations

import copy
import warnings
from math import copysign, cos, radians, sin, sqrt
from scipy.optimize import minimize
from typing import Iterable, Union

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.geometry import Axis, Plane, Vector, VectorLike
from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE
from build123d.topology import Edge, Face, Wire, Curve


Expand Down Expand Up @@ -149,6 +151,105 @@ def __init__(
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):
"""Line Object: Elliptical Start Arc
Expand Down
41 changes: 41 additions & 0 deletions tests/test_build_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,47 @@ def test_bezier(self):
Bezier(*pts, weights=wts)
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):
with self.assertRaises(RuntimeError):
with BuildLine():
Expand Down

0 comments on commit 18aafed

Please sign in to comment.