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