1//////////////////////////////////////////////////////////////////////
  2// LibFile: mutators.scad
  3//   Functions and modules to mutate children in various ways.
  4// Includes:
  5//   include <BOSL2/std.scad>
  6// FileGroup: Basic Modeling
  7// FileSummary: Modules and Functions to mutate items.
  8// FileFootnotes: STD=Included in std.scad
  9//////////////////////////////////////////////////////////////////////
 10
 11//////////////////////////////////////////////////////////////////////
 12// Section: Bounding Box
 13//////////////////////////////////////////////////////////////////////
 14
 15// Module: bounding_box()
 16// Usage:
 17//   bounding_box([excess],[planar]) CHILDREN;
 18// Description:
 19//   Returns the smallest axis-aligned square (or cube) shape that contains all the 2D (or 3D)
 20//   children given.  The module children() must 3d when planar=false and
 21//   2d when planar=true, or you will get a warning of mixing dimension
 22//   or scaling by 0.
 23// Arguments:
 24//   excess = The amount that the bounding box should be larger than needed to bound the children, in each axis.
 25//   planar = If true, creates a 2D bounding rectangle.  Is false, creates a 3D bounding cube.  Default: false
 26// Example(3D):
 27//   module shapes() {
 28//       translate([10,8,4]) cube(5);
 29//       translate([3,0,12]) cube(2);
 30//   }
 31//   #bounding_box() shapes();
 32//   shapes();
 33// Example(2D):
 34//   module shapes() {
 35//       translate([10,8]) square(5);
 36//       translate([3,0]) square(2);
 37//   }
 38//   #bounding_box(planar=true) shapes();
 39//   shapes();
 40module bounding_box(excess=0, planar=false) {
 41    // a 3d (or 2d when planar=true) approx. of the children projection on X axis
 42    module _xProjection() {
 43        if (planar) {
 44            projection()
 45                rotate([90,0,0])
 46                    linear_extrude(1, center=true)
 47                        hull()
 48                            children();
 49        } else {
 50            xs = excess<.1? 1: excess;
 51            linear_extrude(xs, center=true)
 52                projection()
 53                    rotate([90,0,0])
 54                        linear_extrude(xs, center=true)
 55                            projection()
 56                                hull()
 57                                    children();
 58        }
 59    }
 60
 61    // a bounding box with an offset of 1 in all axis
 62    module _oversize_bbox() {
 63        if (planar) {
 64            minkowski() {
 65                _xProjection() children(); // x axis
 66                rotate(-90) _xProjection() rotate(90) children(); // y axis
 67            }
 68        } else {
 69            minkowski() {
 70                _xProjection() children(); // x axis
 71                rotate(-90) _xProjection() rotate(90) children(); // y axis
 72                rotate([0,-90,0]) _xProjection() rotate([0,90,0]) children(); // z axis
 73            }
 74        }
 75    }
 76
 77    // offsets a cube by `excess`
 78    module _shrink_cube() {
 79        intersection() {
 80            translate((1-excess)*[ 1, 1, 1]) children();
 81            translate((1-excess)*[-1,-1,-1]) children();
 82        }
 83    }
 84
 85    if(planar) {
 86        offset(excess-1/2) _oversize_bbox() children();
 87    } else {
 88        render(convexity=2)
 89        if (excess>.1) {
 90            _oversize_bbox() children();
 91        } else {
 92            _shrink_cube() _oversize_bbox() children();
 93        }
 94    }
 95}
 96
 97
 98//////////////////////////////////////////////////////////////////////
 99// Section: Warp Mutators
