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