diff --git a/drawing.scad b/drawing.scad index 38ab16a..f9715c4 100644 --- a/drawing.scad +++ b/drawing.scad @@ -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 diff --git a/math.scad b/math.scad index 1b921ac..28b33d6 100644 --- a/math.scad +++ b/math.scad @@ -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)) diff --git a/shapes2d.scad b/shapes2d.scad index 85b67e9..34351a4 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -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=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)