100//////////////////////////////////////////////////////////////////////
101
102
103// Module: chain_hull()
104//
105// Usage:
106//   chain_hull() CHILDREN;
107//
108// Description:
109//   Performs hull operations between consecutive pairs of children,
110//   then unions all of the hull results.  This can be a very slow
111//   operation, but it can provide results that are hard to get
112//   otherwise.
113//
114// Side Effects:
115//   `$idx` is set to the index value of the first child of each hulling pair, and can be used to modify each child pair individually.
116//   `$primary` is set to true when the child is the first in a chain pair.
117//
118// Example:
119//   chain_hull() {
120//       cube(5, center=true);
121//       translate([30, 0, 0]) sphere(d=15);
122//       translate([60, 30, 0]) cylinder(d=10, h=20);
123//       translate([60, 60, 0]) cube([10,1,20], center=false);
124//   }
125// Example: Using `$idx` and `$primary`
126//   chain_hull() {
127//       zrot(  0) right(100) if ($primary) cube(5+3*$idx,center=true); else sphere(r=10+3*$idx);
128//       zrot( 45) right(100) if ($primary) cube(5+3*$idx,center=true); else sphere(r=10+3*$idx);
129//       zrot( 90) right(100) if ($primary) cube(5+3*$idx,center=true); else sphere(r=10+3*$idx);
130//       zrot(135) right(100) if ($primary) cube(5+3*$idx,center=true); else sphere(r=10+3*$idx);
131//       zrot(180) right(100) if ($primary) cube(5+3*$idx,center=true); else sphere(r=10+3*$idx);
132//   }
133module chain_hull()
134{
135    union() {
136        if ($children == 1) {
137            children();
138        } else if ($children > 1) {
139            for (i =[1:1:$children-1]) {
140                $idx = i;
141                hull() {
142                    let($primary=true) children(i-1);
143                    let($primary=false) children(i);
144                }
145            }
146        }
147    }
148}
149
150
151// Module: path_extrude2d()
152// Usage:
153//   path_extrude2d(path, [caps=], [closed=], [s=], [convexity=]) 2D-CHILDREN;
154// Description:
155//   Extrudes 2D children along the given 2D path, with optional rounded endcaps.
156//   It works by constructing straight sections corresponding to each segment of the path and inserting rounded joints at each corner.
157//   If the children are symmetric across the Y axis line then you can set caps=true to produce rounded caps on the ends of the profile.
158//   If you set caps to true for asymmetric children then incorrect caps will be generated.
159// Arguments:
160//   path = The 2D path to extrude the geometry along.
161//   ---
162//   caps = If true, caps each end of the path with a rounded copy of the children.  Children must by symmetric across the Y axis, or results are wrong.  Default: false
163//   closed = If true, connect the starting point of the path to the ending point.  Default: false
164//   convexity = The max number of times a line could pass though a wall.  Default: 10
165//   s = Mask size to use.  Use a number larger than twice your object's largest axis.  If you make this too large, it messes with centering your view.  Default: The length of the diagonal of the path's bounding box.
166// Example:
167//   path = [
168//       each right(50, p=arc(d=100,angle=[90,180])),
169//       each left(50, p=arc(d=100,angle=[0,-90])),
170//   ];
171//   path_extrude2d(path,caps=false) {
172//       fwd(2.5) square([5,6],center=true);
173//       fwd(6) square([10,5],center=true);
174//   }
175// Example:
176//   path_extrude2d(arc(d=100,angle=[180,270]),caps=true)
177//       trapezoid(w1=10, w2=5, h=10, anchor=BACK);
178// Example:
179//   include <BOSL2/beziers.scad>
180//   path = bezpath_curve([
181//       [-50,0], [-25,50], [0,0], [50,0]
182//   ]);
183//   path_extrude2d(path, caps=false)
184//       trapezoid(w1=10, w2=3, h=5, anchor=BACK);
185// Example: Un-Closed Path
186//   $fn=16;
187//   spath = star(id=15,od=35,n=5);
188//   path_extrude2d(spath, caps=false, closed=false)
189//       move_copies([[-3.5,1.5],[0.0,3.0],[3.5,1.5]])
190//           circle(r=1.5);
191// Example: Complex Endcaps
192//   $fn=16;
193//   spath = star(id=15,od=35,n=5);
194//   path_extrude2d(spath, caps=true, closed=false)
195//       move_copies([[-3.5,1.5],[0.0,3.0],[3.5,1.5]])
196//           circle(r=1.5);
197module path_extrude2d(path, caps=false, closed=false, s, convexity=10) {
198    extra_ang = 0.1; // Extra angle for overlap of joints
199    check =
200       assert(caps==false || closed==false, "Cannot have caps on a closed extrusion")
201       assert(is_path(path,2));
202    path = deduplicate(path);
203    s = s!=undef? s :
204        let(b = pointlist_bounds(path))
205        norm(b[1]-b[0]);
206    check2 = assert(is_finite(s));
207    L = len(path);
208    for (i = [0:1:L-(closed?1:2)]) {
209        seg = select(path, i, i+1);
210        segv = seg[1] - seg[0];
211        seglen = norm(segv);
212        translate((seg[0]+seg[1])/2) {
213            rot(from=BACK, to=segv) {
214                difference() {
215                    xrot(90) {
216                        linear_extrude(height=seglen, center=true, convexity=convexity) {
217                            children();
218                        }
219                    }
220                    if (closed || i>0) {
221                        pt = select(path, i-1);
222                        pang = v_theta(rot(from=-segv, to=RIGHT, p=pt - seg[0]));
223                        fwd(seglen/2+0.01) zrot(pang/2) cube(s, anchor=BACK);
224                    }
225                    if (closed || i<L-2) {
226                        pt = select(path, i+2);
227                        pang = v_theta(rot(from=segv, to=RIGHT, p=pt - seg[1]));
228                        back(seglen/2+0.01) zrot(pang/2) cube(s, anchor=FWD);
229                    }
230                }
231            }
232        }
233    }
234    for (t=triplet(path,wrap=closed)) {
235        ang = -(180-vector_angle(t)) * sign(_point_left_of_line2d(t[2],[t[0],t[1]]));
236        delt = point3d(t[2] - t[1]);
237        if (ang!=0)
238            translate(t[1]) {
239                frame_map(y=delt, z=UP)
240                    rotate(-sign(ang)*extra_ang/2)
241                        rotate_extrude(angle=ang+sign(ang)*extra_ang)
242                            if (ang<0)
243                                right_half(planar=true) children();
244                            else
245                                left_half(planar=true) children();                          
246            }
247                
248    }
249    if (caps) {
250        bseg = select(path,0,1);
251        move(bseg[0])
252            rot(from=BACK, to=bseg[0]-bseg[1])
253                rotate_extrude(angle=180)
254                    right_half(planar=true) children();
255        eseg = select(path,-2,-1);
256        move(eseg[1])
257            rot(from=BACK, to=eseg[1]-eseg[0])
258                rotate_extrude(angle=180)
259                    right_half(planar=true) children();
260    }
261}
262
263
264// Module: cylindrical_extrude()
265// Usage:
266//   cylindrical_extrude(ir|id=, or|od=, [size=], [convexity=], [spin=], [orient=]) 2D-CHILDREN;
267// Description:
268//   Extrudes its 2D children outwards, curved around a cylindrical shape.  Uses $fn/$fa/$fs to
269//   control the faceting of the extrusion.  
270// Arguments:
271//   ir = The inner radius to extrude from.
272//   or = The outer radius to extrude to.
273//   ---
274//   od = The outer diameter to extrude to.
275//   id = The inner diameter to extrude from.
276//   size = The [X,Y] size of the 2D children to extrude.  Default: [1000,1000]
277//   convexity = The max number of times a line could pass though a wall.  Default: 10
278//   spin = Amount in degrees to spin around cylindrical axis.  Default: 0
279//   orient = The orientation of the cylinder to wrap around, given as a vector.  Default: UP
280// Example:
281//   cylindrical_extrude(or=50, ir=45)
282//       text(text="Hello World!", size=10, halign="center", valign="center");
283// Example: Spin Around the Cylindrical Axis
284//   cylindrical_extrude(or=50, ir=45, spin=90)
285//       text(text="Hello World!", size=10, halign="center", valign="center");
286// Example: Orient to the Y Axis.
287//   cylindrical_extrude(or=40, ir=35, orient=BACK)
288//       text(text="Hello World!", size=10, halign="center", valign="center");
289module cylindrical_extrude(ir, or, od, id, size=1000, convexity=10, spin=0, orient=UP) {
290    check1 = assert(is_num(size) || is_vector(size,2));
291    size = is_num(size)? [size,size] : size;
292    ir = get_radius(r=ir,d=id);
293    or = get_radius(r=or,d=od);
294    check2 = assert(all_positive([ir,or]), "Must supply positive inner and outer radius or diameter");
295    index_r = or;
296    circumf = 2 * PI * index_r;
297    width = min(size.x, circumf);
298    check3 = assert(width <= circumf, "Shape would more than completely wrap around.");
299    sides = segs(or);
300    step = circumf / sides;
301    steps = ceil(width / step);
302    rot(from=UP, to=orient) rot(spin) {
303        for (i=[0:1:steps-2]) {
304            x = (i+0.5-steps/2) * step;
305            zrot(360 * x / circumf) {
306                fwd(or*cos(180/sides)) {
307                    xrot(-90) {
308                        linear_extrude(height=or-ir, scale=[ir/or,1], center=false, convexity=convexity) {
309                            yflip()
310                            intersection() {
311                                left(x) children();
312                                rect([quantup(step,pow(2,-15)),size.y]);
313                            }
314                        }
315                    }
316                }
317            }
318        }
319    }
320}
321
322
323// Module: extrude_from_to()
324// Usage:
325//   extrude_from_to(pt1, pt2, [convexity=], [twist=], [scale=], [slices=]) 2D-CHILDREN;
326// Description:
327//   Extrudes the 2D children linearly between the 3d points pt1 and pt2.  The origin of the 2D children are placed on
328//   pt1 and pt2, and oriented perpendicular to the line between the points.  
329// Arguments:
330//   pt1 = starting point of extrusion.
331//   pt2 = ending point of extrusion.
332//   ---
333//   convexity = max number of times a line could intersect a wall of the 2D shape being extruded.
334//   twist = number of degrees to twist the 2D shape over the entire extrusion length.
335//   scale = scale multiplier for end of extrusion compared the start.
336//   slices = Number of slices along the extrusion to break the extrusion into.  Useful for refining `twist` extrusions.
337// Example(FlatSpin,VPD=200,VPT=[0,0,15]):
338//   extrude_from_to([0,0,0], [10,20,30], convexity=4, twist=360, scale=3.0, slices=40) {
339//       xcopies(3) circle(3, $fn=32);
340//   }
341module extrude_from_to(pt1, pt2, convexity, twist, scale, slices) {
342    check =
343      assert(is_vector(pt1),"First point must be a vector")
344      assert(is_vector(pt2),"Second point must be a vector");
345    pt1 = point3d(pt1);
346    pt2 = point3d(pt2);
347    rtp = xyz_to_spherical(pt2-pt1);
348    translate(pt1) {
349        rotate([0, rtp[2], rtp[1]]) {
350            if (rtp[0] > 0) {
351                linear_extrude(height=rtp[0], convexity=convexity, center=false, slices=slices, twist=twist, scale=scale) {
352                    children();
353                }
354            }
355        }
356    }
357}
358
359
360
361// Module: path_extrude()
362// Usage: path_extrude(path, [convexity], [clipsize]) 2D-CHILDREN;
363// Description:
364//   Extrudes 2D children along a 3D path.  This may be slow and can have problems with twisting.  
365// See Also: path_sweep()
366// Arguments:
367//   path = Array of points for the bezier path to extrude along.
368//   convexity = Maximum number of walls a ray can pass through.
369//   clipsize = Increase if artifacts are left.  Default: 100
370// Example(FlatSpin,VPD=600,VPT=[75,16,20]):
371//   path = [ [0, 0, 0], [33, 33, 33], [66, 33, 40], [100, 0, 0], [150,0,0] ];
372//   path_extrude(path) circle(r=10, $fn=6);
373module path_extrude(path, convexity=10, clipsize=100) {
374    rotmats = cumprod([
375       for (i = idx(path,e=-2)) let(
376           vec1 = i==0? UP : unit(path[i]-path[i-1], UP),
377           vec2 = unit(path[i+1]-path[i], UP)
378       ) rot(from=vec1,to=vec2)
379    ]);
380    // This adds a rotation midway between each item on the list
381    interp = rot_resample(rotmats,n=2,method="count");
382    epsilon = 0.0001;  // Make segments ever so slightly too long so they overlap.
383    ptcount = len(path);
384    for (i = [0:1:ptcount-2]) {
385        pt1 = path[i];
386        pt2 = path[i+1];
387        dist = norm(pt2-pt1);
388        T = rotmats[i];
389        difference() {
390            translate(pt1) {
391                multmatrix(T) {
392                    down(clipsize/2/2) {
393                        if ((dist+clipsize/2) > 0) {
394                            linear_extrude(height=dist+clipsize/2, convexity=convexity) {
395                                children();
396                            }
397                        }
398                    }
399                }
400            }
401            translate(pt1) {
402                hq = (i > 0)? interp[2*i-1] : T;
403                multmatrix(hq) down(clipsize/2+epsilon) cube(clipsize, center=true);
404            }
405            translate(pt2) {
406                hq = (i < ptcount-2)? interp[2*i+1] : T;
407                multmatrix(hq) up(clipsize/2+epsilon) cube(clipsize, center=true);
408            }
409        }
410    }
411}
412
413
414
415
416//////////////////////////////////////////////////////////////////////
417// Section: Offset Mutators
418//////////////////////////////////////////////////////////////////////
419
420// Module: minkowski_difference()
421// Usage:
422//   minkowski_difference() { BASE; DIFF1; DIFF2; ... }
423// Description:
424//   Takes a 3D base shape and one or more 3D diff shapes, carves out the diff shapes from the
425//   surface of the base shape, in a way complementary to how `minkowski()` unions shapes to the
426//   surface of its base shape.
427// Arguments:
428//   planar = If true, performs minkowski difference in 2D.  Default: false (3D)
429// Example:
430//   minkowski_difference() {
431//       union() {
432//           cube([120,70,70], center=true);
433//           cube([70,120,70], center=true);
434//           cube([70,70,120], center=true);
435//       }
436//       sphere(r=10);
437//   }
438module minkowski_difference(planar=false) {
439    difference() {
440        bounding_box(excess=0, planar=planar) children(0);
441        render(convexity=20) {
442            minkowski() {
443                difference() {
444                    bounding_box(excess=1, planar=planar) children(0);
445                    children(0);
446                }
447                for (i=[1:1:$children-1]) children(i);
448            }
449        }
450    }
451}
452
453
454
455
456// Module: offset3d()
457// Usage:
458//   offset3d(r, [size], [convexity]) CHILDREN;
459// Description:
460//   Expands or contracts the surface of a 3D object by a given amount.  This is very, very slow.
461//   No really, this is unbearably slow.  It uses `minkowski()`.  Use this as a last resort.
462//   This is so slow that no example images will be rendered.
463// Arguments:
464//   r = Radius to expand object by.  Negative numbers contract the object. 
465//   size = Maximum size of object to be contracted, given as a scalar.  Default: 100
466//   convexity = Max number of times a line could intersect the walls of the object.  Default: 10
467module offset3d(r, size=100, convexity=10) {
468    n = quant(max(8,segs(abs(r))),4);
469    if (r==0) {
470        children();
471    } else if (r>0) {
472        render(convexity=convexity)
473        minkowski() {
474            children();
475            sphere(r, $fn=n);
476        }
477    } else {
478        size2 = size * [1,1,1];
479        size1 = size2 * 1.02;
480        render(convexity=convexity)
481        difference() {
482            cube(size2, center=true);
483            minkowski() {
484                difference() {
485                    cube(size1, center=true);
486                    children();
487                }
488                sphere(-r, $fn=n);
489            }
490        }
491    }
492}
493
494
495// Module: round3d()
496// Usage:
497//   round3d(r) CHILDREN;
498//   round3d(or) CHILDREN;
499//   round3d(ir) CHILDREN;
500//   round3d(or, ir) CHILDREN;
501// Description:
502//   Rounds arbitrary 3D objects.  Giving `r` rounds all concave and convex corners.  Giving just `ir`
503//   rounds just concave corners.  Giving just `or` rounds convex corners.  Giving both `ir` and `or`
504//   can let you round to different radii for concave and convex corners.  The 3D object must not have
505//   any parts narrower than twice the `or` radius.  Such parts will disappear.  This is an *extremely*
506//   slow operation.  I cannot emphasize enough just how slow it is.  It uses `minkowski()` multiple times.
507//   Use this as a last resort.  This is so slow that no example images will be rendered.
508// Arguments:
509//   r = Radius to round all concave and convex corners to.
510//   or = Radius to round only outside (convex) corners to.  Use instead of `r`.
511//   ir = Radius to round only inside (concave) corners to.  Use instead of `r`.
512module round3d(r, or, ir, size=100)
513{
514    or = get_radius(r1=or, r=r, dflt=0);
515    ir = get_radius(r1=ir, r=r, dflt=0);
516    offset3d(or, size=size)
517        offset3d(-ir-or, size=size)
518            offset3d(ir, size=size)
519                children();
520}
521
522
523
524// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap