expand glued_circles to different size circles

This commit is contained in:
Adrian Mariano 2025-10-05 11:20:32 -04:00
parent 5408a36ef9
commit 584635b4f3
3 changed files with 279 additions and 4 deletions

View file

@ -697,8 +697,10 @@ module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, round
// arc(...) [ATTACHMENTS];
// Description:
// If called as a function, returns a 2D or 3D path forming an arc. Numerous methods are available to specify the arc as listed in the Arguments section.
// If `wedge` is true, the centerpoint of the arc appears as the first point in the result.
// If called as a module, the arc must be 2D and the module creates the 2D arc polygon or pie slice shape.
// If called as a module, the arc must be 2D and the module creates the segment of the circle obtained by adding
// the closing segment to the arc.
// If `wedge` is true, the centerpoint of the arc appears as the first point in the result, which is now a sector of a circle.
// The module produces the sector of the circle, or pie shape, bounded by the arc.
// .
// If `endpoint=false`, which is only accepted by the functional form, then the arc stops one step before the final point.
// The `rounding` parameter, which is permitted only when `wedge=true`, applies specified radius roundings at each of the corners, with `rounding[0]` giving

View file

@ -1552,7 +1552,10 @@ function c_norm(z) = norm_fro(z);
// coefficients are real numbers. If real is true, then returns only the
// real roots. Otherwise returns a pair of complex values. This method
// may be more reliable than the general root finder at distinguishing
// real roots from complex roots.
// real roots from complex roots. If the input is a linear equation the
// function returns a single root, and it returns the empty list when no
// appropriate roots exist (such as when all the roots are complex and real=true).
// Algorithm from: https://people.csail.mit.edu/bkph/articles/Quadratics.pdf
function quadratic_roots(a,b,c,real=false) =
real ? [for(root = quadratic_roots(a,b,c,real=false)) if (root.y==0) root.x]
@ -1561,7 +1564,8 @@ function quadratic_roots(a,b,c,real=false) =
assert(is_num(a) && is_num(b) && is_num(c))
assert(a!=0 || b!=0 || c!=0, "\nQuadratic must have a nonzero coefficient.")
a==0 && b==0 ? [] : // No solutions
a==0 ? [[-c/b,0]] :
a==0 ? [[-c/b,0]] : // linear case, only one root
b==0 && c==0 ? [[0,0],[0,0]] : // a*x^2=0, zero is a double root
let(
descrim = b*b-4*a*c,
sqrt_des = sqrt(abs(descrim))

View file

@ -1726,12 +1726,281 @@ function ring(n,ring_width,r,r1,r2,angle,d,d1,d2,cp,points,corner, width,thickne
) new_r>r_actual ? concat(arc2, reverse(arc1)) : concat(arc1,reverse(arc2));
// Function&Module: glued_circles()
// Synopsis: Creates a shape of two circles joined by a curved waist.
// SynTags: Geom, Path
// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable
// See Also: circle(), ellipse(), egg(), keyhole()
// Usage: As Module
// glued_circles(r/d=, [spread], [r1=/d1=], [r2=/d2=], [tangent=], [bulge=], [width=], [blendR=/blendD=], anchor=, spin=) [ATTACHMENTS];
// Usage: As Function
// path = glued_circles(r/d=, [spread], [r1=/d1=], [r2=/d2=], [tangent=], [bulge=], [width=], [blendR=/blendD=], anchor=, spin=) [ATTACHMENTS];
// Description:
// Computes a shape created by joining two circles with arcs. The arcs can join the circles to create a convex, egg shape
// or they can join the circles to create a concave shape. The circles being joined are permitted to overlap each other.
// When called a function returns the path describing shape with its first point on the X+ axis. This module uses "hull" style anchoring.
// When the circles are different sizes, the circle on the left has radius `r1` and the right hand circle has radius `r2`.
// .
// The joining arcs can be specified in four different ways. You can simply specify the radius of the arc using `blendR=` or `blendD=`.
// In this case a positive radius results in a convex shape and a negative radius results in a concave shape. A forbidden radius range exists
// where the requested configuration is impossible; the exact bounds of this range depend on the specific geometry.
// When `abs(blendR)` is very large the connection will be nearly a straight line.
// .
// You can specify the angle of the tangent line at the point where the blending arc meets the left hand circle. This angle is
// measured between the tangent line and the X- axis, so an angle of zero gives a horizontal tangent, which will produces a straight
// joining "arc" if the circles are the same size. A positive angle will rotate the tangent point to the right around the circle
// and a negative one will rotate it around the left. When the tangent angle approaches -90 the shape approaches a circle that covers the
// two circles being joined. The degenerate case of `tangent=-90` is not permitted. A maximum legal tangent angle exists that depends on
// the geometry.
// .
// You can specify the `bulge=` parameter, which measures how the blending arc deviates from a straight line. A positive value means
// it deviates by the specified distance to create a convex, bulging shape. A negative value means it deviates by the specified distance
// to create a concave shape. A value of zero produces a flat connection.
// .
// Finally you can give `width=`. When `width` is smaller than either circle diameter it specifies the width of the waist of the shape (the
// narrowest point). In this case the shape is concave. When `width` is larger than either circle diameter it specifies the maximum width of
// shape. In this case the shape is convex. The width parameter cannot be in between the diameters of the two circles.
// Figure(2D,Med,NoScales): This figure shows width and bulge on a concave shape, when width is smaller than the smallest circle or bulge is negative. The black line shows the flat connection between the two circles, which is the bulge=0 case.
// r1=25;
// r2=15;
// s=35;
// h=7;
// $fa=5;$fs=1;
// path = glued_circles(r1=r1,r2=r2,spread=s, bulge=-h);
// pathflat = glued_circles(r1=r1,r2=r2,spread=s, bulge=0);
// stroke(pathflat,width=.5,color="black");
// stroke(path,width=.5);
// rr=_gs_indent_R(r1,r2,s,h);
// pts=circle_circle_intersection( abs(r1+rr),[-s/2,0], abs(r2+rr), [s/2,0]);
// cp = pts[rr<0?1:0];
// tan_pts = circle_circle_tangents(r1,[-s/2,0],r2,[s/2,0])[0];
// dir = unit(zrot(-90,tan_pts[1]-tan_pts[0]));
// stroke(tan_pts, width=.1, color="black");
// isect = line_intersection([cp,cp+dir], tan_pts, LINE, LINE);
// stroke([isect, cp+rr*dir], endcaps="arrow2", width=.5, color="blue");
// left(2)back(15)color("blue")text("bulge < 0", size=4,anchor=RIGHT);
// width=11.6;
// color("green"){
// stroke([[cp.x,-width],[cp.x,width]], endcaps="arrow2", width=.5);
// fwd(4)right(cp.x+2)text("width", size=4, anchor=LEFT);
// }
// Figure(2D,Med,NoScales): This figure shows width and bulge on a convexe shape, when width is larger than the largest circle or bulge is positive. The black line shows the flat connection between the two circles, which is the bulge=0 case.
// r1=25;
// r2=15;
// s=35;
// h=-5.5;
// $fa=5;$fs=1;
// path = glued_circles(r1=r1,r2=r2,spread=s, bulge=-h);
// pathflat = glued_circles(r1=r1,r2=r2,spread=s, bulge=0);
// stroke(pathflat,width=.5,color="black");
// stroke(path,width=.5);
// rr=_gs_indent_R(r1,r2,s,h);
// pts=circle_circle_intersection( abs(r1+rr),[-s/2,0], abs(r2+rr), [s/2,0]);
// cp = pts[rr<0?1:0];
// tan_pts = circle_circle_tangents(r1,[-s/2,0],r2,[s/2,0])[0];
// dir = unit(zrot(-90,tan_pts[1]-tan_pts[0]));
// stroke(tan_pts, width=.1, color="black");
// isect = line_intersection([cp,cp+dir], tan_pts, LINE, LINE);
// stroke([isect, cp+rr*dir], endcaps="arrow2", width=.5, color="blue");
// left(3.2)back(12)color("blue")text("bulge > 0", size=4,anchor=LEFT);
// width=27;
// color("green"){
// stroke([[cp.x,-width],[cp.x,width]], endcaps="arrow2", width=.5);
// fwd(4)right(cp.x-2)text("width", size=4, anchor=RIGHT);
// }
// Arguments:
// r = The radius or diameter of the end circles.
// spread = The distance between the centers of the end circles. Default: 10
// ---
// tangent = The angle in degrees of the tangent point for the joining arcs, measured away from the X- axis, a positive or negative value. Default: 30
// bulge = Deviation of the blending arc from a straight line connection, positive for a convex shape or negative for a concave shape.
// blendR / blendD = The radius or diameter of the blending arc, a positive for a convex shape, negative for a concave shape.
// width = width of the narrowest or widest point of the shape. A positive value.
// ---
// d = The diameter of the end circles.
// r1 / d1 = Radius or diameter of left circle.
// r2 / d2 = Radius or diameter of right circle.
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// Examples(2D):
// glued_circles(r=15, spread=40, tangent=45);
// glued_circles(d=30, spread=30, tangent=30);
// glued_circles(d=30, spread=30, tangent=15);
// glued_circles(d=30, spread=30, tangent=-30);
// Example(2D): Called as Function
// stroke(closed=true, glued_circles(r=15, spread=40, tangent=45));
// Example(2D): Circles with different sizes
// glued_circles(r1=15, r2=25, spread=40, tangent=30);
// Example(2D): Setting negative bulge value
// $fa=1;$fs=1;
// glued_circles(r1=15, r2=25, spread=40, bulge=-4);
// Example(2D): Setting positive bulge value
// $fa=1;$fs=1;
// glued_circles(r1=15, r2=25, spread=40, bulge=4);
// Example(2D): Zero bulge gives a flat connection
// glued_circles(r1=15, r2=25, spread=40, bulge=0);
// Example(2D): Specifying negative blending radius
// $fa=1;$fs=1;
// glued_circles(r1=25, r2=10, spread=40, blendR=-15);
// Example(2D): Giving positive blending radius
// $fa=1;$fs=1;
// glued_circles(r1=25, r2=10, spread=40, blendR=45);
// Example(2D): Overlapping circles
// $fa=1;$fs=1;
// glued_circles(r1=25, r2=20, spread=25, blendR=-4);
// Example(2D): Giving a small width
// glued_circles(r1=25, r2=10, spread=40, width=8);
// Example(2D): Giving a large width
// $fs=1;$fa=1;
// glued_circles(r1=25, r2=10, spread=40, width=58);
// Example(2D): Largest possible concave width is the diameter of the smaller circle
// glued_circles(r1=25, r2=10, spread=40, width=20);
// Example(2D): Smallest possible convex width is the diameter of the larger circle
// glued_circles(r1=25, r2=10, spread=40, width=50);
function glued_circles(r,spread=10, tangent, r1,r2,d,d1,d2, bulge, blendR,blendD, width, anchor=CENTER, spin=0) =
let(
r1 = get_radius(r=r,r1=r1,d=d,d1=d1),
r2 = get_radius(r=r,r2=r2,d=d,d2=d2),
blendR = get_radius(r=blendR, d=blendD)
)
assert(num_defined([tangent,bulge,blendR,width])<=1, "Can define at most one of tangent, bulge, width, and blendR/blendD")
assert(spread>abs(r1-r2), "Spread is too small: one circle is inside the other one")
let(
tangent = num_defined([tangent,bulge,blendR,width])==0 ? 30 : tangent,
cp1 = [-spread/2,0],
cp2 = [spread/2,0],
blendR = is_def(blendR) ? let(
max_indent = spread<=r1+r2 ? 0 : -_gs_waist_R(r1,r2,spread,0),
max_bulge = (r1+r2+spread)/2
)
assert(blendR>max_bulge || blendR<max_indent,
str("For this geometry blendR must be smaller than ",max_indent," or larger than ",max_bulge," but was ",blendR))
-blendR
: is_def(tangent) ? gs_get_tangent_R(r1,r2,spread,tangent)
: is_def(width) ? let(
pts = circle_circle_intersection(r1,cp1,r2,cp2),
minwidth = len(pts)==0 ? 0 : 2*pts[0].y
)
assert(width>=minwidth, str("For this geometry must have width >= ",minwidth," but width is ",width))
assert(width <= 2*min(r1,r2) || width >= 2*max(r1,r2),"The width parameter cannot be between 2*r1 and 2*r2")
let( fact=width>=2*max(r1,r2) ? -1 : 1)
fact*_gs_waist_R(fact*r1,fact*r2,spread,fact*width)
: _gs_indent_R(r1,r2,spread,-bulge),
cp_blend = is_finite(blendR) ?
let(
pts=circle_circle_intersection(abs(r1+blendR), cp1, abs(r2+blendR), cp2)
)
pts[blendR<0?0:1]
: undef,
pts = is_finite(blendR) ?
let(
result = [ cp1 + sign(blendR)*r1*unit(cp_blend-cp1),
cp2 + sign(blendR)*r2*unit(cp_blend-cp2)]
)
result
: let(
tan_pts = circle_circle_tangents(r1,cp1,r2,cp2)
)
tan_pts[1],
botpath = [
each arc(r=r2, cp=cp2, points=[right(r2,cp2),pts[1]],endpoint=false),
if (is_finite(blendR))
each arc(r=r2, cp=cp_blend, points=reverse(pts),endpoint=false)
else
pts[1],
each arc(r=r1, cp=cp1, points=[pts[0],left(r1,cp1)], endpoint=false)],
toppath = yflip(reverse(botpath)),
path = [each botpath, left(r1,cp1), each toppath]
)
reorient(anchor,spin, two_d=true, path=path, extent=true, p=path);
module glued_circles(r,spread=10, tangent, r1,r2,d,d1,d2, bulge, blendR,blendD, width, anchor=CENTER, spin=0)
{
path = glued_circles(r=r, spread=spread, tangent=tangent, r1=r1, r2=r2, d=d, d1=d1, d2=d2,
bulge=bulge, blendR=blendR, blendD=blendD,width=width);
attachable(anchor,spin, two_d=true, path=path, extent=true) {
polygon(path);
children();
}
}
function _gs_waist_R(r1,r2,s,waist) =
let(
A = (r1^2-r2^2)/2/s+s/2,
B = (r1-r2)/s,
coefs = [B^2,
2*A*B-2*r1+waist,
A^2+(waist/2)^2 - r1^2],
rts = waist<0 && approx(waist, 2*min(r1,r2))
? [-coefs[1]/2/coefs[0]]
: quadratic_roots(coefs,real=true)
)
min(rts);
function gs_get_tangent_R(r1,r2,s,ang) =
let(
pts = circle_circle_intersection(r1,[-s/2,0],r2,[s/2,0]),
minang = len(pts)==0 ? let( minr=_gs_waist_R(r1,r2,s,0),
pts=circle_circle_intersection(r1+minr,[-s/2,0], r2+minr, [s/2,0]))
atan2(pts[0].y, pts[0].x+s/2)
: atan2(pts[0].y, pts[0].x+s/2),
dummy = assert(ang>-90 && ang<90-minang,
str("Tangent angle must be between -90 and ", 90-minang, " for this geometry but was ",ang)),
A = (r1^2-r2^2)/2/s+s/2,
B = (r1-r2)/s,
flatang = acos(B)
)
approx(90-ang,flatang,eps=1e-3) ? echo("force")INF
:
let(
alpha = 90-ang > flatang ? 90+ang : 90-ang,
rts = quadratic_roots(B^2-cos(alpha)^2,
2*A*B-2*cos(alpha)^2*r1,
A^2-cos(alpha)^2*r1^2,real=true)
)
90-ang>flatang ? alpha>=90 ? rts[0] : rts[1]
: alpha<=90 ? rts[0] : rts[1];
function _gs_indent_R(r1,r2,s,h) =
let(
tan_pts = circle_circle_tangents(r1,[-s/2,0],r2,[s/2,0])[0],
circ_int = circle_circle_intersection(r1,[-s/2,0],r2,[s/2,0]),
minR = h<0 ? -2500 : len(circ_int)==0 ? _gs_waist_R(r1,r2,s,0) : 0,
maxR = h>=0 ? 2500 : -(r1+r2+s)/2,
dir = unit(zrot(-90,tan_pts[1]-tan_pts[0])),
gap = function(rr)
let(
pts=circle_circle_intersection( abs(r1+rr),[-s/2,0], abs(r2+rr), [s/2,0]),
cp = len(pts)==1 ? pts[0] : pts[rr<0?1:0],
isect = line_intersection([cp,cp+dir], tan_pts, LINE, LINE)
)
norm(isect - cp-rr*dir)
)
h==0 || abs(h) < min(gap(minR),gap(maxR)) ? INF
:
let(
maxh = gap(minR),
minh = gap(maxR),
dummy = assert(h<0 || h<=maxh, str("For this geometry must have h < ",maxh," but h=",h))
assert(h>0 || abs(h)<minh, str("For this geometry must have abs(h) < ",minh," but h=",h)),
error = function(r) gap(r)-abs(h),
goodR = root_find(error, minR, maxR, tol=1e-7)
)
goodR;
// Function&Module: old_glued_circles()
// Synopsis: Creates a shape of two circles joined by a curved waist.
// SynTags: Geom, Path
// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable
// See Also: circle(), ellipse(), egg(), keyhole()
// Usage: As Module
// glued_circles(r/d=, [spread], [tangent], ...) [ATTACHMENTS];
// Usage: As Function
// path = glued_circles(r/d=, [spread], [tangent], ...);