1/////////////////////////////////////////////////////////////////////
2// LibFile: rounding.scad
3// Routines to create rounded corners, with either circular rounding,
4// or continuous curvature rounding with no sudden curvature transitions.
5// Provides rounding of corners or rounding that preserves corner points and curves the edges.
6// Also provides some 3D rounding functions, and a powerful function for joining
7// two prisms together with a rounded fillet at the joint.
8// Includes:
9// include <BOSL2/std.scad>
10// include <BOSL2/rounding.scad>
11// FileGroup: Advanced Modeling
12// FileSummary: Round path corners, rounded prisms, rounded cutouts in tubes, filleted prism joints
13//////////////////////////////////////////////////////////////////////
14include <beziers.scad>
15include <structs.scad>
16
17// Section: Types of Roundovers
18// The functions and modules in this file support two different types of roundovers and some different mechanisms for specifying
19// the size of the roundover. The usual circular roundover can produce a tactile "bump" where the curvature changes from flat to
20// circular. See https://hackernoon.com/apples-icons-have-that-shape-for-a-very-good-reason-720d4e7c8a14 for details.
21// We compute continuous curvature rounding using 4th order Bezier curves. This type of rounding, which we call "smooth" rounding,
22// does not have a "radius" so we need different ways to specify the size of the roundover. We introduce the `cut` and `joint`
23// parameters for this purpose. They can specify dimensions of circular roundovers, continuous curvature "smooth" roundovers, and even chamfers.
24// .
25// The `cut` parameter specifies the distance from the unrounded corner to the rounded tip, so how
26// much of the corner to "cut" off. This can be easier to understand than setting a circular radius, which can be
27// unexpectedly extreme when the corner is very sharp. It also allows a systematic specification of
28// corner treatments that are the same size for all corner treatments.
29// .
30// The `joint` parameter specifies the distance
31// away from the corner along the path where the roundover or chamfer should start. This parameter is good for ensuring that
32// your roundover will fit on the polygon or polyhedron, since you can easily tell whether you have enough space, and whether
33// adjacent corner treatments will interfere.
34// .
35// For circular rounding you can use the `radius` or `r` parameter to set the rounding radius.
36// .
37// For chamfers you can use `width` to set the width of the chamfer.
38// .
39// The "smooth" rounding method also has a parameter that specifies how smooth the curvature match is. This parameter, `k`,
40// ranges from 0 to 1, with a default of 0.5. Larger values gives a more
41// abrupt transition and smaller ones a more gradual transition. If you set the value much higher
42// than 0.8 the curvature changes abruptly enough that though it is theoretically continuous, it may
43// not be continuous in practice. If you set it very small then the transition is so gradual that
44// the length of the roundover may be extremely long, and the actual rounded part of the curve may be very small.
45// Figure(2D,Med,NoAxes): Parameters of a "circle" roundover
46// h = 18;
47// w = 12.6;
48// strokewidth = .3;
49// example = [[0,0],[w,h],[2*w,0]];
50// stroke(example, width=strokewidth*1.5);
51// textangle = 90-vector_angle(example)/2;
52// theta = vector_angle(example)/2;
53// color("green"){ stroke([[w,h], [w,h-18*(1-sin(theta))/cos(theta)]], width=strokewidth, endcaps="arrow2");
54// translate([w-1.75,h-7])scale(.1)rotate(textangle)text("cut",size=14); }
55// ll=lerp([w,h], [0,0],18/norm([w,h]-[0,0]) );
56// color("blue"){ stroke(_shift_segment([[w,h], ll], -.7), width=strokewidth,endcaps="arrow2");
57// translate([w/2-1.3,h/2+.6]) scale(.1)rotate(textangle)text("joint",size=14);}
58// color("red")stroke(
59// select(round_corners(example, joint=18, method="circle",$fn=64,closed=false),1,-2),
60// width=strokewidth);
61// r=18*tan(theta);
62// color("black"){
63// stroke([ll, [w,h-r-18*(1-sin(theta))/cos(theta)]], width=strokewidth, endcaps="arrow2");
64// translate([w/1.6,0])text("radius", size=1.4);
65// }
66// Figure(2D,Med,NoAxes): Parameters of a "smooth" roundover with the default of `k=0.5`. Note the long, slow transition from flat to round.
67// h = 18;
68// w = 12.6;
69// strokewidth = .3;
70// example = [[0,0],[w,h],[2*w,0]];
71// stroke(example, width=strokewidth*1.5);
72// textangle = 90-vector_angle(example)/2;
73// color("green"){ stroke([[w,h], [w,h-cos(vector_angle(example)/2) *3/8*h]], width=strokewidth, endcaps="arrow2");
74// translate([w-1.75,h-5.5])scale(.1)rotate(textangle)text("cut",size=14); }
75// ll=lerp([w,h], [0,0],18/norm([w,h]-[0,0]) );
76// color("blue"){ stroke(_shift_segment([[w,h], ll], -.7), width=strokewidth,endcaps="arrow2");
77// translate([w/2-1.3,h/2+.6]) scale(.1)rotate(textangle)text("joint",size=14);}
78// color("red")stroke(
79// select(round_corners(example, joint=18, method="smooth",closed=false),1,-2),
80// width=strokewidth);
81// Figure(2D,Med,NoAxes): Parameters of a "smooth" roundover, with `k=0.75`. The transition into the roundover is shorter, and faster. The cut length is bigger for the same joint length.
82// h = 18;
83// w = 12.6;
84// strokewidth = .3;
85// example = [[0,0],[w,h],[2*w,0]];
86// stroke(example, width=strokewidth*1.5);
87// textangle = 90-vector_angle(example)/2;
88// color("green"){ stroke([[w,h], [w,h-cos(vector_angle(example)/2) *4/8*h]], width=strokewidth, endcaps="arrow2");
89// translate([w-1.75,h-5.5])scale(.1)rotate(textangle)text("cut",size=14); }
90// ll=lerp([w,h], [0,0],18/norm([w,h]-[0,0]) );
91// color("blue"){ stroke(_shift_segment([[w,h], ll], -.7), width=strokewidth,endcaps="arrow2");
92// translate([w/2-1.3,h/2+.6]) scale(.1)rotate(textangle)text("joint",size=14);}
93// color("red")stroke(
94// select(round_corners(example, joint=18, method="smooth",closed=false,k=.75),1,-2),
95// width=strokewidth);
96// Figure(2D,Med,NoAxes): Parameters of a "smooth" roundover, with `k=0.15`. The transition is so gradual that it appears that the roundover is much smaller than specified. The cut length is much smaller for the same joint length.
97// h = 18;
98// w = 12.6;
99// strokewidth = .3;
100// example = [[0,0],[w,h],[2*w,0]];
101// stroke(example, width=strokewidth*1.5);
102// textangle = 90-vector_angle(example)/2;
103// color("green"){ stroke([[w,h], [w,h-cos(vector_angle(example)/2) *1.6/8*h]], width=strokewidth, endcaps="arrow2");
104// translate([w+.3,h])text("cut",size=1.4); }
105// ll=lerp([w,h], [0,0],18/norm([w,h]-[0,0]) );
106// color("blue"){ stroke(_shift_segment([[w,h], ll], -.7), width=strokewidth,endcaps="arrow2");
107// translate([w/2-1.3,h/2+.6]) scale(.1)rotate(textangle)text("joint",size=14);}
108// color("red")stroke(
109// select(round_corners(example, joint=18, method="smooth",closed=false,k=.15),1,-2),
110// width=strokewidth);
111// Figure(2D,Med,NoAxes): Parameters of a symmetric "chamfer".
112// h = 18;
113// w = 12.6;
114// strokewidth = .3;
115// example = [[0,0],[w,h],[2*w,0]];
116// stroke(example, width=strokewidth*1.5);
117// textangle = 90-vector_angle(example)/2;
118// color("black"){
119// stroke(fwd(1,
120// select(round_corners(example, joint=18, method="chamfer",closed=false),1,-2)),
121// width=strokewidth,endcaps="arrow2");
122// translate([w,.3])text("width", size=1.4,halign="center");
123// }
124// color("green"){ stroke([[w,h], [w,h-18*cos(vector_angle(example)/2)]], width=strokewidth, endcaps="arrow2");
125// translate([w-1.75,h-5.5])scale(.1)rotate(textangle)text("cut",size=14); }
126// ll=lerp([w,h], [0,0],18/norm([w,h]-[0,0]) );
127// color("blue"){ stroke(_shift_segment([[w,h], ll], -.7), width=strokewidth,endcaps="arrow2");
128// translate([w/2-1.3,h/2+.6]) rotate(textangle)text("joint",size=1.4);}
129// color("red")stroke(
130// select(round_corners(example, joint=18, method="chamfer",closed=false),1,-2),
131// width=strokewidth);
132
133
134// Section: Rounding Paths
135
136// Function: round_corners()
137// Synopsis: Round or chamfer the corners of a path (clipping them off).
138// SynTags: Path
139// Topics: Rounding, Paths
140// See Also: round_corners(), smooth_path(), path_join(), offset_stroke()
141// Usage:
142// rounded_path = round_corners(path, [method], [radius=], [cut=], [joint=], [closed=], [verbose=]);
143// Description:
144// Takes a 2D or 3D path as input and rounds each corner
145// by a specified amount. The rounding at each point can be different and some points can have zero
146// rounding. The `round_corners()` function supports three types of corner treatment: chamfers, circular rounding,
147// and continuous curvature rounding using 4th order bezier curves. See
148// [Types of Roundover](rounding.scad#subsection-types-of-roundover) for details on rounding types.
149// .
150// You select the type of rounding using the `method` parameter, which should be `"smooth"` to
151// get continuous curvature rounding, `"circle"` to get circular rounding, or `"chamfer"` to get chamfers. The default is circle
152// rounding. Each method accepts multiple options to specify the amount of rounding. See
153// [Types of Roundover](rounding.scad#subsection-types-of-roundover) for example diagrams.
154// .
155// * The `cut` parameter specifies the distance from the unrounded corner to the rounded tip, so how
156// much of the corner to "cut" off.
157// * The `joint` parameter specifies the distance
158// away from the corner along the path where the roundover or chamfer should start. This makes it easy to ensure your roundover will fit,
159// so use it if you want the largest possible roundover.
160// * For circular rounding you can use the `radius` or `r` parameter to set the rounding radius.
161// * For chamfers you can use the `width` parameter, which sets the width of the chamfer edge.
162// .
163// As explained in [Types of Roundover](rounding.scad#subsection-types-of-roundover), the continuous curvature "smooth"
164// type of rounding also accepts the `k` parameter, between 0 and 1, which specifies how fast the curvature changes at
165// the joint. The default is `k=0.5`.
166// .
167// If you select curves that are too large to fit the function will fail with an error. You can set `verbose=true` to
168// get a message showing a list of scale factors you can apply to your rounding parameters so that the
169// roundovers will fit on the curve. If the scale factors are larger than one
170// then they indicate how much you can increase the curve sizes before collisions will occur.
171// .
172// The parameters `radius`, `cut`, `joint` and `k` can be numbers, which round every corner using the same parameters, or you
173// can specify a list to round each corner with different parameters. If the curve is not closed then the first and last points
174// of the curve are not rounded. In this case you can specify a full list of points anyway, and the endpoint values are ignored,
175// or you can specify a list that has length len(path)-2, omitting the two dummy values.
176// .
177// If your input path includes collinear points you must use a cut or radius value of zero for those "corners". You can
178// choose a nonzero joint parameter when the collinear points form a 180 degree angle. This will cause extra points to be inserted.
179// If the collinear points form a spike (0 degree angle) then round_corners will fail.
180// .
181// Examples:
182// * `method="circle", radius=2`:
183// Rounds every point with circular, radius 2 roundover
184// * `method="smooth", cut=2`:
185// Rounds every point with continuous curvature rounding with a cut of 2, and a default 0.5 smoothing parameter
186// * `method="smooth", cut=2, k=0.3`:
187// Rounds every point with continuous curvature rounding with a cut of 2, and a very gentle 0.3 smoothness setting
188// .
189// The number of segments used for roundovers is determined by `$fa`, `$fs` and `$fn` as usual for
190// circular roundovers. For continuous curvature roundovers `$fs` and `$fn` are used and `$fa` is
191// ignored. Note that $fn is interpreted as the number of points on the roundover curve, which is
192// not equivalent to its meaning for rounding circles because roundovers are usually small fractions
193// of a circular arc. As usual, $fn overrides $fs. When doing continuous curvature rounding be sure to use lots of segments or the effect
194// will be hidden by the discretization. Note that if you use $fn with "smooth" then $fn points are added at each corner.
195// This guarantees a specific output length. It also means that if
196// you set `joint` nonzero on a flat "corner", with collinear points, you will get $fn points at that "corner."
197// If you have two roundovers that fully consume a segment then they share a point where they meet in the segment, which means the output
198// point count will be decreased by one.
199// Arguments:
200// path = list of 2d or 3d points defining the path to be rounded.
201// method = rounding method to use. Set to "chamfer" for chamfers, "circle" for circular rounding and "smooth" for continuous curvature 4th order bezier rounding. Default: "circle"
202// ---
203// radius/r = rounding radius, only compatible with `method="circle"`. Can be a number or vector.
204// cut = rounding cut distance, compatible with all methods. Can be a number or vector.
205// joint = rounding joint distance, compatible with `method="chamfer"` and `method="smooth"`. Can be a number or vector.
206// width = width of the flat edge created by chamfering, compatible with `method="chamfer"`. Can be a number or vector.
207// k = continuous curvature smoothness parameter for `method="smooth"`. Can be a number or vector. Default: 0.5
208// closed = if true treat the path as a closed polygon, otherwise treat it as open. Default: true.
209// verbose = if true display rounding scale factors that show how close roundovers are to overlapping. Default: false
210//
211// Example(2D,Med): Standard circular roundover with radius the same at every point. Compare results at the different corners.
212// $fn=36;
213// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
214// polygon(round_corners(shape, radius=1));
215// color("red") down(.1) polygon(shape);
216// Example(2D,Med): Circular roundover using the "cut" specification, the same at every corner.
217// $fn=36;
218// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
219// polygon(round_corners(shape, cut=1));
220// color("red") down(.1) polygon(shape);
221// Example(2D,Med): Continous curvature roundover using "cut", still the same at every corner. The default smoothness parameter of 0.5 was too gradual for these roundovers to fit, but 0.7 works.
222// $fn=36;
223// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
224// polygon(round_corners(shape, method="smooth", cut=1, k=0.7));
225// color("red") down(.1) polygon(shape);
226// Example(2D,Med): Continuous curvature roundover using "joint", for the last time the same at every corner. Notice how small the roundovers are.
227// $fn=36;
228// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
229// polygon(round_corners(shape, method="smooth", joint=1, k=0.7));
230// color("red") down(.1) polygon(shape);
231// Example(2D,Med): Circular rounding, different at every corner, some corners left unrounded
232// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
233// radii = [1.8, 0, 2, 0.3, 1.2, 0];
234// polygon(round_corners(shape, radius = radii,$fn=64));
235// color("red") down(.1) polygon(shape);
236// Example(2D,Med): Continuous curvature rounding, different at every corner, with varying smoothness parameters as well, and `$fs` set very small. Note that `$fa` is ignored here with method set to "smooth".
237// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
238// cuts = [1.5,0,2,0.3, 1.2, 0];
239// k = [0.6, 0.5, 0.5, 0.7, 0.3, 0.5];
240// polygon(round_corners(shape, method="smooth", cut=cuts, k=k, $fs=0.1));
241// color("red") down(.1) polygon(shape);
242// Example(2D,Med): Chamfers
243// $fn=36;
244// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
245// polygon(round_corners(shape, method="chamfer", cut=1));
246// color("red") down(.1) polygon(shape);
247// Example(Med3D): 3D printing test pieces to display different curvature shapes. You can see the discontinuity in the curvature on the "C" piece in the rendered image.
248// ten = square(50);
249// cut = 5;
250// linear_extrude(height=14) {
251// translate([25,25,0])text("C",size=30, valign="center", halign="center");
252// translate([85,25,0])text("5",size=30, valign="center", halign="center");
253// translate([85,85,0])text("3",size=30, valign="center", halign="center");
254// translate([25,85,0])text("7",size=30, valign="center", halign="center");
255// }
256// linear_extrude(height=13) {
257// polygon(round_corners(ten, cut=cut, $fn=96*4));
258// translate([60,0,0])polygon(round_corners(ten, method="smooth", cut=cut, $fn=96));
259// translate([60,60,0])polygon(round_corners(ten, method="smooth", cut=cut, k=0.32, $fn=96));
260// translate([0,60,0])polygon(round_corners(ten, method="smooth", cut=cut, k=0.7, $fn=96));
261// }
262// Example(2D,Med): Rounding a path that is not closed in a three different ways.
263// $fs=.1;
264// $fa=1;
265// zigzagx = [-10, 0, 10, 20, 29, 38, 46, 52, 59, 66, 72, 78, 83, 88, 92, 96, 99, 102, 112];
266// zigzagy = concat([0], flatten(repeat([-10,10],8)), [-10,0]);
267// zig = hstack(zigzagx,zigzagy);
268// stroke(zig,width=1); // Original shape
269// fwd(20) // Smooth size corners with a cut of 4 and curvature parameter 0.6
270// stroke(round_corners(zig,cut=4, k=0.6, method="smooth", closed=false),width=1);
271// fwd(40) // Smooth size corners with circular arcs and a cut of 4
272// stroke(round_corners(zig,cut=4,closed=false, method="circle"),width=1);
273// // Smooth size corners with a circular arc and radius 1.5 (close to maximum possible)
274// fwd(60) // Note how the different points are cut back by different amounts
275// stroke(round_corners(zig,radius=1.5,closed=false),width=1);
276// Example(FlatSpin,VPD=42,VPT=[7.75,6.69,5.22]): Rounding some random 3D paths
277// $fn=36;
278// list1= [
279// [2.887360, 4.03497, 6.372090],
280// [5.682210, 9.37103, 0.783548],
281// [7.808460, 4.39414, 1.843770],
282// [0.941085, 5.30548, 4.467530],
283// [1.860540, 9.81574, 6.497530],
284// [6.938180, 7.21163, 5.794530]
285// ];
286// list2= [
287// [1.079070, 4.74091, 6.900390],
288// [8.775850, 4.42248, 6.651850],
289// [5.947140, 9.17137, 6.156420],
290// [0.662660, 6.95630, 5.884230],
291// [6.564540, 8.86334, 9.953110],
292// [5.420150, 4.91874, 3.866960]
293// ];
294// path_sweep(regular_ngon(n=36,or=.1),round_corners(list1,closed=false, method="smooth", cut = 0.65));
295// right(6)
296// path_sweep(regular_ngon(n=36,or=.1),round_corners(list2,closed=false, method="circle", cut = 0.75));
297// Example(3D,Med): Rounding a spiral with increased rounding along the length
298// // Construct a square spiral path in 3D
299// $fn=36;
300// square = [[0,0],[1,0],[1,1],[0,1]];
301// spiral = flatten(repeat(concat(square,reverse(square)),5)); // Squares repeat 10x, forward and backward
302// squareind = [for(i=[0:9]) each [i,i,i,i]]; // Index of the square for each point
303// z = count(40)*.2+squareind;
304// path3d = hstack(spiral,z); // 3D spiral
305// rounding = squareind/20;
306// // Setting k=1 means curvature won't be continuous, but curves are as round as possible
307// // Try changing the value to see the effect.
308// rpath = round_corners(path3d, joint=rounding, k=1, method="smooth", closed=false);
309// path_sweep( regular_ngon(n=36, or=.1), rpath);
310// Example(2D): The rounding invocation that is commented out gives an error because the rounding parameters interfere with each other. The error message gives a list of factors that can help you fix this: [0.852094, 0.852094, 1.85457, 10.1529]
311// $fn=64;
312// path = [[0, 0],[10, 0],[20, 20],[30, -10]];
313// debug_polygon(path);
314// //polygon(round_corners(path,cut = [1,3,1,1],
315// // method="circle"));
316// Example(2D): The list of factors shows that the problem is in the first two rounding values, because the factors are smaller than one. If we multiply the first two parameters by 0.85 then the roundings fit. The verbose option gives us the same fit factors.
317// $fn=64;
318// path = [[0, 0],[10, 0],[20, 20],[30, -10]];
319// polygon(round_corners(path,cut = [0.85,3*0.85,1,1],
320// method="circle", verbose=true));
321// Example(2D): From the fit factors we can see that rounding at vertices 2 and 3 could be increased a lot. Applying those factors we get this more rounded shape. The new fit factors show that we can still further increase the rounding parameters if we wish.
322// $fn=64;
323// path = [[0, 0],[10, 0],[20, 20],[30, -10]];
324// polygon(round_corners(path,cut = [0.85,3*0.85,2.13, 10.15],
325// method="circle",verbose=true));
326// Example(2D): Using the `joint` parameter it's easier to understand whether your roundvers will fit. We can guarantee a fairly large roundover on any path by picking each one to use up half the segment distance along the shorter of its two segments:
327// $fn=64;
328// path = [[0, 0],[10, 0],[20, 20],[30, -10]];
329// path_len = path_segment_lengths(path,closed=true);
330// halflen = [for(i=idx(path)) min(select(path_len,i-1,i))/2];
331// polygon(round_corners(path,joint = halflen,
332// method="circle",verbose=true));
333// Example(2D): Chamfering, specifying the chamfer width
334// path = star(5, step=2, d=100);
335// path2 = round_corners(path, method="chamfer", width=5);
336// polygon(path2);
337// Example(2D): Chamfering, specifying the cut
338// path = star(5, step=2, d=100);
339// path2 = round_corners(path, method="chamfer", cut=5);
340// polygon(path2);
341// Example(2D): Chamfering, specifying joint length
342// path = star(5, step=2, d=100);
343// path2 = round_corners(path, method="chamfer", joint=5);
344// polygon(path2);
345// Example(2D): Two passes to apply chamfers first, and then round the unchamfered corners. Chamfers always add one point, so it's not hard to keep track of the vertices
346// $fn=32;
347// shape = square(10);
348// chamfered = round_corners(shape, method="chamfer",
349// cut=[2,0,2,0]);
350// rounded = round_corners(chamfered,
351// cut = [0, 0, // 1st original vertex, chamfered
352// 1.5, // 2nd original vertex
353// 0, 0, // 3rd original vertex, chamfered
354// 2.5]); // 4th original vertex
355// polygon(rounded);
356// Example(2D): Another example of mixing chamfers and roundings with two passes
357// path = star(5, step=2, d=100);
358// chamfcut = [for (i=[0:4]) each [7,0]];
359// radii = [for (i=[0:4]) each [0,0,10]];
360// path2=round_corners(
361// round_corners(path,
362// method="chamfer",
363// cut=chamfcut),
364// radius=radii);
365// stroke(path2, closed=true);
366// Example(2D,Med,NoAxes): Specifying by corner index. Use {{list_set()}} to construct the full chamfer cut list.
367// path = star(47, ir=25, or=50); // long path, lots of corners
368// chamfind = [8, 28, 60]; // But only want 3 chamfers
369// chamfcut = list_set([],chamfind,[10,13,15],minlen=len(path));
370// rpath = round_corners(path, cut=chamfcut, method="chamfer");
371// polygon(rpath);
372// Example(2D,Med,NoAxes): Two-pass to chamfer and round by index. Use {{repeat_entries()}} to correct for first pass chamfers.
373// $fn=32;
374// path = star(47, ir=32, or=65); // long path, lots of corners
375// chamfind = [8, 28, 60]; // But only want 3 chamfers
376// roundind = [7,9,27,29,59,61]; // And 6 roundovers
377// chamfcut = list_set([],chamfind,[10,13,15],minlen=len(path));
378// roundcut = list_set([],roundind,repeat(8,6),minlen=len(path));
379// dups = list_set([], chamfind, repeat(2,len(chamfind)), dflt=1, minlen=len(path));
380// rpath1 = round_corners(path, cut=chamfcut, method="chamfer");
381// rpath2 = round_corners(rpath1, cut=repeat_entries(roundcut,dups));
382// polygon(rpath2);
383module round_corners(path, method="circle", radius, r, cut, joint, width, k, closed=true, verbose=false) {no_module();}
384function round_corners(path, method="circle", radius, r, cut, joint, width, k, closed=true, verbose=false) =
385 assert(in_list(method,["circle", "smooth", "chamfer"]), "method must be one of \"circle\", \"smooth\" or \"chamfer\"")
386 let(
387 default_k = 0.5,
388 size=one_defined([radius, r, cut, joint, width], "radius,r,cut,joint,width"),
389 path = force_path(path),
390 size_ok = is_num(size) || len(size)==len(path) || (!closed && len(size)==len(path)-2),
391 k_ok = is_undef(k) || (method=="smooth" && (is_num(k) || len(k)==len(path) || (!closed && len(k)==len(path)-2))),
392 measure = is_def(radius) ? "radius"
393 : is_def(r) ? "radius"
394 : is_def(cut) ? "cut"
395 : is_def(joint) ? "joint"
396 : "width"
397 )
398 assert(is_path(path,[2,3]), "input path must be a 2d or 3d path")
399 assert(len(path)>2,str("Path has length ",len(path),". Length must be 3 or more."))
400 assert(size_ok,str("Input ",measure," must be a number or list with length ",len(path), closed?"":str(" or ",len(path)-2)))
401 assert(k_ok,method=="smooth" ? str("Input k must be a number or list with length ",len(path), closed?"":str(" or ",len(path)-2)) :
402 "Input k is only allowed with method=\"smooth\"")
403 assert(method=="circle" || measure!="radius", "radius parameter allowed only with method=\"circle\"")
404 assert(method=="chamfer" || measure!="width", "width parameter allowed only with method=\"chamfer\"")
405 let(
406 parm = is_num(size) ? repeat(size, len(path)) :
407 len(size)<len(path) ? [0, each size, 0] :
408 size,
409 k = is_undef(k) ? repeat(default_k,len(path)) :
410 is_num(k) ? repeat(k, len(path)) :
411 len(k)<len(path) ? [0, each k, 0] :
412 k,
413 badparm = [for(i=idx(parm)) if(parm[i]<0)i],
414 badk = [for(i=idx(k)) if(k[i]<0 || k[i]>1)i]
415 )
416 assert(is_vector(parm) && badparm==[], str(measure," must be nonnegative"))
417 assert(is_vector(k) && badk==[], "k parameter must be in the interval [0,1]")
418 let(
419 // dk is a list of parameters, where distance is the joint length to move away from the corner
420 // "smooth" method: [distance, curvature]
421 // "circle" method: [distance, radius]
422 // "chamfer" method: [distance]
423 dk = [
424 for(i=[0:1:len(path)-1])
425 let(
426 pathbit = select(path,i-1,i+1),
427 // This is the half-angle at the corner
428 angle = approx(pathbit[0],pathbit[1]) || approx(pathbit[1],pathbit[2]) ? undef
429 : vector_angle(select(path,i-1,i+1))/2
430 )
431 (!closed && (i==0 || i==len(path)-1)) ? [0] : // Force zeros at ends for non-closed
432 parm[i]==0 ? [0] : // If no rounding requested then don't try to compute parameters
433 assert(is_def(angle), str("Repeated point in path at index ",i," with nonzero rounding"))
434 assert(!approx(angle,0), closed && i==0 ? "Closing the path causes it to turn back on itself at the end" :
435 str("Path turns back on itself at index ",i," with nonzero rounding"))
436 (method=="chamfer" && measure=="joint")? [parm[i]] :
437 (method=="chamfer" && measure=="cut") ? [parm[i]/cos(angle)] :
438 (method=="chamfer" && measure=="width") ? [parm[i]/sin(angle)/2] :
439 (method=="smooth" && measure=="joint") ? [parm[i],k[i]] :
440 (method=="smooth" && measure=="cut") ? [8*parm[i]/cos(angle)/(1+4*k[i]),k[i]] :
441 (method=="circle" && measure=="radius")? [parm[i]/tan(angle), parm[i]] :
442 (method=="circle" && measure=="joint") ? [parm[i], parm[i]*tan(angle)] :
443 /*(method=="circle" && measure=="cut")*/ approx(angle,90) ? [INF] :
444 let( circ_radius = parm[i] / (1/sin(angle) - 1))
445 [circ_radius/tan(angle), circ_radius],
446 ],
447 lengths = [for(i=[0:1:len(path)]) norm(select(path,i)-select(path,i-1))],
448 scalefactors = [
449 for(i=[0:1:len(path)-1])
450 if (closed || (i!=0 && i!=len(path)-1))
451 min(
452 lengths[i]/(select(dk,i-1)[0]+dk[i][0]),
453 lengths[i+1]/(dk[i][0]+select(dk,i+1)[0])
454 )
455 ],
456 dummy = verbose ? echo("Roundover scale factors:",scalefactors) : 0
457 )
458 assert(min(scalefactors)>=1,str("Roundovers are too big for the path. If you multitply them by this vector they should fit: ",scalefactors))
459 // duplicates are introduced when roundings fully consume a segment, so remove them
460 deduplicate([
461 for(i=[0:1:len(path)-1]) each
462 (dk[i][0] == 0)? [path[i]] :
463 (method=="smooth")? _bezcorner(select(path,i-1,i+1), dk[i]) :
464 (method=="chamfer") ? _chamfcorner(select(path,i-1,i+1), dk[i]) :
465 _circlecorner(select(path,i-1,i+1), dk[i])
466 ]);
467
468// Computes the continuous curvature control points for a corner when given as
469// input three points in a list defining the corner. The points must be
470// equidistant from each other to produce the continuous curvature result.
471// The output control points will include the 3 input points plus two
472// interpolated points.
473//
474// k is the curvature parameter, ranging from 0 for very slow transition
475// up to 1 for a sharp transition that doesn't have continuous curvature any more
476function _smooth_bez_fill(points,k) = [
477 points[0],
478 lerp(points[1],points[0],k),
479 points[1],
480 lerp(points[1],points[2],k),
481 points[2],
482];
483
484// Computes the points of a continuous curvature roundover given as input
485// the list of 3 points defining the corner and a parameter specification
486//
487// If parm is a scalar then it is treated as the curvature and the control
488// points are calculated using _smooth_bez_fill. Otherwise, parm is assumed
489// to be a pair [d,k] where d is the length of the curve. The length is
490// calculated from the input point list and the control point list will not
491// necessarily include points[0] or points[2] on its output.
492//
493// The number of points output is $fn if it is set. Otherwise $fs is used
494// to calculate the point count.
495
496function _bezcorner(points, parm) =
497 let(
498 P = is_list(parm)?
499 let(
500 d = parm[0],
501 k = parm[1],
502 prev = unit(points[0]-points[1]),
503 next = unit(points[2]-points[1])
504 ) [
505 points[1]+d*prev,
506 points[1]+k*d*prev,
507 points[1],
508 points[1]+k*d*next,
509 points[1]+d*next
510 ] : _smooth_bez_fill(points,parm),
511 N = max(3,$fn>0 ?$fn : ceil(bezier_length(P)/$fs))
512 )
513 bezier_curve(P,N,endpoint=true);
514
515function _chamfcorner(points, parm) =
516 let(
517 d = parm[0],
518 prev = unit(points[0]-points[1]),
519 next = unit(points[2]-points[1])
520 )
521 [points[1]+prev*d, points[1]+next*d];
522
523function _circlecorner(points, parm) =
524 let(
525 angle = vector_angle(points)/2,
526 d = parm[0],
527 r = parm[1],
528 prev = unit(points[0]-points[1]),
529 next = unit(points[2]-points[1])
530 )
531 approx(angle,90) ? [points[1]+prev*d, points[1]+next*d] :
532 let(
533 center = r/sin(angle) * unit(prev+next)+points[1],
534 start = points[1]+prev*d,
535 end = points[1]+next*d
536 ) // 90-angle is half the angle of the circular arc
537 arc(max(3,ceil((90-angle)/180*segs(r))), cp=center, points=[start,end]);
538
539
540// Used by offset_sweep and convex_offset_extrude.
541// Produce edge profile curve from the edge specification
542// z_dir is the direction multiplier (1 to build up, -1 to build down)
543function _rounding_offsets(edgespec,z_dir=1) =
544 let(
545 edgetype = struct_val(edgespec, "type"),
546 extra = struct_val(edgespec,"extra"),
547 N = struct_val(edgespec, "steps"),
548 r = struct_val(edgespec,"r"),
549 cut = struct_val(edgespec,"cut"),
550 k = struct_val(edgespec,"k"),
551 radius = in_list(edgetype,["circle","teardrop"])
552 ? (is_def(cut) ? cut/(sqrt(2)-1) : r)
553 :edgetype=="chamfer"
554 ? (is_def(cut) ? sqrt(2)*cut : r)
555 : undef,
556 chamf_angle = struct_val(edgespec, "angle"),
557 cheight = struct_val(edgespec, "chamfer_height"),
558 cwidth = struct_val(edgespec, "chamfer_width"),
559 chamf_width = first_defined([!all_defined([cut,chamf_angle]) ? undef : cut/cos(chamf_angle),
560 cwidth,
561 !all_defined([cheight,chamf_angle]) ? undef : cheight*tan(chamf_angle)]),
562 chamf_height = first_defined([
563 !all_defined([cut,chamf_angle]) ? undef : cut/sin(chamf_angle),
564 cheight,
565 !all_defined([cwidth, chamf_angle]) ? undef : cwidth/tan(chamf_angle)]),
566 joint = first_defined([
567 struct_val(edgespec,"joint"),
568 all_defined([cut,k]) ? 16*cut/sqrt(2)/(1+4*k) : undef
569 ]),
570 points = struct_val(edgespec, "points"),
571 argsOK = in_list(edgetype,["circle","teardrop"])? is_def(radius) :
572 edgetype == "chamfer"? chamf_angle>0 && chamf_angle<90 && num_defined([chamf_height,chamf_width])==2 :
573 edgetype == "smooth"? num_defined([k,joint])==2 :
574 edgetype == "profile"? points[0]==[0,0] :
575 false
576 )
577 assert(argsOK,str("Invalid specification with type ",edgetype))
578 let(
579 offsets =
580 edgetype == "profile"? scale([-1,z_dir], p=list_tail(points)) :
581 edgetype == "chamfer"? chamf_width==0 && chamf_height==0? [] : [[-chamf_width,z_dir*abs(chamf_height)]] :
582 edgetype == "teardrop"? (
583 radius==0? [] : concat(
584 [for(i=[1:N]) [radius*(cos(i*45/N)-1),z_dir*abs(radius)* sin(i*45/N)]],
585 [[-2*radius*(1-sqrt(2)/2), z_dir*abs(radius)]]
586 )
587 ) :
588 edgetype == "circle"? radius==0? [] : [for(i=[1:N]) [radius*(cos(i*90/N)-1), z_dir*abs(radius)*sin(i*90/N)]] :
589 /* smooth */ joint==0 ? [] :
590 list_tail(
591 _bezcorner([[0,0],[0,z_dir*abs(joint)],[-joint,z_dir*abs(joint)]], k, $fn=N+2)
592 )
593 )
594 quant(extra > 0 && len(offsets)>0 ? concat(offsets, [last(offsets)+[0,z_dir*extra]]) : offsets, 1/1024);
595
596
597
598// Function: smooth_path()
599// Synopsis: Create smoothed path that passes through all the points of a given path.
600// SynTags: Path
601// Topics: Rounding, Paths
602// See Also: round_corners(), smooth_path(), path_join(), offset_stroke()
603// Usage:
604// smoothed = smooth_path(path, [tangents], [size=|relsize=], [splinesteps=], [closed=], [uniform=]);
605// Description:
606// Smooths the input path using a cubic spline. Every segment of the path will be replaced by a cubic curve
607// with `splinesteps` points. The cubic interpolation will pass through every input point on the path
608// and will match the tangents at every point. If you do not specify tangents they will be computed using
609// path_tangents with uniform=false by default. Note that setting uniform to true with non-uniform
610// sampling may be desirable in some cases but tends to produces curves that overshoot the point on the path.
611// .
612// The size or relsize parameter determines how far the curve can bend away from
613// the input path. In the case where the curve has a single hump, the size specifies the exact distance
614// between the specified path and the curve. If you give relsize then it is relative to the segment
615// length (e.g. 0.05 means 5% of the segment length). In 2d when the spline may make an S-curve,
616// in which case the size parameter specifies the sum of the deviations of the two peaks of the curve. In 3-space
617// the bezier curve may have three extrema: two maxima and one minimum. In this case the size specifies
618// the sum of the maxima minus the minimum. At a given segment there is a maximum size: if your size
619// value is too large it will be rounded down. See also path_to_bezpath().
620// Arguments:
621// path = path to smooth
622// tangents = tangents constraining curve direction at each point. Default: computed automatically
623// ---
624// relsize = relative size specification for the curve, a number or vector. Default: 0.1
625// size = absolute size specification for the curve, a number or vector
626// uniform = set to true to compute tangents with uniform=true. Default: false
627// closed = true if the curve is closed. Default: false.
628// Example(2D): Original path in green, smoothed path in yellow:
629// color("green")stroke(square(4), width=0.1);
630// stroke(smooth_path(square(4),size=0.4), width=0.1);
631// Example(2D): Closing the path changes the end tangents
632// polygon(smooth_path(square(4),size=0.4,closed=true));
633// Example(2D): Turning on uniform tangent calculation also changes the end derivatives:
634// color("green")stroke(square(4), width=0.1);
635// stroke(smooth_path(square(4),size=0.4,uniform=true),
636// width=0.1);
637// Example(2D): Here's a wide rectangle. Using size means all edges bulge the same amount, regardless of their length.
638// color("green")
639// stroke(square([10,4]), closed=true, width=0.1);
640// stroke(smooth_path(square([10,4]),size=1,closed=true),
641// width=0.1);
642// Example(2D): With relsize the bulge is proportional to the side length.
643// color("green")stroke(square([10,4]), closed=true, width=0.1);
644// stroke(smooth_path(square([10,4]),relsize=0.1,closed=true),
645// width=0.1);
646// Example(2D): Settting uniform to true biases the tangents to aline more with the line sides
647// color("green")
648// stroke(square([10,4]), closed=true, width=0.1);
649// stroke(smooth_path(square([10,4]),uniform=true,
650// relsize=0.1,closed=true),
651// width=0.1);
652// Example(2D): A more interesting shape:
653// path = [[0,0], [4,0], [7,14], [-3,12]];
654// polygon(smooth_path(path,size=1,closed=true));
655// Example(2D): Here's the square again with less smoothing.
656// polygon(smooth_path(square(4), size=.25,closed=true));
657// Example(2D): Here's the square with a size that's too big to achieve, so you get the maximum possible curve:
658// color("green")stroke(square(4), width=0.1,closed=true);
659// stroke(smooth_path(square(4), size=4, closed=true),
660// closed=true,width=.1);
661// Example(2D): You can alter the shape of the curve by specifying your own arbitrary tangent values
662// polygon(smooth_path(square(4),
663// tangents=1.25*[[-2,-1], [-4,1], [1,2], [6,-1]],
664// size=0.4,closed=true));
665// Example(2D): Or you can give a different size for each segment
666// polygon(smooth_path(square(4),size = [.4, .05, 1, .3],
667// closed=true));
668// Example(FlatSpin,VPD=35,VPT=[4.5,4.5,1]): Works on 3d paths as well
669// path = [[0,0,0],[3,3,2],[6,0,1],[9,9,0]];
670// stroke(smooth_path(path,relsize=.1),width=.3);
671// Example(2D): This shows the type of overshoot that can occur with uniform=true. You can produce overshoots like this if you supply a tangent that is difficult to connect to the adjacent points
672// pts = [[-3.3, 1.7], [-3.7, -2.2], [3.8, -4.8], [-0.9, -2.4]];
673// stroke(smooth_path(pts, uniform=true, relsize=0.1),width=.1);
674// color("red")move_copies(pts)circle(r=.15,$fn=12);
675// Example(2D): With the default of uniform false no overshoot occurs. Note that the shape of the curve is quite different.
676// pts = [[-3.3, 1.7], [-3.7, -2.2], [3.8, -4.8], [-0.9, -2.4]];
677// stroke(smooth_path(pts, uniform=false, relsize=0.1),width=.1);
678// color("red")move_copies(pts)circle(r=.15,$fn=12);
679module smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed=false) {no_module();}
680function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed) =
681 is_1region(path) ? smooth_path(path[0], tangents, size, relsize, splinesteps, uniform, default(closed,true)) :
682 let (
683 bez = path_to_bezpath(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=default(closed,false)),
684 smoothed = bezpath_curve(bez,splinesteps=splinesteps)
685 )
686 closed ? list_unwrap(smoothed) : smoothed;
687
688
689function _scalar_to_vector(value,length,varname) =
690 is_vector(value)
691 ? assert(len(value)==length, str(varname," must be length ",length))
692 value
693 : assert(is_num(value), str(varname, " must be a numerical value"))
694 repeat(value, length);
695
696
697// Function: path_join()
698// Synopsis: Join paths end to end with optional rounding.
699// SynTags: Path
700// Topics: Rounding, Paths
701// See Also: round_corners(), smooth_path(), path_join(), offset_stroke()
702// Usage:
703// joined_path = path_join(paths, [joint], [k=], [relocate=], [closed=]);
704// Description:
705// Connect a sequence of paths together into a single path with optional continuous curvature rounding
706// applied at the joints. By default the first path is taken as specified and subsequent paths are
707// translated into position so that each path starts where the previous path ended.
708// If you set relocate to false then this relocation is skipped.
709// You specify rounding using the `joint` parameter, which specifies the distance away from the corner
710// where the roundover should start. The path_join function may remove many path points to cut the path
711// back by the joint length. Rounding is using continous curvature 4th order bezier splines and
712// the parameter `k` specifies how smooth the curvature match is. This parameter ranges from 0 to 1 with
713// a default of 0.5. Use a larger k value to get a curve that is bigger for the same joint value. When
714// k=1 the curve may be similar to a circle if your curves are symmetric. As the path is built up, the joint
715// parameter applies to the growing path, so if you pick a large joint parameter it may interact with the
716// previous path sections. See [Types of Roundover](rounding.scad#subsection-types-of-roundover) for more details
717// on continuous curvature rounding.
718// .
719// The rounding is created by extending the two clipped paths to define a corner point. If the extensions of
720// the paths do not intersect, the function issues an error. When closed=true the final path should actually close
721// the shape, repeating the starting point of the shape. If it does not, then the rounding will fill the gap.
722// .
723// The number of segments in the roundovers is set based on $fn and $fs. If you use $fn it specifies the number of
724// segments in the roundover, regardless of its angular extent.
725// Arguments:
726// paths = list of paths to join
727// joint = joint distance, either a number, a pair (giving the previous and next joint distance) or a list of numbers and pairs. Default: 0
728// ---
729// k = curvature parameter, either a number or vector. Default: 0.5
730// relocate = set to false to prevent paths from being arranged tail to head. Default: true
731// closed = set to true to round the junction between the last and first paths. Default: false
732// Example(2D): Connection of 3 simple paths.
733// horiz = [[0,0],[10,0]];
734// vert = [[0,0],[0,10]];
735// stroke(path_join([horiz, vert, -horiz]));
736// Example(2D): Adding curvature with joint of 3
737// horiz = [[0,0],[10,0]];
738// vert = [[0,0],[0,10]];
739// stroke(path_join([horiz, vert, -horiz],joint=3,$fn=16));
740// Example(2D): Setting k=1 increases the amount of curvature
741// horiz = [[0,0],[10,0]];
742// vert = [[0,0],[0,10]];
743// stroke(path_join([horiz, vert, -horiz],joint=3,k=1,$fn=16));
744// Example(2D): Specifying pairs of joint values at a path joint creates an asymmetric curve
745// horiz = [[0,0],[10,0]];
746// vert = [[0,0],[0,10]];
747// stroke(path_join([horiz, vert, -horiz],
748// joint=[[4,1],[1,4]],$fn=16),width=.3);
749// Example(2D): A closed square
750// horiz = [[0,0],[10,0]];
751// vert = [[0,0],[0,10]];
752// stroke(path_join([horiz, vert, -horiz, -vert],
753// joint=3,k=1,closed=true,$fn=16),closed=true);
754// Example(2D): Different curve at each corner by changing the joint size
755// horiz = [[0,0],[10,0]];
756// vert = [[0,0],[0,10]];
757// stroke(path_join([horiz, vert, -horiz, -vert],
758// joint=[3,0,1,2],k=1,closed=true,$fn=16),
759// closed=true,width=0.4);
760// Example(2D): Different curve at each corner by changing the curvature parameter. Note that k=0 still gives a small curve, unlike joint=0 which gives a sharp corner.
761// horiz = [[0,0],[10,0]];
762// vert = [[0,0],[0,10]];
763// stroke(path_join([horiz, vert, -horiz, -vert],joint=3,
764// k=[1,.5,0,.7],closed=true,$fn=16),
765// closed=true,width=0.4);
766// Example(2D): Joint value of 7 is larger than half the square so curves interfere with each other, which breaks symmetry because they are computed sequentially
767// horiz = [[0,0],[10,0]];
768// vert = [[0,0],[0,10]];
769// stroke(path_join([horiz, vert, -horiz, -vert],joint=7,
770// k=.4,closed=true,$fn=16),
771// closed=true);
772// Example(2D): Unlike round_corners, we can add curves onto curves.
773// $fn=64;
774// myarc = arc(width=20, thickness=5 );
775// stroke(path_join(repeat(myarc,3), joint=4));
776// Example(2D): Here we make a closed shape from two arcs and round the sharp tips
777// arc1 = arc(width=20, thickness=4,$fn=75);
778// arc2 = reverse(arc(width=20, thickness=2,$fn=75));
779// // Without rounding
780// stroke(path_join([arc1,arc2]),width=.3);
781// // With rounding
782// color("red")stroke(path_join([arc1,arc2], 3,k=1,closed=true),
783// width=.3,closed=true,$fn=12);
784// Example(2D): Combining arcs with segments
785// arc1 = arc(width=20, thickness=4,$fn=75);
786// arc2 = reverse(arc(width=20, thickness=2,$fn=75));
787// vpath = [[0,0],[0,-5]];
788// stroke(path_join([arc1,vpath,arc2,reverse(vpath)]),width=.2);
789// color("red")stroke(path_join([arc1,vpath,arc2,reverse(vpath)],
790// [1,2,2,1],k=1,closed=true),
791// width=.2,closed=true,$fn=12);
792// Example(2D): Here relocation is off. We have three segments (in yellow) and add the curves to the segments. Notice that joint zero still produces a curve because it refers to the endpoints of the supplied paths.
793// p1 = [[0,0],[2,0]];
794// p2 = [[3,1],[1,3]];
795// p3 = [[0,3],[-1,1]];
796// color("red")stroke(
797// path_join([p1,p2,p3], joint=0, relocate=false,
798// closed=true),
799// width=.3,$fn=48);
800// for(x=[p1,p2,p3]) stroke(x,width=.3);
801// Example(2D): If you specify closed=true when the last path doesn't meet the first one then it is similar to using relocate=false: the function tries to close the path using a curve. In the example below, this results in a long curve to the left, when given the unclosed three segments as input. Note that if the segments are parallel the function fails with an error. The extension of the curves must intersect in a corner for the rounding to be well-defined. To get a normal rounding of the closed shape, you must include a fourth path, the last segment that closes the shape.
802// horiz = [[0,0],[10,0]];
803// vert = [[0,0],[0,10]];
804// h2 = [[0,-3],[10,0]];
805// color("red")stroke(
806// path_join([horiz, vert, -h2],closed=true,
807// joint=3,$fn=25),
808// closed=true,width=.5);
809// stroke(path_join([horiz, vert, -h2]),width=.3);
810// Example(2D): With a single path with closed=true the start and end junction is rounded.
811// tri = regular_ngon(n=3, r=7);
812// stroke(path_join([tri], joint=3,closed=true,$fn=12),
813// closed=true,width=.5);
814module path_join(paths,joint=0,k=0.5,relocate=true,closed=false) { no_module();}
815function path_join(paths,joint=0,k=0.5,relocate=true,closed=false)=
816 assert(is_list(paths),"Input paths must be a list of paths")
817 let(
818 paths = [for(i=idx(paths)) force_path(paths[i],str("paths[",i,"]"))],
819 badpath = [for(j=idx(paths)) if (!is_path(paths[j])) j]
820 )
821 assert(badpath==[], str("Entries in paths are not valid paths: ",badpath))
822 len(paths)==0 ? [] :
823 len(paths)==1 && !closed ? paths[0] :
824 let(
825 paths = !closed || len(paths)>1
826 ? paths
827 : [list_wrap(paths[0])],
828 N = len(paths) + (closed?0:-1),
829 k = _scalar_to_vector(k,N),
830 repjoint = is_num(joint) || (is_vector(joint,2) && len(paths)!=3),
831 joint = repjoint ? repeat(joint,N) : joint
832 )
833 assert(all_nonnegative(k), "k must be nonnegative")
834 assert(len(joint)==N,str("Input joint must be scalar or length ",N))
835 let(
836 bad_j = [for(j=idx(joint)) if (!is_num(joint[j]) && !is_vector(joint[j],2)) j]
837 )
838 assert(bad_j==[], str("Invalid joint values at indices ",bad_j))
839 let(result=_path_join(paths,joint,k, relocate=relocate, closed=closed))
840 closed ? list_unwrap(result) : result;
841
842function _path_join(paths,joint,k=0.5,i=0,result=[],relocate=true,closed=false) =
843 let(
844 result = result==[] ? paths[0] : result,
845 loop = i==len(paths)-1,
846 revresult = reverse(result),
847 nextpath = loop ? result
848 : relocate ? move(revresult[0]-paths[i+1][0], p=paths[i+1])
849 : paths[i+1],
850 d_first = is_vector(joint[i]) ? joint[i][0] : joint[i],
851 d_next = is_vector(joint[i]) ? joint[i][1] : joint[i]
852 )
853 assert(d_first>=0 && d_next>=0, str("Joint value negative when adding path ",i+1))
854
855 assert(d_first<path_length(revresult),str("Path ",i," is too short for specified cut distance ",d_first))
856 assert(d_next<path_length(nextpath), str("Path ",i+1," is too short for specified cut distance ",d_next))
857 let(
858 firstcut = path_cut_points(revresult, d_first, direction=true),
859 nextcut = path_cut_points(nextpath, d_next, direction=true)
860 )
861 assert(!loop || nextcut[1] < len(revresult)-1-firstcut[1], "Path is too short to close the loop")
862 let(
863 first_dir=firstcut[2],
864 next_dir=nextcut[2],
865 corner = approx(firstcut[0],nextcut[0]) ? firstcut[0]
866 : line_intersection([firstcut[0], firstcut[0]-first_dir], [nextcut[0], nextcut[0]-next_dir],RAY,RAY)
867 )
868 assert(is_def(corner), str("Curve directions at cut points don't intersect in a corner when ",
869 loop?"closing the path":str("adding path ",i+1)))
870 let(
871 bezpts = _smooth_bez_fill([firstcut[0], corner, nextcut[0]],k[i]),
872 N = max(3,$fn>0 ?$fn : ceil(bezier_length(bezpts)/$fs)),
873 bezpath = approx(firstcut[0],corner) && approx(corner,nextcut[0])
874 ? []
875 : bezier_curve(bezpts,N),
876 new_result = [each select(result,loop?nextcut[1]:0,len(revresult)-1-firstcut[1]),
877 each bezpath,
878 nextcut[0],
879 if (!loop) each list_tail(nextpath,nextcut[1])
880 ]
881 )
882 i==len(paths)-(closed?1:2)
883 ? new_result
884 : _path_join(paths,joint,k,i+1,new_result, relocate,closed);
885
886
887
888// Function&Module: offset_stroke()
889// Synopsis: Draws a line along a path with options to specify angles and roundings at the ends.
890// SynTags: Path, Region
891// Topics: Rounding, Paths
892// See Also: round_corners(), smooth_path(), path_join(), offset_stroke()
893// Usage: as module
894// offset_stroke(path, [width], [rounded=], [chamfer=], [start=], [end=], [check_valid=], [quality=], [closed=],...) [ATTACHMENTS];
895// Usage: as function
896// path = offset_stroke(path, [width], closed=false, [rounded=], [chamfer=], [start=], [end=], [check_valid=], [quality=],...);
897// region = offset_stroke(path, [width], closed=true, [rounded=], [chamfer=], [start=], [end=], [check_valid=], [quality=],...);
898// Description:
899// Uses `offset()` to compute a stroke for the input path. Unlike `stroke`, the result does not need to be
900// centered on the input path. The corners can be rounded, pointed, or chamfered, and you can make the ends
901// rounded, flat or pointed with the `start` and `end` parameters.
902// .
903// The `check_valid` and `quality` parameters are passed through to `offset()`
904// .
905// If `width` is a scalar then the output will be a centered stroke of the specified width. If width
906// is a list of two values then those two values will define the stroke side positions relative to the center line, where
907// as with offset(), the shift is to the left for open paths and outward for closed paths. For example,
908// setting `width` to `[0,1]` will create a stroke of width 1 that extends entirely to the left of the input, and and [-4,-6]
909// will create a stroke of width 2 offset 4 units to the right of the input path.
910// .
911// If closed==false then the function form will return a path. If closed==true then it will return a region. The `start` and
912// `end` parameters are forbidden for closed paths.
913// .
914// Three simple end treatments are supported, "flat" (the default), "round" and "pointed". The "flat" treatment
915// cuts off the ends perpendicular to the path and the "round" treatment applies a semicircle to the end. The
916// "pointed" end treatment caps the stroke with a centered triangle that has 45 degree angles on each side.
917// .
918// More complex end treatments are available through parameter lists with helper functions to ease parameter passing. The parameter list
919// keywords are
920// - "for" : must appear first in the list and have the value "offset_stroke"
921// - "type": the type of end treatment, one of "shifted_point", "roundover", or "flat"
922// - "angle": relative angle (relative to the path)
923// - "abs_angle": absolute angle (angle relative to x-axis)
924// - "cut": cut distance for roundovers, a single value to round both corners identically or a list of two values for the two corners. Negative values round outward.
925// - "k": curvature smoothness parameter for roundovers, default 0.75
926// .
927// Function helpers for defining ends, prefixed by "os" for offset_stroke, are:
928// - os_flat(angle|absangle): specify a flat end either relative to the path or relative to the x-axis
929// - os_pointed(dist, [loc]): specify a pointed tip where the point is distance `loc` from the centerline (positive is the left direction as for offset), and `dist` is the distance from the path end to the point tip. The default value for `loc` is zero (the center). You must specify `dist` when using this option.
930// - os_round(cut, [angle|absangle], [k]). Rounded ends with the specified cut distance, based on the specified angle or absolute angle. The `k` parameter is the smoothness parameter for continuous curvature rounding. See [Types of Roundover](rounding.scad#subsection-types-of-roundover) for more details on
931// continuous curvature rounding.
932// .
933// Note that `offset_stroke()` will attempt to apply roundovers and angles at the ends even when it means deleting segments of the stroke, unlike round_corners which only works on a segment adjacent to a corner. If you specify an overly extreme angle it will fail to find an intersection with the stroke and display an error. When you specify an angle the end segment is rotated around the center of the stroke and the last segment of the stroke one one side is extended to the corner.
934// .
935// The `$fn` and `$fs` variables are used in the usual way to determine the number of segments for roundings produced by the offset
936// invocations and roundings produced by the semi-circular "round" end treatment. The os_round() end treatment
937// uses a bezier curve, and will produce segments of approximate length `$fs` or it will produce `$fn` segments.
938// (This means that even a quarter circle will have `$fn` segments, unlike the usual case where it would have `$fn/4` segments.)
939// Arguments:
940// path = 2d path that defines the stroke
941// width = width of the stroke, a scalar or a vector of 2 values giving the offset from the path. Default: 1
942// ---
943// rounded = set to true to use rounded offsets, false to use sharp (delta) offsets. Default: true
944// chamfer = set to true to use chamfers when `rounded=false`. Default: false
945// start = end treatment for the start of the stroke when closed=false. See above for details. Default: "flat"
946// end = end treatment for the end of the stroke when closed=false. See above for details. Default: "flat"
947// check_valid = passed to offset(). Default: true
948// quality = passed to offset(). Default: 1
949// closed = true if the curve is closed, false otherwise. Default: false
950// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
951// spin = Rotate this many degrees after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
952// cp = Centerpoint for determining intersection anchors or centering the shape. Determintes the base of the anchor vector. Can be "centroid", "mean", "box" or a 2D point. Default: "centroid"
953// atype = Set to "hull" or "intersect" to select anchor type. Default: "hull"
954// Named Anchors:
955// "origin" = The native position of the region.
956// Anchor Types:
957// "hull" = Anchors to the virtual convex hull of the region.
958// "intersect" = Anchors to the outer edge of the region.
959// Example(2D): Basic examples illustrating flat, round, and pointed ends, on a finely sampled arc and a path made from 3 segments.
960// arc = arc(points=[[1,1],[3,4],[6,3]],n=50);
961// path = [[0,0],[6,2],[9,7],[8,10]];
962// xdistribute(spacing=10){
963// offset_stroke(path, width = 2);
964// offset_stroke(path, start="round", end="round", width = 2, $fn=32);
965// offset_stroke(path, start="pointed", end="pointed", width = 2);
966// }
967// fwd(10) xdistribute(spacing=10){
968// offset_stroke(arc, width = 2);
969// offset_stroke(arc, start="round", end="round", width = 2, $fn=32);
970// offset_stroke(arc, start="pointed", end="pointed", width = 2);
971// }
972// Example(2D): The effect of the `rounded` and `chamfer` options is most evident at sharp corners. This only affects the middle of the path, not the ends.
973// sharppath = [[0,0], [1.5,5], [3,0]];
974// xdistribute(spacing=5){
975// offset_stroke(sharppath, $fn=16);
976// offset_stroke(sharppath, rounded=false);
977// offset_stroke(sharppath, rounded=false, chamfer=true);
978// }
979// Example(2D): When closed is enabled all the corners are affected by those options.
980// sharppath = [[0,0], [1.5,5], [3,0]];
981// xdistribute(spacing=5){
982// offset_stroke(sharppath,closed=true, $fn=16);
983// offset_stroke(sharppath, rounded=false, closed=true);
984// offset_stroke(sharppath, rounded=false, chamfer=true,
985// closed=true);
986// }
987// Example(2D): The left stroke uses flat ends with a relative angle of zero. The right hand one uses flat ends with an absolute angle of zero, so the ends are parallel to the x-axis.
988// path = [[0,0],[6,2],[9,7],[8,10]];
989// offset_stroke(path, start=os_flat(angle=0), end=os_flat(angle=0));
990// right(5)
991// offset_stroke(path, start=os_flat(abs_angle=0), end=os_flat(abs_angle=0));
992// Example(2D): With continuous sampling the end treatment can remove segments or extend the last segment linearly, as shown here. Again the left side uses relative angle flat ends and the right hand example uses absolute angle.
993// arc = arc(points=[[4,0],[3,4],[6,3]],n=50);
994// offset_stroke(arc, start=os_flat(angle=45), end=os_flat(angle=45));
995// right(5)
996// offset_stroke(arc, start=os_flat(abs_angle=45), end=os_flat(abs_angle=45));
997// Example(2D): The os_pointed() end treatment allows adjustment of the point tip, as shown here. The width is 2 so a location of 1 is at the edge.
998// arc = arc(points=[[1,1],[3,4],[6,3]],n=50);
999// offset_stroke(arc, width=2, start=os_pointed(loc=1,dist=3),end=os_pointed(loc=1,dist=3));
1000// right(10)
1001// offset_stroke(arc, width=2, start=os_pointed(dist=4),end=os_pointed(dist=-1));
1002// fwd(7)
1003// offset_stroke(arc, width=2, start=os_pointed(loc=2,dist=2),end=os_pointed(loc=.5,dist=-1));
1004// Example(2D): The os_round() end treatment adds roundovers to the end corners by specifying the `cut` parameter. In the first example, the cut parameter is the same at each corner. The bezier smoothness parameter `k` is given to allow a larger cut. In the second example, each corner is given a different roundover, including zero for no rounding at all. The red shows the same strokes without the roundover.
1005// $fn=36;
1006// arc = arc(points=[[1,1],[3,4],[6,3]],n=50);
1007// path = [[0,0],[6,2],[9,7],[8,10]];
1008// offset_stroke(path, width=2, rounded=false,start=os_round(angle=-20, cut=0.4,k=.9),
1009// end=os_round(angle=-35, cut=0.4,k=.9));
1010// color("red")down(.1)offset_stroke(path, width=2, rounded=false,start=os_flat(-20),
1011// end=os_flat(-35));
1012// right(9){
1013// offset_stroke(arc, width=2, rounded=false, start=os_round(cut=[.3,.6],angle=-45),
1014// end=os_round(angle=20,cut=[.6,0]));
1015// color("red")down(.1)offset_stroke(arc, width=2, rounded=false, start=os_flat(-45),
1016// end=os_flat(20));
1017// }
1018// Example(2D): Negative cut values produce a flaring end. Note how the absolute angle aligns the ends of the first example withi the axes. In the second example positive and negative cut values are combined. Note also that very different cuts are needed at the start end to produce a similar looking flare.
1019// arc = arc(points=[[1,1],[3,4],[6,3]],n=50);
1020// path = [[0,0],[6,2],[9,7],[8,10]];
1021// offset_stroke(path, width=2, rounded=false,start=os_round(cut=-1, abs_angle=90),
1022// end=os_round(cut=-0.5, abs_angle=0),$fn=36);
1023// right(10)
1024// offset_stroke(arc, width=2, rounded=false, start=os_round(cut=[-.75,-.2], angle=-45),
1025// end=os_round(cut=[-.2,.2], angle=20),$fn=36);
1026// Example(2D): Setting the width to a vector allows you to offset the stroke. Here with successive increasing offsets we create a set of parallel strokes
1027// path = [[0,0],[4,4],[8,4],[2,9],[10,10]];
1028// for(i=[0:.25:2])
1029// offset_stroke(path, rounded=false,width = [i,i+.08]);
1030// Example(2D): Setting rounded=true in the above example makes a very big difference in the result.
1031// path = [[0,0],[4,4],[8,4],[2,9],[10,10]];
1032// for(i=[0:.25:2])
1033// offset_stroke(path, rounded=true,width = [i,i+.08], $fn=36);
1034// Example(2D): In this example a spurious triangle appears. This results from overly enthusiastic validity checking. Turning validity checking off fixes it in this case.
1035// path = [[0,0],[4,4],[8,4],[2,9],[10,10]];
1036// offset_stroke(path, check_valid=true,rounded=false,
1037// width = [1.4, 1.5]);
1038// right(2)
1039// offset_stroke(path, check_valid=false,rounded=false,
1040// width = [1.4, 1.5]);
1041// Example(2D): But in this case, disabling the validity check produces an invalid result.
1042// path = [[0,0],[4,4],[8,4],[2,9],[10,10]];
1043// offset_stroke(path, check_valid=true,rounded=false,
1044// width = [1.9, 2]);
1045// translate([1,-0.25])
1046// offset_stroke(path, check_valid=false,rounded=false,
1047// width = [1.9, 2]);
1048// Example(2D): Self-intersecting paths are handled differently than with the `stroke()` module.
1049// $fn=16;
1050// path = turtle(["move",10,"left",144], repeat=4);
1051// stroke(path, closed=true);
1052// right(12)
1053// offset_stroke(path, width=1, closed=true);
1054function offset_stroke(path, width=1, rounded=true, start, end, check_valid=true, quality=1, chamfer=false, closed=false,
1055 atype="hull", anchor="origin", spin, cp="centroid") =
1056 let(path = force_path(path))
1057 assert(is_path(path,2),"path is not a 2d path")
1058 let(
1059 closedok = !closed || (is_undef(start) && is_undef(end)),
1060 start = default(start,"flat"),
1061 end = default(end,"flat")
1062 )
1063 assert(closedok, "Parameters `start` and `end` not allowed with closed path")
1064 let(
1065 start = closed? [] : _parse_stroke_end(default(start,"flat"),"start"),
1066 end = closed? [] : _parse_stroke_end(default(end,"flat"),"end"),
1067 width = is_list(width)? reverse(sort(width)) : [1,-1]*width/2,
1068 left_r = !rounded? undef : width[0],
1069 left_delta = rounded? undef : width[0],
1070 right_r = !rounded? undef : width[1],
1071 right_delta = rounded? undef : width[1],
1072 left_path = offset(
1073 path, delta=left_delta, r=left_r, closed=closed,
1074 check_valid=check_valid, quality=quality,
1075 chamfer=chamfer
1076 ),
1077 right_path = offset(
1078 path, delta=right_delta, r=right_r, closed=closed,
1079 check_valid=check_valid, quality=quality,
1080 chamfer=chamfer
1081 )
1082 )
1083 closed? let(pts = [left_path, right_path])
1084 reorient(anchor=anchor, spin=spin, two_d=true, region=pts, extent=atype=="hull", cp=cp, p=pts)
1085 :
1086 let(
1087 startpath = _stroke_end(width,left_path, right_path, start),
1088 endpath = _stroke_end(reverse(width),reverse(right_path), reverse(left_path),end),
1089 clipping_ok = startpath[1]+endpath[2]<=len(left_path) && startpath[2]+endpath[1]<=len(right_path)
1090 )
1091 assert(clipping_ok, "End treatment removed the whole stroke")
1092 let(
1093 pts = concat(
1094 slice(left_path,startpath[1],-1-endpath[2]),
1095 endpath[0],
1096 reverse(slice(right_path,startpath[2],-1-endpath[1])),
1097 startpath[0]
1098 )
1099 )
1100 reorient(anchor=anchor, spin=spin, two_d=true, path=pts, extent=atype=="hull", cp=cp, p=pts);
1101
1102function os_pointed(dist,loc=0) =
1103 assert(is_def(dist), "Must specify `dist`")
1104 [
1105 "for", "offset_stroke",
1106 "type", "shifted_point",
1107 "loc",loc,
1108 "dist",dist
1109 ];
1110
1111function os_round(cut, angle, abs_angle, k, r) =
1112 assert(is_undef(r), "Radius not supported for os_round with offset_stroke. (Did you mean os_circle for offset_sweep?)")
1113 let(
1114 acount = num_defined([angle,abs_angle]),
1115 use_angle = first_defined([angle,abs_angle,0])
1116 )
1117 assert(acount<2, "You must define only one of `angle` and `abs_angle`")
1118 assert(is_def(cut), "Parameter `cut` not defined.")
1119 [
1120 "for", "offset_stroke",
1121 "type", "roundover",
1122 "angle", use_angle,
1123 "absolute", is_def(abs_angle),
1124 "cut", is_vector(cut)? point2d(cut) : [cut,cut],
1125 "k", first_defined([k, 0.75])
1126 ];
1127
1128
1129function os_flat(angle, abs_angle) =
1130 let(
1131 acount = num_defined([angle,abs_angle]),
1132 use_angle = first_defined([angle,abs_angle,0])
1133 )
1134 assert(acount<2, "You must define only one of `angle` and `abs_angle`")
1135 [
1136 "for", "offset_stroke",
1137 "type", "flat",
1138 "angle", use_angle,
1139 "absolute", is_def(abs_angle)
1140 ];
1141
1142
1143
1144// Return angle in (-90,90] required to map line1 onto line2 (lines specified as lists of two points)
1145function angle_between_lines(line1,line2) =
1146 let(angle = atan2(det2([line1,line2]),line1*line2))
1147 angle > 90 ? angle-180 :
1148 angle <= -90 ? angle+180 :
1149 angle;
1150
1151
1152function _parse_stroke_end(spec,name) =
1153 is_string(spec)?
1154 assert(
1155 in_list(spec,["flat","round","pointed"]),
1156 str("Unknown \"",name,"\" string specification \"", spec,"\". Must be \"flat\", \"round\", or \"pointed\"")
1157 )
1158 [["type", spec]]
1159 : let(
1160 dummy = _struct_valid(spec,"offset_stroke",name)
1161 )
1162 struct_set([], spec);
1163
1164
1165function _stroke_end(width,left, right, spec) =
1166 let(
1167 type = struct_val(spec, "type"),
1168 user_angle = default(struct_val(spec, "angle"), 0),
1169 normal_seg = _normal_segment(right[0], left[0]),
1170 normal_pt = normal_seg[1],
1171 center = normal_seg[0],
1172 parallel_dir = unit(left[0]-right[0]),
1173 normal_dir = unit(normal_seg[1]-normal_seg[0]),
1174 width_dir = sign(width[0]-width[1])
1175 )
1176 type == "round"? [arc(points=[right[0],normal_pt,left[0]],n=ceil(segs(width/2)/2)),1,1] :
1177 type == "pointed"? [[normal_pt],0,0] :
1178 type == "shifted_point"? (
1179 let(shiftedcenter = center + width_dir * parallel_dir * struct_val(spec, "loc"))
1180 [[shiftedcenter+normal_dir*struct_val(spec, "dist")],0,0]
1181 ) :
1182 // Remaining types all support angled cutoff, so compute that
1183 assert(abs(user_angle)<=90, "End angle must be in [-90,90]")
1184 let(
1185 angle = struct_val(spec,"absolute")?
1186 angle_between_lines(left[0]-right[0],[cos(user_angle),sin(user_angle)]) :
1187 user_angle,
1188 endseg = [center, rot(p=[left[0]], angle, cp=center)[0]],
1189 intright = angle>0,
1190 pathclip = _path_line_intersection(intright? right : left, endseg),
1191 pathextend = line_intersection(endseg, select(intright? left:right,0,1))
1192 )
1193 type == "flat"? (
1194 intright?
1195 [[pathclip[0], pathextend], 1, pathclip[1]] :
1196 [[pathextend, pathclip[0]], pathclip[1],1]
1197 ) :
1198 type == "roundover"? (
1199 let(
1200 bez_k = struct_val(spec,"k"),
1201 cut = struct_val(spec,"cut"),
1202 cutleft = cut[0],
1203 cutright = cut[1],
1204 // Create updated paths taking into account clipping for end rotation
1205 newright = intright?
1206 concat([pathclip[0]],list_tail(right,pathclip[1])) :
1207 concat([pathextend],list_tail(right)),
1208 newleft = !intright?
1209 concat([pathclip[0]],list_tail(left,pathclip[1])) :
1210 concat([pathextend],list_tail(left)),
1211 // calculate corner angles, which are different when the cut is negative (outside corner)
1212 leftangle = cutleft>=0?
1213 vector_angle([newleft[1],newleft[0],newright[0]])/2 :
1214 90-vector_angle([newleft[1],newleft[0],newright[0]])/2,
1215 rightangle = cutright>=0?
1216 vector_angle([newright[1],newright[0],newleft[0]])/2 :
1217 90-vector_angle([newright[1],newright[0],newleft[0]])/2,
1218 jointleft = 8*cutleft/cos(leftangle)/(1+4*bez_k),
1219 jointright = 8*cutright/cos(rightangle)/(1+4*bez_k),
1220 pathcutleft = path_cut_points(newleft,abs(jointleft)),
1221 pathcutright = path_cut_points(newright,abs(jointright)),
1222 leftdelete = intright? pathcutleft[1] : pathcutleft[1] + pathclip[1] -1,
1223 rightdelete = intright? pathcutright[1] + pathclip[1] -1 : pathcutright[1],
1224 leftcorner = line_intersection([pathcutleft[0], newleft[pathcutleft[1]]], [newright[0],newleft[0]]),
1225 rightcorner = line_intersection([pathcutright[0], newright[pathcutright[1]]], [newright[0],newleft[0]]),
1226 roundover_fits = is_def(rightcorner) && is_def(leftcorner) &&
1227 jointleft+jointright < norm(rightcorner-leftcorner)
1228 )
1229 assert(roundover_fits,"Roundover too large to fit")
1230 let(
1231 angled_dir = unit(newleft[0]-newright[0]),
1232 nPleft = [
1233 leftcorner - jointleft*angled_dir,
1234 leftcorner,
1235 pathcutleft[0]
1236 ],
1237 nPright = [
1238 pathcutright[0],
1239 rightcorner,
1240 rightcorner + jointright*angled_dir
1241 ],
1242 leftcurve = _bezcorner(nPleft, bez_k),
1243 rightcurve = _bezcorner(nPright, bez_k)
1244 )
1245 [concat(rightcurve, leftcurve), leftdelete, rightdelete]
1246 ) : [[],0,0]; // This case shouldn't occur
1247
1248// returns [intersection_pt, index of first point in path after the intersection]
1249function _path_line_intersection(path, line, ind=0) =
1250 ind==len(path)-1 ? undef :
1251 let(intersect=line_intersection(line, select(path,ind,ind+1),LINE,SEGMENT))
1252 // If it intersects the segment excluding it's final point, then we're done
1253 // The final point is treated as part of the next segment
1254 is_def(intersect) && intersect != path[ind+1]?
1255 [intersect, ind+1] :
1256 _path_line_intersection(path, line, ind+1);
1257
1258module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true, quality=1, chamfer=false, closed=false,
1259 atype="hull", anchor="origin", spin, cp="centroid")
1260{
1261 result = offset_stroke(
1262 path, width=width, rounded=rounded,
1263 start=start, end=end,
1264 check_valid=check_valid, quality=quality,
1265 chamfer=chamfer,
1266 closed=closed,anchor="origin"
1267 );
1268 region(result,atype=atype, anchor=anchor, spin=spin, cp=cp) children();
1269}
1270
1271
1272// Section: Three-Dimensional Rounding
1273
1274// Function&Module: offset_sweep()
1275// Synopsis: Make a solid from a polygon with offset that changes along its length.
1276// SynTags: Geom, VNF
1277// Topics: Rounding, Offsets
1278// See Also: convex_offset_extrude(), rounded_prism(), bent_cutout_mask(), join_prism(), linear_sweep()
1279// Usage: most common module arguments. See Arguments list below for more.
1280// offset_sweep(path, [height|length=|h=|l=], [bottom], [top], [offset=], [convexity=],...) [ATTACHMENTS];
1281// Usage: most common function arguments. See Arguments list below for more.
1282// vnf = offset_sweep(path, [height|length=|h=|l=], [bottom], [top], [offset=], ...);
1283// Description:
1284// Takes a 2d path as input and extrudes it upwards and/or downward. Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer. When invoked as a function returns a VNF; when invoked as a module produces geometry.
1285// Using the `top` and/or `bottom` arguments you can specify a sequence of offsets values, or you can use several built-in offset profiles that
1286// provide end treatments such as roundovers.
1287// The height of the resulting object can be specified using the `height` argument, in which case `height` must be larger than the combined height
1288// of the end treatments. If you omit `height` then the object height will be the height of just the top and bottom end treatments.
1289// .
1290// The path is shifted by `offset()` multiple times in sequence
1291// to produce the final shape (not multiple shifts from one parent), so coarse definition of the input path will degrade
1292// from the successive shifts. If the result seems rough or strange try increasing the number of points you use for
1293// your input. If you get unexpected corners in your result you may have forgotten to set `$fn` or `$fa` and `$fs`.
1294// Be aware that large numbers of points (especially when check_valid is true) can lead to lengthy run times. If your
1295// shape doesn't develop new corners from the offsetting you may be able to save a lot of time by setting `check_valid=false`. Be aware that
1296// disabling the validity check when it is needed can generate invalid polyhedra that will produce CGAL errors upon
1297// rendering. Such validity errors will also occur if you specify a self-intersecting shape.
1298// The offset profile is quantized to 1/1024 steps to avoid failures in offset() that can occur with very tiny offsets.
1299// .
1300// The build-in profiles are: circular rounding, teardrop rounding, continuous curvature rounding, and chamfer.
1301// Also note that when a rounding radius is negative the rounding will flare outwards. The easiest way to specify
1302// the profile is by using the profile helper functions. These functions take profile parameters, as well as some
1303// general settings and translate them into a profile specification, with error checking on your input. The description below
1304// describes the helper functions and the parameters specific to each function. Below that is a description of the generic
1305// settings that you can optionally use with all of the helper functions. For more details on the "cut" and "joint" rounding parameters, and
1306// on continuous curvature rounding, see [Types of Roundover](rounding.scad#subsection-types-of-roundover).
1307// .
1308// - profile: os_profile(points)
1309// Define the offset profile with a list of points. The first point must be [0,0] and the roundover should rise in the positive y direction, with positive x values for inward motion (standard roundover) and negative x values for flaring outward. If the y value ever decreases then you might create a self-intersecting polyhedron, which is invalid. Such invalid polyhedra will create cryptic assertion errors when you render your model and it is your responsibility to avoid creating them. Note that the starting point of the profile is the center of the extrusion. If you use a profile as the top it will rise upwards. If you use it as the bottom it will be inverted, and will go downward.
1310// - circle: os_circle(r|cut). Define circular rounding either by specifying the radius or cut distance.
1311// - smooth: os_smooth(cut|joint, [k]). Define continuous curvature rounding, with `cut` and `joint` as for round_corners. The k parameter controls how fast the curvature changes and should be between 0 and 1.
1312// - teardrop: os_teardrop(r|cut). Rounding using a 1/8 circle that then changes to a 45 degree chamfer. The chamfer is at the end, and enables the object to be 3d printed without support. The radius gives the radius of the circular part.
1313// - chamfer: os_chamfer([height], [width], [cut], [angle]). Chamfer the edge at desired angle or with desired height and width. You can specify height and width together and the angle will be ignored, or specify just one of height and width and the angle is used to determine the shape. Alternatively, specify "cut" along with angle to specify the cut back distance of the chamfer.
1314// - mask: os_mask(mask, [out]). Create a profile from one of the [2d masking shapes](shapes2d.scad#5-2d-masking-shapes). The `out` parameter specifies that the mask should flare outward (like crown molding or baseboard). This is set false by default.
1315// .
1316// The general settings that you can use with all of the helper functions are mostly used to control how offset_sweep() calls the offset() function.
1317// - extra: Add an extra vertical step of the specified height, to be used for intersections or differences. This extra step will extend the resulting object beyond the height you specify. It is ignored by anchoring. Default: 0
1318// - check_valid: passed to offset(). Default: true
1319// - quality: passed to offset(). Default: 1
1320// - steps: Number of vertical steps to use for the profile. (Not used by os_profile). Default: 16
1321// - offset: Select "round" (r=) or "delta" (delta=) offset types for offset. You can also choose "chamfer" but this leads to exponential growth in the number of vertices with the steps parameter. Default: "round"
1322// .
1323// Many of the arguments are described as setting "default" values because they establish settings which may be overridden by
1324// the top and bottom profile specifications.
1325// .
1326// You will generally want to use the above helper functions to generate the profiles.
1327// The profile specification is a list of pairs of keywords and values, e.g. ["for","offset_sweep","r",12, type, "circle"]. The keywords are
1328// - "for" - must appear first in the list and have the value "offset_sweep"
1329// - "type" - type of rounding to apply, one of "circle", "teardrop", "chamfer", "smooth", or "profile" (Default: "circle")
1330// - "r" - the radius of the roundover, which may be zero for no roundover, or negative to round or flare outward. Default: 0
1331// - "cut" - the cut distance for the roundover or chamfer, which may be negative for flares
1332// - "chamfer_width" - the width of a chamfer
1333// - "chamfer_height" - the height of a chamfer
1334// - "angle" - the chamfer angle, measured from the vertical (so zero is vertical, 90 is horizontal). Default: 45
1335// - "joint" - the joint distance for a "smooth" roundover
1336// - "k" - the curvature smoothness parameter for "smooth" roundovers, a value in [0,1]. Default: 0.75
1337// - "points" - point list for use with the "profile" type
1338// - "extra" - extra height added for unions/differences. This makes the shape taller than the requested height. (Default: 0)
1339// - "check_valid" - passed to offset. Default: true.
1340// - "quality" - passed to offset. Default: 1.
1341// - "steps" - number of vertical steps to use for the roundover. Default: 16.
1342// - "offset" - select "round" (r=), "delta" (delta=), or "chamfer" offset type for offset. Default: "round"
1343// .
1344// Note that if you set the "offset" parameter to "chamfer" then every exterior corner turns from one vertex into two vertices with
1345// each offset operation. Since the offsets are done one after another, each on the output of the previous one, this leads to
1346// exponential growth in the number of vertices. This can lead to long run times or yield models that
1347// run out of recursion depth and give a cryptic error. Furthermore, the generated vertices are distributed non-uniformly. Generally you
1348// will get a similar or better looking model with fewer vertices using "round" instead of
1349// "chamfer". Use the "chamfer" style offset only in cases where the number of steps is very small or just one (such as when using
1350// the `os_chamfer` profile type).
1351//
1352// Arguments:
1353// path = 2d path (list of points) to extrude
1354// height / length / l / h = total height (including rounded portions, but not extra sections) of the output. Default: combined height of top and bottom end treatments.
1355// bottom / bot = rounding spec for the bottom end
1356// top = rounding spec for the top end.
1357// ---
1358// ends = give a rounding spec that applies to both the top and bottom
1359// offset = default offset, `"round"` or `"delta"`. Default: `"round"`
1360// steps = default step count. Default: 16
1361// quality = default quality. Default: 1
1362// check_valid = default check_valid. Default: true.
1363// extra = default extra height. Default: 0
1364// caps = if false do not create end faces. Can be a boolean vector to control ends independent. (function only) Default: true.
1365// cut = default cut value.
1366// chamfer_width = default width value for chamfers.
1367// chamfer_height = default height value for chamfers.
1368// angle = default angle for chamfers. Default: 45
1369// joint = default joint value for smooth roundover.
1370// k = default curvature parameter value for "smooth" roundover
1371// convexity = convexity setting for use with polyhedron. (module only) Default: 10
1372// anchor = Translate so anchor point is at the origin. Default: "base"
1373// spin = Rotate this many degrees around Z axis after anchor. Default: 0
1374// orient = Vector to rotate top towards after spin
1375// atype = Select "hull", "intersect", "surf_hull" or "surf_intersect" anchor types. Default: "hull"
1376// cp = Centerpoint for determining "intersect" anchors or centering the shape. Determintes the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid"
1377// Anchor Types:
1378// hull = Anchors to the convex hull of the linear sweep of the path, ignoring any end roundings.
1379// intersect = Anchors to the surface of the linear sweep of the path, ignoring any end roundings.
1380// surf_hull = Anchors to the convex hull of the offset_sweep shape, including end treatments.
1381// surf_intersect = Anchors to the surface of the offset_sweep shape, including any end treatments.
1382// Named Anchors:
1383// "base" = Anchor to the base of the shape in its native position, ignoring any "extra"
1384// "top" = Anchor to the top of the shape in its native position, ignoring any "extra"
1385// "zcenter" = Center shape in the Z direction in the native XY position, ignoring any "extra"
1386// Example: Rounding a star shaped prism with postive radius values
1387// star = star(5, r=22, ir=13);
1388// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
1389// offset_sweep(rounded_star, height=20, bottom=os_circle(r=4), top=os_circle(r=1), steps=15);
1390// Example: Rounding a star shaped prism with negative radius values. The starting shape has no corners, so the value of `$fn` does not matter.
1391// star = star(5, r=22, ir=13);
1392// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=36);
1393// offset_sweep(rounded_star, height=20, bottom=os_circle(r=-4), top=os_circle(r=-1), steps=15);
1394// Example: If the shape has sharp corners, make sure to set `$fn/$fs/$fa`. The corners of this triangle are not round, even though `offset="round"` (the default) because the number of segments is small.
1395// triangle = [[0,0],[10,0],[5,10]];
1396// offset_sweep(triangle, height=6, bottom = os_circle(r=-2),steps=4);
1397// Example: Can improve the result by increasing `$fn`
1398// $fn=12;
1399// triangle = [[0,0],[10,0],[5,10]];
1400// offset_sweep(triangle, height=6, bottom = os_circle(r=-2),steps=4);
1401// Example: Using `$fa` and `$fs` works too; it produces a different looking triangulation of the rounded corner
1402// $fa=1;$fs=0.3;
1403// triangle = [[0,0],[10,0],[5,10]];
1404// offset_sweep(triangle, height=6, bottom = os_circle(r=-2),steps=4);
1405// Example: Here is the star chamfered at the top with a teardrop rounding at the bottom. Check out the rounded corners on the chamfer. The large `$fn` value ensures a smooth curve on the concave corners of the chamfer. It has no effect anywhere else on the model. Observe how the rounded star points vanish at the bottom in the teardrop: the number of vertices does not remain constant from layer to layer.
1406// star = star(5, r=22, ir=13);
1407// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
1408// offset_sweep(rounded_star, height=20, bottom=os_teardrop(r=4), top=os_chamfer(width=4),$fn=64);
1409// Example: We round a cube using the continous curvature rounding profile. But note that the corners are not smooth because the curved square collapses into a square with corners. When a collapse like this occurs, we cannot turn `check_valid` off. For a better result use `rounded_prism()` instead.
1410// square = square(1);
1411// rsquare = round_corners(square, method="smooth", cut=0.1, k=0.7, $fn=36);
1412// end_spec = os_smooth(cut=0.1, k=0.7, steps=22);
1413// offset_sweep(rsquare, height=1, bottom=end_spec, top=end_spec);
1414// Example(3D,Med): A nice rounded box, with a teardrop base and circular rounded interior and top
1415// box = square([255,50]);
1416// rbox = round_corners(box, method="smooth", cut=4, $fn=12);
1417// thickness = 2;
1418// difference(){
1419// offset_sweep(rbox, height=50, check_valid=false, steps=22,
1420// bottom=os_teardrop(r=2), top=os_circle(r=1));
1421// up(thickness)
1422// offset_sweep(offset(rbox, r=-thickness, closed=true,check_valid=false),
1423// height=48, steps=22, check_valid=false,
1424// bottom=os_circle(r=4), top=os_circle(r=-1,extra=1));
1425// }
1426// Example: This box is much thicker, and cut in half to show the profiles. Note also that we can turn `check_valid` off for the outside and for the top inside, but not for the bottom inside. This example shows use of the direct keyword syntax without the helper functions.
1427// smallbox = square([75,50]);
1428// roundbox = round_corners(smallbox, method="smooth", cut=4, $fn=12);
1429// thickness=4;
1430// height=50;
1431// back_half(y=25, s=200)
1432// difference(){
1433// offset_sweep(roundbox, height=height, bottom=["for","offset_sweep","r",10,"type","teardrop"],
1434// top=["for","offset_sweep","r",2], steps = 22, check_valid=false);
1435// up(thickness)
1436// offset_sweep(offset(roundbox, r=-thickness, closed=true),
1437// height=height-thickness, steps=22,
1438// bottom=["for","offset_sweep","r",6],
1439// top=["for","offset_sweep","type","chamfer","angle",30,
1440// "chamfer_height",-3,"extra",1,"check_valid",false]);
1441// }
1442// Example(3D,Med): A box with multiple sections and rounded dividers
1443// thickness = 2;
1444// box = square([255,50]);
1445// cutpoints = [0, 125, 190, 255];
1446// rbox = round_corners(box, method="smooth", cut=4, $fn=12);
1447// back_half(y=25, s=700)
1448// difference(){
1449// offset_sweep(rbox, height=50, check_valid=false, steps=22,
1450// bottom=os_teardrop(r=2), top=os_circle(r=1));
1451// up(thickness)
1452// for(i=[0:2]){
1453// ofs = i==1 ? 2 : 0;
1454// hole = round_corners([[cutpoints[i]-ofs,0], [cutpoints[i]-ofs,50],
1455// [cutpoints[i+1]+ofs, 50], [cutpoints[i+1]+ofs,0]],
1456// method="smooth", cut=4, $fn=36);
1457// offset_sweep(offset(hole, r=-thickness, closed=true,check_valid=false),
1458// height=48, steps=22, check_valid=false,
1459// bottom=os_circle(r=4), top=os_circle(r=-1,extra=1));
1460// }
1461// }
1462// Example(3D,Med): Star shaped box
1463// star = star(5, r=22, ir=13);
1464// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
1465// thickness = 2;
1466// ht=20;
1467// difference(){
1468// offset_sweep(rounded_star, height=ht, bottom=["for","offset_sweep","r",4],
1469// top=["for","offset_sweep","r",1], steps=15);
1470// up(thickness)
1471// offset_sweep(offset(rounded_star,r=-thickness,closed=true),
1472// height=ht-thickness, check_valid=false,
1473// bottom=os_circle(r=7), top=os_circle(r=-1, extra=1),$fn=40);
1474// }
1475// Example: A profile defined by an arbitrary sequence of points.
1476// star = star(5, r=22, ir=13);
1477// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
1478// profile = os_profile(points=[[0,0],[.3,.1],[.6,.3],[.9,.9], [1.2, 2.7],[.8,2.7],[.8,3]]);
1479// offset_sweep(reverse(rounded_star), height=20, top=profile, bottom=profile, $fn=32);
1480// Example: Parabolic rounding
1481// star = star(5, r=22, ir=13);
1482// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
1483// offset_sweep(rounded_star, height=20, top=os_profile(points=[for(r=[0:.1:2])[sqr(r),r]]),
1484// bottom=os_profile(points=[for(r=[0:.2:5])[-sqrt(r),r]]),$fn=32);
1485// Example: This example uses a sine wave offset profile. Note that we give no specification for the bottom, so it is straight.
1486// sq = [[0,0],[20,0],[20,20],[0,20]];
1487// sinwave = os_profile(points=[for(theta=[0:5:720]) [4*sin(theta), theta/700*15]]);
1488// offset_sweep(sq, height=20, top=sinwave, $fn=32);
1489// Example: The same as the previous example but `offset="delta"`
1490// sq = [[0,0],[20,0],[20,20],[0,20]];
1491// sinwave = os_profile(points=[for(theta=[0:5:720]) [4*sin(theta), theta/700*15]]);
1492// offset_sweep(sq, height=20, top=sinwave, offset="delta");
1493// Example: a box with a flared top. A nice roundover on the top requires a profile edge, but we can use "extra" to create a small chamfer.
1494// rhex = round_corners(hexagon(side=10), method="smooth", joint=2, $fs=0.2);
1495// back_half()
1496// difference(){
1497// offset_sweep(rhex, height=10, bottom=os_teardrop(r=2), top=os_teardrop(r=-4, extra=0.2));
1498// up(1)
1499// offset_sweep(offset(rhex,r=-1), height=9.5, bottom=os_circle(r=2), top=os_teardrop(r=-4));
1500// }
1501// Example: Using os_mask to create ogee profiles:
1502// ogee = mask2d_ogee([
1503// "xstep",1, "ystep",1, // Starting shoulder.
1504// "fillet",5, "round",5, // S-curve.
1505// "ystep",1, // Ending shoulder.
1506// ]);
1507// star = star(5, r=220, ir=130);
1508// rounded_star = round_corners(star, cut=flatten(repeat([5,0],5)), $fn=24);
1509// offset_sweep(rounded_star, height=100, top=os_mask(ogee), bottom=os_mask(ogee,out=true));
1510
1511
1512// This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce
1513// the inputs for the polyhedron module.
1514function _make_offset_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, cap=true,
1515 offsetind=0, vertexcount=0, vertices=[], faces=[] )=
1516 offsetind==len(offsets)?
1517 let(
1518 bottom = count(len(path),vertexcount),
1519 oriented_bottom = !flip_faces? bottom : reverse(bottom)
1520 )
1521 [
1522 vertices,
1523 [each faces,
1524 if (cap) oriented_bottom]
1525 ]
1526 :
1527 let(
1528 this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0],
1529 delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef,
1530 r = offset_type=="round"? this_offset : undef,
1531 do_chamfer = offset_type == "chamfer",
1532 vertices_faces = offset(
1533 path, r=r, delta=delta, chamfer = do_chamfer, closed=true,
1534 check_valid=check_valid, quality=quality,
1535 return_faces=true,
1536 firstface_index=vertexcount,
1537 flip_faces=flip_faces
1538 )
1539 )
1540 _make_offset_polyhedron(
1541 vertices_faces[0], offsets, offset_type,
1542 flip_faces, quality, check_valid, cap,
1543 offsetind+1, vertexcount+len(path),
1544 vertices=concat(
1545 vertices,
1546 path3d(vertices_faces[0],offsets[offsetind][1])
1547 ),
1548 faces=concat(faces, vertices_faces[1])
1549 );
1550
1551
1552function _struct_valid(spec, func, name) =
1553 spec==[] ? true :
1554 assert(is_list(spec) && len(spec)>=2 && spec[0]=="for",str("Specification for \"", name, "\" is an invalid structure"))
1555 assert(spec[1]==func, str("Specification for \"",name,"\" is for a different function (",func,")"));
1556
1557function offset_sweep(
1558 path, height,
1559 bottom, top,
1560 h, l, length,
1561 ends,bot,
1562 offset="round", r=0, steps=16,
1563 quality=1, check_valid=true,
1564 extra=0, caps=true,
1565 cut=undef, chamfer_width=undef, chamfer_height=undef,
1566 joint=undef, k=0.75, angle=45, anchor="base", orient=UP, spin=0,atype="hull", cp="centroid",
1567 _return_height=false
1568 ) =
1569 let(
1570 argspec = [
1571 ["for",""],
1572 ["r",r],
1573 ["extra",extra],
1574 ["type","circle"],
1575 ["check_valid",check_valid],
1576 ["quality",quality],
1577 ["steps",steps],
1578 ["offset",offset],
1579 ["chamfer_width",chamfer_width],
1580 ["chamfer_height",chamfer_height],
1581 ["angle",angle],
1582 ["cut",cut],
1583 ["joint",joint],
1584 ["k", k],
1585 ["points", []],
1586 ],
1587 path = force_path(path)
1588 )
1589 assert(is_path(path,2), "Input path must be a 2D path")
1590 assert(is_bool(caps) || is_bool_list(caps,2), "caps must be boolean or a list of two booleans")
1591 let(
1592 caps = is_bool(caps) ? [caps,caps] : caps,
1593 clockwise = is_polygon_clockwise(path),
1594 top_temp = one_defined([ends,top],"ends,top",dflt=[]),
1595 bottom_temp = one_defined([ends,bottom,bot],"ends,bottom,bot",dflt=[]),
1596 dummy1 = _struct_valid(top_temp,"offset_sweep","top"),
1597 dummy2 = _struct_valid(bottom_temp,"offset_sweep","bottom"),
1598 top = struct_set(argspec, top_temp, grow=false),
1599 bottom = struct_set(argspec, bottom_temp, grow=false),
1600 offsetsok = in_list(struct_val(top, "offset"),["round","delta","chamfer"])
1601 && in_list(struct_val(bottom, "offset"),["round","delta","chamfer"])
1602 )
1603 assert(offsetsok,"Offsets must be one of \"round\", \"delta\", or \"chamfer\"")
1604 let(
1605 offsets_bot = _rounding_offsets(bottom, -1),
1606 offsets_top = _rounding_offsets(top, 1),
1607 dummy = (struct_val(top,"offset")=="chamfer" && len(offsets_top)>5)
1608 || (struct_val(bottom,"offset")=="chamfer" && len(offsets_bot)>5)
1609 ? echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested more than 5 layers. This can be slow or run out of recursion depth.")
1610 : 0,
1611
1612 // "Extra" height enlarges the result beyond the requested height, so subtract it
1613 bottom_height = len(offsets_bot)==0 ? 0 : abs(last(offsets_bot)[1]) - struct_val(bottom,"extra"),
1614 top_height = len(offsets_top)==0 ? 0 : abs(last(offsets_top)[1]) - struct_val(top,"extra"),
1615
1616 height = one_defined([l,h,height,length], "l,h,height,length", dflt=u_add(bottom_height,top_height)),
1617 dummy1 = assert(is_finite(height) && height>0, "Height must be positive"),
1618 middle = height-bottom_height-top_height,
1619 dummy2= assert(middle>=0, str("Specified end treatments (bottom height = ",bottom_height,
1620 " top_height = ",top_height,") are too large for extrusion height (",height,")")),
1621 initial_vertices_bot = path3d(path),
1622
1623 vertices_faces_bot = _make_offset_polyhedron(
1624 path, offsets_bot, struct_val(bottom,"offset"), clockwise,
1625 struct_val(bottom,"quality"),
1626 struct_val(bottom,"check_valid"),
1627 caps[0],
1628 vertices=initial_vertices_bot
1629 ),
1630
1631 top_start_ind = len(vertices_faces_bot[0]),
1632 initial_vertices_top = path3d(path, middle),
1633 vertices_faces_top = _make_offset_polyhedron(
1634 path, move(p=offsets_top,[0,middle]),
1635 struct_val(top,"offset"), !clockwise,
1636 struct_val(top,"quality"),
1637 struct_val(top,"check_valid"),
1638 caps[1],
1639 vertexcount=top_start_ind,
1640 vertices=initial_vertices_top
1641 ),
1642 middle_faces = middle==0 ? [] : [
1643 for(i=[0:len(path)-1]) let(
1644 oneface=[i, (i+1)%len(path), top_start_ind+(i+1)%len(path), top_start_ind+i]
1645 ) !clockwise ? reverse(oneface) : oneface
1646 ],
1647 vnf = [up(bottom_height-height/2, concat(vertices_faces_bot[0],vertices_faces_top[0])), // Vertices
1648 concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces)], // Faces
1649 anchors = [
1650 named_anchor("zcenter", [0,0,0], UP),
1651 named_anchor("base", [0,0,-height/2], UP),
1652 named_anchor("top", [0,0,height/2], UP)
1653 ],
1654 final_vnf = in_list(atype,["hull","intersect"])
1655 ? reorient(anchor,spin,orient, path=path, h=height, extent=atype=="hull", cp=cp, p=vnf, anchors=anchors)
1656 : reorient(anchor,spin,orient, vnf=vnf, p=vnf, extent=atype=="surf_hull", cp=cp, anchors=anchors)
1657 ) _return_height ? [final_vnf,height] : final_vnf;
1658
1659module offset_sweep(path, height,
1660 bottom, top,
1661 h, l, length, ends, bot,
1662 offset="round", r=0, steps=16,
1663 quality=1, check_valid=true,
1664 extra=0,
1665 cut=undef, chamfer_width=undef, chamfer_height=undef,
1666 joint=undef, k=0.75, angle=45,
1667 convexity=10,anchor="base",cp="centroid",
1668 spin=0, orient=UP, atype="hull")
1669{
1670 assert(in_list(atype, ["intersect","hull","surf_hull","surf_intersect"]), "Anchor type must be \"hull\" or \"intersect\"");
1671 vnf_h = offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top, bottom=bottom, ends=ends,
1672 offset=offset, r=r, steps=steps,
1673 quality=quality, check_valid=check_valid, extra=extra, cut=cut, chamfer_width=chamfer_width,
1674 chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _return_height=true);
1675 vnf = vnf_h[0];
1676 height = vnf_h[1];
1677 anchors = [
1678 named_anchor("zcenter", [0,0,0], UP),
1679 named_anchor("base", [0,0,-height/2], UP),
1680 named_anchor("top", [0,0,height/2], UP)
1681 ];
1682 if (in_list(atype,["hull","intersect"]))
1683 attachable(anchor,spin,orient,region=force_region(path),h=height,cp=cp,anchors=anchors,extent=atype=="hull"){
1684 down(height/2)polyhedron(vnf[0],vnf[1],convexity=convexity);
1685 children();
1686 }
1687 else
1688 attachable(anchor,spin.orient,vnf=vnf, cp=cp,anchors=anchors, extent = atype=="surf_hull"){
1689 vnf_polyhedron(vnf,convexity=convexity);
1690 children();
1691 }
1692}
1693
1694
1695function os_circle(r,cut,extra,check_valid, quality,steps, offset) =
1696 assert(num_defined([r,cut])==1, "Must define exactly one of `r` and `cut`")
1697 _remove_undefined_vals([
1698 "for", "offset_sweep",
1699 "type", "circle",
1700 "r",r,
1701 "cut",cut,
1702 "extra",extra,
1703 "check_valid",check_valid,
1704 "quality", quality,
1705 "steps", steps,
1706 "offset", offset
1707 ]);
1708
1709function os_teardrop(r,cut,extra,check_valid, quality,steps, offset) =
1710 assert(num_defined([r,cut])==1, "Must define exactly one of `r` and `cut`")
1711 _remove_undefined_vals([
1712 "for", "offset_sweep",
1713 "type", "teardrop",
1714 "r",r,
1715 "cut",cut,
1716 "extra",extra,
1717 "check_valid",check_valid,
1718 "quality", quality,
1719 "steps", steps,
1720 "offset", offset
1721 ]);
1722
1723function os_chamfer(height, width, cut, angle, extra,check_valid, quality,steps, offset) =
1724 let(ok = (is_def(cut) && num_defined([height,width])==0) || num_defined([height,width])>0)
1725 assert(ok, "Must define `cut`, or one or both of `width` and `height`")
1726 _remove_undefined_vals([
1727 "for", "offset_sweep",
1728 "type", "chamfer",
1729 "chamfer_width",width,
1730 "chamfer_height",height,
1731 "cut",cut,
1732 "angle",angle,
1733 "extra",extra,
1734 "check_valid",check_valid,
1735 "quality", quality,
1736 "steps", steps,
1737 "offset", offset
1738 ]);
1739
1740function os_smooth(cut, joint, k, extra,check_valid, quality,steps, offset) =
1741 assert(num_defined([joint,cut])==1, "Must define exactly one of `joint` and `cut`")
1742 _remove_undefined_vals([
1743 "for", "offset_sweep",
1744 "type", "smooth",
1745 "joint",joint,
1746 "k",k,
1747 "cut",cut,
1748 "extra",extra,
1749 "check_valid",check_valid,
1750 "quality", quality,
1751 "steps", steps,
1752 "offset", offset
1753 ]);
1754
1755function os_profile(points, extra,check_valid, quality, offset) =
1756 assert(is_path(points),"Profile point list is not valid")
1757 _remove_undefined_vals([
1758 "for", "offset_sweep",
1759 "type", "profile",
1760 "points", points,
1761 "extra",extra,
1762 "check_valid",check_valid,
1763 "quality", quality,
1764 "offset", offset
1765 ]);
1766
1767
1768function os_mask(mask, out=false, extra,check_valid, quality, offset) =
1769 let(
1770 origin_index = [for(i=idx(mask)) if (mask[i].x<0 && mask[i].y<0) i],
1771 xfactor = out ? -1 : 1
1772 )
1773 assert(len(origin_index)==1,"Cannot find origin in the mask")
1774 let(
1775 points = ([for(pt=list_rotate(mask,origin_index[0])) [xfactor*max(pt.x,0),-max(pt.y,0)]])
1776 )
1777 os_profile(deduplicate(move(-points[1],p=list_tail(points))), extra,check_valid,quality,offset);
1778
1779
1780// Module: convex_offset_extrude()
1781// Synopsis: Make a solid from geometry where offset changes along the object's length.
1782// SynTags: Geom
1783// Topics: Rounding, Offsets
1784// See Also: offset_sweep(), rounded_prism(), bent_cutout_mask(), join_prism(), linear_sweep()
1785// Usage: Basic usage. See below for full options
1786// convex_offset_extrude(height, [bottom], [top], ...) 2D-CHILDREN;
1787// Description:
1788// Extrudes 2d children with layers formed from the convex hull of the offset of each child according to a sequence of offset values.
1789// Like `offset_sweep` this module can use built-in offset profiles to provide treatments such as roundovers or chamfers but unlike `offset_sweep()` it
1790// operates on 2d children rather than a point list. Each offset is computed using
1791// the native `offset()` module from the input geometry.
1792// If your shape has corners that you want rounded by offset be sure to set `$fn` or `$fs` appropriately.
1793// If your geometry has internal holes or is too small for the specified offset then you may get
1794// unexpected results.
1795// .
1796// The build-in profiles are: circular rounding, teardrop rounding, continuous curvature rounding, and chamfer.
1797// Also note that when a rounding radius is negative the rounding will flare outwards. The easiest way to specify
1798// the profile is by using the profile helper functions. These functions take profile parameters, as well as some
1799// general settings and translate them into a profile specification, with error checking on your input. The description below
1800// describes the helper functions and the parameters specific to each function. Below that is a description of the generic
1801// settings that you can optionally use with all of the helper functions.
1802// For more details on the "cut" and "joint" rounding parameters, and
1803// on continuous curvature rounding, see [Types of Roundover](rounding.scad#subsection-types-of-roundover).
1804// .
1805// The final shape is created by combining convex hulls of small extrusions. The thickness of these small extrusions may result
1806// your model being slightly too long (if the curvature at the end is flaring outward), so if the exact length is very important
1807// you may need to intersect with a bounding cube. (Note that extra length can also be intentionally added with the `extra` argument.)
1808// .
1809// - profile: os_profile(points)
1810// Define the offset profile with a list of points. The first point must be [0,0] and the roundover should rise in the positive y direction, with positive x values for inward motion (standard roundover) and negative x values for flaring outward. If the y value ever decreases then you might create a self-intersecting polyhedron, which is invalid. Such invalid polyhedra will create cryptic assertion errors when you render your model and it is your responsibility to avoid creating them. Note that the starting point of the profile is the center of the extrusion. If you use a profile as the top it will rise upwards. If you use it as the bottom it will be inverted, and will go downward.
1811// - circle: os_circle(r|cut). Define circular rounding either by specifying the radius or cut distance.
1812// - smooth: os_smooth(cut|joint, [k]). Define continuous curvature rounding, with `cut` and `joint` as for round_corners. The k parameter controls how fast the curvature changes and should be between 0 and 1.
1813// - teardrop: os_teardrop(r|cut). Rounding using a 1/8 circle that then changes to a 45 degree chamfer. The chamfer is at the end, and enables the object to be 3d printed without support. The radius gives the radius of the circular part.
1814// - chamfer: os_chamfer([height], [width], [cut], [angle]). Chamfer the edge at desired angle or with desired height and width. You can specify height and width together and the angle will be ignored, or specify just one of height and width and the angle is used to determine the shape. Alternatively, specify "cut" along with angle to specify the cut back distance of the chamfer.
1815// .
1816// The general settings that you can use with all of the helper functions are mostly used to control how offset_sweep() calls the offset() function.
1817// - extra: Add an extra vertical step of the specified height, to be used for intersections or differences. This extra step will extend the resulting object beyond the height you specify. Default: 0
1818// - steps: Number of vertical steps to use for the profile. (Not used by os_profile). Default: 16
1819// - offset: Select "round" (r=), "delta" (delta=), or "chamfer" offset types for offset. Default: "round"
1820// .
1821// Many of the arguments are described as setting "default" values because they establish settings which may be overridden by
1822// the top and bottom profile specifications.
1823// .
1824// You will generally want to use the above helper functions to generate the profiles.
1825// The profile specification is a list of pairs of keywords and values, e.g. ["r",12, type, "circle"]. The keywords are
1826// - "type" - type of rounding to apply, one of "circle", "teardrop", "chamfer", "smooth", or "profile" (Default: "circle")
1827// - "r" - the radius of the roundover, which may be zero for no roundover, or negative to round or flare outward. Default: 0
1828// - "cut" - the cut distance for the roundover or chamfer, which may be negative for flares
1829// - "chamfer_width" - the width of a chamfer
1830// - "chamfer_height" - the height of a chamfer
1831// - "angle" - the chamfer angle, measured from the vertical (so zero is vertical, 90 is horizontal). Default: 45
1832// - "joint" - the joint distance for a "smooth" roundover
1833// - "k" - the curvature smoothness parameter for "smooth" roundovers, a value in [0,1]. Default: 0.75
1834// - "points" - point list for use with the "profile" type
1835// - "extra" - extra height added for unions/differences. This makes the shape taller than the requested height. (Default: 0)
1836// - "steps" - number of vertical steps to use for the roundover. Default: 16.
1837// - "offset" - select "round" (r=) or "delta" (delta=) offset type for offset. Default: "round"
1838// .
1839// Note that unlike `offset_sweep`, because the offset operation is always performed from the base shape, using chamfered offsets does not increase the
1840// number of vertices or lead to any special complications.
1841//
1842// Arguments:
1843// height / length / l / h = total height (including rounded portions, but not extra sections) of the output. Default: combined height of top and bottom end treatments.
1844// bottom = rounding spec for the bottom end
1845// top = rounding spec for the top end.
1846// ---
1847// offset = default offset, `"round"`, `"delta"`, or `"chamfer"`. Default: `"round"`
1848// steps = default step count. Default: 16
1849// extra = default extra height. Default: 0
1850// cut = default cut value.
1851// chamfer_width = default width value for chamfers.
1852// chamfer_height = default height value for chamfers.
1853// angle = default angle for chamfers. Default: 45
1854// joint = default joint value for smooth roundover.
1855// k = default curvature parameter value for "smooth" roundover
1856// convexity = convexity setting for use with polyhedron. Default: 10
1857// Example: Chamfered elliptical prism. If you stretch a chamfered cylinder the chamfer will be uneven.
1858// convex_offset_extrude(bottom = os_chamfer(height=-2),
1859// top=os_chamfer(height=1), height=7)
1860// xscale(4)circle(r=6,$fn=64);
1861// Example: Elliptical prism with circular roundovers.
1862// convex_offset_extrude(bottom=os_circle(r=-2),
1863// top=os_circle(r=1), height=7,steps=10)
1864// xscale(4)circle(r=6,$fn=64);
1865// Example: If you give a non-convex input you get a convex hull output
1866// right(50) linear_extrude(height=7) star(5,r=22,ir=13);
1867// convex_offset_extrude(bottom = os_chamfer(height=-2),
1868// top=os_chamfer(height=1), height=7, $fn=32)
1869// star(5,r=22,ir=13);
1870function convex_offset_extrude(
1871 height,
1872 bottom=[], top=[],
1873 h, l, length,
1874 offset="round", r=0, steps=16,
1875 extra=0,
1876 cut=undef, chamfer_width=undef, chamfer_height=undef,
1877 joint=undef, k=0.75, angle=45,
1878 convexity=10, thickness = 1/1024
1879) = no_function("convex_offset_extrude");
1880module convex_offset_extrude(
1881 height,
1882 bottom=[],
1883 top=[],
1884 h, l, length,
1885 offset="round", r=0, steps=16,
1886 extra=0,
1887 cut=undef, chamfer_width=undef, chamfer_height=undef,
1888 joint=undef, k=0.75, angle=45,
1889 convexity=10, thickness = 1/1024
1890) {
1891 req_children($children);
1892 argspec = [
1893 ["for", ""],
1894 ["r",r],
1895 ["extra",extra],
1896 ["type","circle"],
1897 ["steps",steps],
1898 ["offset",offset],
1899 ["chamfer_width",chamfer_width],
1900 ["chamfer_height",chamfer_height],
1901 ["angle",angle],
1902 ["cut",cut],
1903 ["joint",joint],
1904 ["k", k],
1905 ["points", []],
1906 ];
1907 top = struct_set(argspec, top, grow=false);
1908 bottom = struct_set(argspec, bottom, grow=false);
1909
1910 offsets_bot = _rounding_offsets(bottom, -1);
1911 offsets_top = _rounding_offsets(top, 1);
1912
1913 // "Extra" height enlarges the result beyond the requested height, so subtract it
1914 bottom_height = len(offsets_bot)==0 ? 0 : abs(last(offsets_bot)[1]) - struct_val(bottom,"extra");
1915 top_height = len(offsets_top)==0 ? 0 : abs(last(offsets_top)[1]) - struct_val(top,"extra");
1916
1917 height = one_defined([l,h,height,length], "l,h,height,length", dflt=u_add(bottom_height,top_height));
1918 middle = height-bottom_height-top_height;
1919 check =
1920 assert(height>=0, "Height must be nonnegative")
1921 assert(middle>=0, str(
1922 "Specified end treatments (bottom height = ",bottom_height,
1923 " top_height = ",top_height,") are too large for extrusion height (",height,")"
1924 )
1925 );
1926 // The entry r[i] is [radius,z] for a given layer
1927 r = move([0,bottom_height],p=concat(
1928 reverse(offsets_bot), [[0,0], [0,middle]], move([0,middle], p=offsets_top)));
1929 delta = [for(val=deltas(column(r,0))) sign(val)];
1930 below=[-thickness,0];
1931 above=[0,thickness];
1932 // layers is a list of pairs of the relative positions for each layer, e.g. [0,thickness]
1933 // puts the layer above the polygon, and [-thickness,0] puts it below.
1934 layers = [for (i=[0:len(r)-1])
1935 i==0 ? (delta[0]<0 ? below : above) :
1936 i==len(r)-1 ? (delta[len(delta)-1] < 0 ? below : above) :
1937 delta[i]==0 ? above :
1938 delta[i+1]==0 ? below :
1939 delta[i]==delta[i-1] ? [-thickness/2, thickness/2] :
1940 delta[i] == 1 ? above :
1941 /* delta[i] == -1 ? */ below];
1942 dochamfer = offset=="chamfer";
1943 attachable(){
1944 for(i=[0:len(r)-2])
1945 for(j=[0:$children-1])
1946 hull(){
1947 up(r[i][1]+layers[i][0])
1948 linear_extrude(convexity=convexity,height=layers[i][1]-layers[i][0])
1949 if (offset=="round")
1950 offset(r=r[i][0])
1951 children(j);
1952 else
1953 offset(delta=r[i][0],chamfer = dochamfer)
1954 children(j);
1955 up(r[i+1][1]+layers[i+1][0])
1956 linear_extrude(convexity=convexity,height=layers[i+1][1]-layers[i+1][0])
1957 if (offset=="round")
1958 offset(r=r[i+1][0])
1959 children(j);
1960 else
1961 offset(delta=r[i+1][0],chamfer=dochamfer)
1962 children(j);
1963 }
1964 union();
1965 }
1966}
1967
1968
1969
1970function _remove_undefined_vals(list) =
1971 let(ind=search([undef],list,0)[0])
1972 list_remove(list, concat(ind, add_scalar(ind,-1)));
1973
1974
1975
1976function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) =
1977 let(
1978 N = len(top),
1979 plane = plane3pt_indexed(top,0,1,2),
1980 rtop_in = is_list(rtop) ? rtop[0] : rtop,
1981 rtop_down = is_list(rtop) ? rtop[1] : abs(rtop)
1982 )
1983 [for(i=[0:N-1])
1984 let(
1985 rside_prev = is_list(rsides[i])? rsides[i][0] : rsides[i],
1986 rside_next = is_list(rsides[i])? rsides[i][1] : rsides[i],
1987 concave_sign = (concave[i] ? -1 : 1) * (rtop_in>=0 ? 1 : -1), // Negative if normals need to go "out"
1988 prev = select(top,i-1) - top[i],
1989 next = select(top,i+1) - top[i],
1990 prev_offset = top[i] + rside_prev * unit(prev) / sin(vector_angle(prev,bot[i]-top[i])),
1991 next_offset = top[i] + rside_next * unit(next) / sin(vector_angle(next,bot[i]-top[i])),
1992 down = rtop_down * unit(bot[i]-top[i]) / sin(abs(plane_line_angle(plane, [bot[i],top[i]]))),
1993 row2 = [prev_offset, top[i], next_offset ],
1994 row4 = [prev_offset+down,top[i]+down,next_offset+down],
1995 in_prev = concave_sign * unit(next-(next*prev)*prev/(prev*prev)),
1996 in_next = concave_sign * unit(prev-(prev*next)*next/(next*next)),
1997 far_corner = top[i]+ concave_sign*unit(unit(prev)+unit(next))* abs(rtop_in) / sin(vector_angle(prev,next)/2),
1998 row0 =
1999 concave_sign<0 ?
2000 [prev_offset+abs(rtop_in)*in_prev, far_corner, next_offset+abs(rtop_in)*in_next]
2001 :
2002 let(
2003 prev_corner = prev_offset + abs(rtop_in)*in_prev,
2004 next_corner = next_offset + abs(rtop_in)*in_next,
2005 line = project_plane(plane, [
2006 [far_corner, far_corner+prev],
2007 [prev_offset, prev_offset+in_prev],
2008 [far_corner, far_corner+next],
2009 [next_offset, next_offset+in_next]
2010 ]),
2011 prev_degenerate = is_undef(line_intersection(line[0],line[1],RAY,RAY)),
2012 next_degenerate = is_undef(line_intersection(line[2],line[3],RAY,RAY))
2013 )
2014 [ prev_degenerate ? far_corner : prev_corner,
2015 far_corner,
2016 next_degenerate ? far_corner : next_corner]
2017 ) _smooth_bez_fill(
2018 [for(row=[row0, row2, row4]) _smooth_bez_fill(row,ksides[i])],
2019 ktop)];
2020
2021
2022// Function&Module: rounded_prism()
2023// Synopsis: Make a rounded 3d object by connecting two polygons with the same vertex count.
2024// SynTags: Geom, VNF
2025// Topics: Rounding, Offsets
2026// See Also: offset_sweep(), convex_offset_extrude(), rounded_prism(), bent_cutout_mask(), join_prism()
2027// Usage: as a module
2028// rounded_prism(bottom, [top], [height=|h=|length=|l=], [joint_top=], [joint_bot=], [joint_sides=], [k=], [k_top=], [k_bot=], [k_sides=], [splinesteps=], [debug=], [convexity=],...) [ATTACHMENTS];
2029// Usage: as a function
2030// vnf = rounded_prism(bottom, [top], [height=|h=|length=|l=], [joint_top=], [joint_bot=], [joint_sides=], [k=], [k_top=], [k_bot=], [k_sides=], [splinesteps=], [debug=]);
2031// Description:
2032// Construct a generalized prism with continuous curvature rounding. You supply the polygons for the top and bottom of the prism. The only
2033// limitation is that joining the edges must produce a valid polyhedron with coplanar side faces. You specify the rounding by giving
2034// the joint distance away from the corner for the rounding curve. The k parameter ranges from 0 to 1 with a default of 0.5. Larger
2035// values give a more abrupt transition and smaller ones a more gradual transition. If you set the value much higher
2036// than 0.8 the curvature changes abruptly enough that though it is theoretically continuous, it may
2037// not be continuous in practice. A value of 0.92 is a good approximation to a circle. If you set it very small then the transition
2038// is so gradual that the roundover may be very small. If you want a very smooth roundover, set the joint parameter as large as possible and
2039// then adjust the k value down as low as gives a sufficiently large roundover. See
2040// [Types of Roundover](rounding.scad#subsection-types-of-roundover) for more information on continuous curvature rounding.
2041// .
2042// You can specify the bottom and top polygons by giving two compatible 3d paths. You can also give 2d paths and a height/length and the
2043// two shapes will be offset in the z direction from each other. The final option is to specify just the bottom along with a height/length;
2044// in this case the top will be a copy of the bottom, offset in the z direction by the specified height.
2045// .
2046// You define rounding for all of the top edges, all of the bottom edges, and independently for each of the connecting side edges.
2047// You specify rounding the rounding by giving the joint distance for where the curved section should start. If the joint distance is 1 then
2048// it means the curved section begins 1 unit away from the edge (in the perpendicular direction). Typically each joint distance is a scalar
2049// value and the rounding is symmetric around each edge. However, you can specify a 2-vector for the joint distance to produce asymmetric
2050// rounding which is different on the two sides of the edge. This may be useful when one one edge in your polygon is much larger than another.
2051// For the top and bottom you can specify negative joint distances. If you give a scalar negative value then the roundover will flare
2052// outward. If you give a vector value then a negative value then if joint_top[0] is negative the shape will flare outward, but if
2053// joint_top[1] is negative the shape will flare upward. At least one value must be non-negative. The same rules apply for joint_bot.
2054// The joint_sides parameter must be entirely nonnegative.
2055// .
2056// If the roundings at two adjacent side edges exceed the width of the face then the polyhedron will have self-intersecting faces, so it will be invalid.
2057// Similarly, if the roundings on the top or bottom edges cross the top face and intersect with each other, the resulting polyhedron is invalid:
2058// the top face after the roundings are applied must be a valid, non-degenerate polyhedron. There are two exceptions: it is permissible to
2059// construct a top that is a single point or two points. This means you can completely round a cube by setting the joint to half of
2060// the cube's width.
2061// If you set `debug` to true the module version will display the polyhedron even when it is invalid and it will show the bezier patches at the corners.
2062// This can help troubleshoot problems with your parameters. With the function form setting debug to true causes it to return [patches,vnf] where
2063// patches is a list of the bezier control points for the corner patches.
2064// .
2065// Note that rounded_prism() is not well suited to rounding shapes that have already been rounded, or that have many points.
2066// It works best when the top and bottom are polygons with well-defined corners. When the polygons have been rounded already,
2067// further rounding generates tiny bezier patches patches that can more easily
2068// interfere, giving rise to an invalid polyhedron. It's also slow because you get bezier patches for every corner in the model.
2069// .
2070// Arguments:
2071// bottom = 2d or 3d path describing bottom polygon
2072// top = 2d or 3d path describing top polygon (must be the same dimension as bottom)
2073// ---
2074// height/length/h/l = height of the shape when you give 2d bottom
2075// joint_top = rounding length for top (number or 2-vector). Default: 0
2076// joint_bot = rounding length for bottom (number or 2-vector). Default: 0
2077// joint_sides = rounding length for side edges, a number/2-vector or list of them. Default: 0
2078// k = continuous curvature rounding parameter for all edges. Default: 0.5
2079// k_top = continuous curvature rounding parameter for top
2080// k_bot = continuous curvature rounding parameter for bottom
2081// k_sides = continuous curvature rounding parameter side edges, a number or vector.
2082// splinesteps = number of segments to use for curved patches. Default: 16
2083// debug = turn on debug mode which displays illegal polyhedra and shows the bezier corner patches for troubleshooting purposes. Default: False
2084// convexity = convexity parameter for polyhedron(), only for module version. Default: 10
2085// anchor = Translate so anchor point is at the origin. (module only) Default: "origin"
2086// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0
2087// orient = Vector to rotate top towards after spin (module only)
2088// atype = Select "hull" or "intersect" anchor types. (module only) Default: "hull"
2089// cp = Centerpoint for determining "intersect" anchors or centering the shape. Determintes the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. (module only) Default: "centroid"
2090// Named Anchors:
2091// "origin" = The native position of the prism.
2092// Anchor Types:
2093// "hull" = Anchors to the virtual convex hull of the prism.
2094// "intersect" = Anchors to the surface of the prism.
2095// Example: Uniformly rounded pentagonal prism
2096// rounded_prism(pentagon(3), height=3,
2097// joint_top=0.5, joint_bot=0.5, joint_sides=0.5);
2098// Example: Maximum possible rounding.
2099// rounded_prism(pentagon(3), height=3,
2100// joint_top=1.5, joint_bot=1.5, joint_sides=1.5);
2101// Example: Decreasing k from the default of 0.5 to 0.3 gives a smoother round over which takes up more space, so it appears less rounded.
2102// rounded_prism(pentagon(3), height=3, joint_top=1.5, joint_bot=1.5,
2103// joint_sides=1.5, k=0.3, splinesteps=32);
2104// Example: Increasing k from the default of 0.5 to 0.92 approximates a circular roundover, which does not have continuous curvature. Notice the visible "edges" at the boundary of the corner and edge patches.
2105// rounded_prism(pentagon(3), height=3, joint_top=0.5,
2106// joint_bot=0.5, joint_sides=0.5, k=0.92);
2107// Example: rounding just one edge
2108// rounded_prism(pentagon(side=3), height=3, joint_top=0.5, joint_bot=0.5,
2109// joint_sides=[0,0,0.5,0,0], splinesteps=16);
2110// Example: rounding all the edges differently
2111// rounded_prism(pentagon(side=3), height=3, joint_top=0.25, joint_bot=0.5,
2112// joint_sides=[1.7,.5,.7,1.2,.4], splinesteps=32);
2113// Example: different k values for top, bottom and sides
2114// rounded_prism(pentagon(side=3.0), height=3.0, joint_top=1.4, joint_bot=1.4,
2115// joint_sides=0.7, k_top=0.7, k_bot=0.3, k_sides=0.5, splinesteps=48);
2116// Example: flared bottom
2117// rounded_prism(pentagon(3), height=3, joint_top=1.0,
2118// joint_bot=-0.5, joint_sides=0.5);
2119// Example: truncated pyramid
2120// rounded_prism(pentagon(3), apply(scale(.7),pentagon(3)),
2121// height=3, joint_top=0.5, joint_bot=0.5, joint_sides=0.5);
2122// Example: top translated
2123// rounded_prism(pentagon(3), apply(right(2),pentagon(3)),
2124// height=3, joint_top=0.5, joint_bot=0.5, joint_sides=0.5);
2125// Example(NORENDER): top rotated: fails due to non-coplanar side faces
2126// rounded_prism(pentagon(3), apply(rot(45),pentagon(3)), height=3,
2127// joint_top=0.5, joint_bot=0.5, joint_sides=0.5);
2128// Example: skew top
2129// rounded_prism(path3d(pentagon(3)), apply(affine3d_skew_yz(0,-20),path3d(pentagon(3),3)),
2130// joint_top=0.5, joint_bot=0.5, joint_sides=0.5);
2131// Example: this rotation gives coplanar sides
2132// rounded_prism(path3d(square(4)), apply(yrot(-100)*right(2),path3d(square(4),3)),
2133// joint_top=0.5, joint_bot=0.5, joint_sides=0.5);
2134// Example: a shape with concave corners
2135// M = path3d(turtle(["left", 180, "length",3,"move", "left", "move", 3, "right",
2136// "move", "right", "move", 4, "right", "move", 3, "right", "move", 2]));
2137// rounded_prism(M, apply(up(3),M), joint_top=0.75, joint_bot=0.2,
2138// joint_sides=[.2,2.5,2,0.5,1.5,.5,2.5], splinesteps=32);
2139// Example: using debug mode to see the corner patch sizes, which may help figure out problems with interfering corners or invalid polyhedra. The corner patches must not intersect each other.
2140// M = path3d(turtle(["left", 180, "length",3,"move", "left", "move", 3, "right",
2141// "move", "right", "move", 4, "right", "move", 3, "right", "move", 2]));
2142// rounded_prism(M, apply(up(3),M), joint_top=0.75, joint_bot=0.2,
2143// joint_sides=[.2,2.5,2,0.5,1.5,.5,2.5], splinesteps=16,debug=true);
2144// Example: applying transformation to the previous example
2145// M = path3d(turtle(["left", 180, "length",3,"move", "left", "move", 3, "right",
2146// "move", "right", "move", 4, "right", "move", 3, "right", "move", 2]));
2147// rounded_prism(M, apply(right(1)*scale(.75)*up(3),M), joint_top=0.5, joint_bot=0.2,
2148// joint_sides=[.2,1,1,0.5,1.5,.5,2], splinesteps=32);
2149// Example: this example shows most of the different types of patches that rounded_prism creates. Note that some of the patches are close to interfering with each other across the top of the polyhedron, which would create an invalid result.
2150// N = apply(rot(180)*yscale(.8),turtle(["length",3,"left", "move", 2, "right", 135,"move", sqrt(2),
2151// "left", "move", sqrt(2), "right", 135, "move", 2]));
2152// rounded_prism(N, height=3, joint_bot=0.5, joint_top=1.25, joint_sides=[[1,1.75],0,.5,.5,2], debug=true);
2153// Example: This object has different scales on its different axies. Here is the largest symmetric rounding that fits. Note that the rounding is slightly smaller than the object dimensions because of roundoff error.
2154// rounded_prism(square([100.1,30.1]), height=8.1, joint_top=4, joint_bot=4,
2155// joint_sides=15, k_sides=0.3, splinesteps=32);
2156// Example: Using asymetric rounding enables a much more rounded form:
2157// rounded_prism(square([100.1,30.1]), height=8.1, joint_top=[15,4], joint_bot=[15,4],
2158// joint_sides=[[15,50],[50,15],[15,50],[50,15]], k_sides=0.3, splinesteps=32);
2159// Example: Flaring the top upward instead of outward. The bottom has an asymmetric rounding with a small flare but a large rounding up the side.
2160// rounded_prism(pentagon(3), height=3, joint_top=[1,-1],
2161// joint_bot=[-0.5,2], joint_sides=0.5);
2162// Example: Sideways polygons:
2163// rounded_prism(apply(yrot(95),path3d(hexagon(3))), apply(yrot(95), path3d(hexagon(3),3)),
2164// joint_top=2, joint_bot=1, joint_sides=1);
2165// Example: Chamfer a polyhedron by setting splinesteps to 1
2166// N = apply(rot(180)*yscale(.8),turtle(["length",3,"left", "move", 2, "right", 135,"move", sqrt(2),
2167// "left", "move", sqrt(2), "right", 135, "move", 2]));
2168// rounded_prism(N, height=3, joint_bot=-0.3, joint_top=.4, joint_sides=[.75,0,.2,.2,.7], splinesteps=1);
2169
2170
2171module rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_bot, k_top, k_sides,
2172 k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false,
2173 anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull")
2174{
2175 assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"");
2176 result = rounded_prism(bottom=bottom, top=top, joint_bot=joint_bot, joint_top=joint_top, joint_sides=joint_sides,
2177 k_bot=k_bot, k_top=k_top, k_sides=k_sides, k=k, splinesteps=splinesteps, h=h, length=length, height=height, l=l,debug=debug);
2178 vnf = debug ? result[1] : result;
2179 attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=atype=="hull", cp=cp)
2180 {
2181 if (debug){
2182 vnf_polyhedron(vnf, convexity=convexity);
2183 debug_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false);
2184 }
2185 else vnf_polyhedron(vnf,convexity=convexity);
2186 children();
2187 }
2188}
2189
2190
2191function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_bot, k_top, k_sides, k=0.5, splinesteps=16,
2192 h, length, l, height, debug=false) =
2193 let(
2194 bottom = force_path(bottom,"bottom"),
2195 top = force_path(top,"top")
2196 )
2197 assert(is_path(bottom,[2,3]) && len(bottom)>=3, "bottom must be a 2D or 3D path")
2198 assert(is_num(k) && k>=0 && k<=1, "Curvature parameter k must be in interval [0,1]")
2199 let(
2200 N=len(bottom),
2201 k_top = default(k_top, k),
2202 k_bot = default(k_bot, k),
2203 k_sides = default(k_sides, k),
2204 height = one_defined([h,l,height,length],"height,length,l,h", dflt=undef),
2205 shapedimok = (len(bottom[0])==3 && is_path(top,3)) || (len(bottom[0])==2 && (is_undef(top) || is_path(top,2)))
2206 )
2207 assert(is_num(k_top) && k_top>=0 && k_top<=1, "Curvature parameter k_top must be in interval [0,1]")
2208 assert(is_num(k_bot) && k_bot>=0 && k_bot<=1, "Curvature parameter k_bot must be in interval [0,1]")
2209 assert(shapedimok,"bottom and top must be 2d or 3d paths with the same dimension")
2210 assert(len(bottom[0])==3 || is_num(height),"Must give height/length with 2d polygon input")
2211 let(
2212 // Determine which points are concave by making bottom 2d if necessary
2213 bot_proj = len(bottom[0])==2 ? bottom : project_plane(select(bottom,0,2),bottom),
2214 bottom_sign = is_polygon_clockwise(bot_proj) ? 1 : -1,
2215 concave = [for(i=[0:N-1]) bottom_sign*sign(_point_left_of_line2d(select(bot_proj,i+1), select(bot_proj, i-1,i)))>0],
2216 top = is_undef(top) ? path3d(bottom,height/2) :
2217 len(top[0])==2 ? path3d(top,height/2) :
2218 top,
2219 bottom = len(bottom[0])==2 ? path3d(bottom,-height/2) : bottom,
2220 jssingleok = (is_num(joint_sides) && joint_sides >= 0) || (is_vector(joint_sides,2) && joint_sides[0]>=0 && joint_sides[1]>=0),
2221 jsvecok = is_list(joint_sides) && len(joint_sides)==N && []==[for(entry=joint_sides) if (!(is_num(entry) || is_vector(entry,2))) entry]
2222 )
2223 assert(is_num(joint_top) || is_vector(joint_top,2))
2224 assert(is_num(joint_bot) || is_vector(joint_bot,2))
2225 assert(is_num(joint_top) || (joint_top[0]>=0 ||joint_top[1]>=0), "Both entries in joint_top cannot be negative")
2226 assert(is_num(joint_bot) || (joint_bot[0]>=0 ||joint_bot[1]>=0), "Both entries in joint_bot cannot be negative")
2227 assert(jsvecok || jssingleok,
2228 str("Argument joint_sides is invalid. All entries must be nonnegative, and it must be a number, 2-vector, or a length ",N," list those."))
2229 assert(is_num(k_sides) || is_vector(k_sides,N), str("Curvature parameter k_sides must be a number or length ",N," vector"))
2230 assert(is_coplanar(bottom))
2231 assert(is_coplanar(top))
2232 assert(!is_num(k_sides) || (k_sides>=0 && k_sides<=1), "Curvature parameter k_sides must be in interval [0,1]")
2233 let(
2234 non_coplanar=[for(i=[0:N-1]) if (!is_coplanar(concat(select(top,i,i+1), select(bottom,i,i+1)))) [i,(i+1)%N]],
2235 k_sides_vec = is_num(k_sides) ? repeat(k_sides, N) : k_sides,
2236 kbad = [for(i=[0:N-1]) if (k_sides_vec[i]<0 || k_sides_vec[i]>1) i],
2237 joint_sides_vec = jssingleok ? repeat(joint_sides,N) : joint_sides,
2238 top_collinear = [for(i=[0:N-1]) if (is_collinear(select(top,i-1,i+1))) i],
2239 bot_collinear = [for(i=[0:N-1]) if (is_collinear(select(bottom,i-1,i+1))) i]
2240 )
2241 assert(non_coplanar==[], str("Side faces are non-coplanar at edges: ",non_coplanar))
2242 assert(top_collinear==[], str("Top has collinear or duplicated points at indices: ",top_collinear))
2243 assert(bot_collinear==[], str("Bottom has collinear or duplicated points at indices: ",bot_collinear))
2244 assert(kbad==[], str("k_sides parameter outside interval [0,1] at indices: ",kbad))
2245 let(
2246 top_patch = _rp_compute_patches(top, bottom, joint_top, joint_sides_vec, k_top, k_sides_vec, concave),
2247 bot_patch = _rp_compute_patches(bottom, top, joint_bot, joint_sides_vec, k_bot, k_sides_vec, concave),
2248
2249 vertbad = [for(i=[0:N-1])
2250 if (norm(top[i]-top_patch[i][4][2]) + norm(bottom[i]-bot_patch[i][4][2]) > EPSILON + norm(bottom[i]-top[i])) i],
2251 // Check that the patch fits on the polygon edge
2252 topbad = [for(i=[0:N-1])
2253 if (norm(top_patch[i][2][4]-top_patch[i][2][2]) + norm(select(top_patch,i+1)[2][0]-select(top_patch,i+1)[2][2])
2254 > EPSILON + norm(top_patch[i][2][2] - select(top_patch,i+1)[2][2])) [i,(i+1)%N]],
2255 botbad = [for(i=[0:N-1])
2256 if (norm(bot_patch[i][2][4]-bot_patch[i][2][2]) + norm(select(bot_patch,i+1)[2][0]-select(bot_patch,i+1)[2][2])
2257 > EPSILON + norm(bot_patch[i][2][2] - select(bot_patch,i+1)[2][2])) [i,(i+1)%N]],
2258 // If top/bot is L-shaped, check that arms of L from adjacent patches don't cross
2259 topLbad = [for(i=[0:N-1])
2260 if (norm(top_patch[i][0][2]-top_patch[i][0][4]) + norm(select(top_patch,i+1)[0][0]-select(top_patch,i+1)[0][2])
2261 > EPSILON + norm(top_patch[i][0][2]-select(top_patch,i+1)[0][2])) [i,(i+1)%N]],
2262 botLbad = [for(i=[0:N-1])
2263 if (norm(bot_patch[i][0][2]-bot_patch[i][0][4]) + norm(select(bot_patch,i+1)[0][0]-select(bot_patch,i+1)[0][2])
2264 > EPSILON + norm(bot_patch[i][0][2]-select(bot_patch,i+1)[0][2])) [i,(i+1)%N]],
2265 // Check that the inner edges of the patch don't cross
2266 topinbad = [for(i=[0:N-1])
2267 let(
2268 line1 = project_plane(top,[top_patch[i][2][0],top_patch[i][0][0]]),
2269 line2 = project_plane(top,[select(top_patch,i+1)[2][4],select(top_patch,i+1)[0][4]])
2270 )
2271 if (!approx(line1[0],line1[1]) && !approx(line2[0],line2[1]) &&
2272 line_intersection(line1,line2, SEGMENT,SEGMENT))
2273 [i,(i+1)%N]],
2274 botinbad = [for(i=[0:N-1])
2275 let(
2276 line1 = project_plane(bottom,[bot_patch[i][2][0],bot_patch[i][0][0]]),
2277 line2 = project_plane(bottom,[select(bot_patch,i+1)[2][4],select(bot_patch,i+1)[0][4]])
2278 )
2279 if (!approx(line1[0],line1[1]) && !approx(line2[0],line2[1]) &&
2280 line_intersection(line1,line2, SEGMENT,SEGMENT))
2281 [i,(i+1)%N]]
2282 )
2283 assert(debug || vertbad==[], str("Top and bottom joint lengths are too large; they interfere with each other at vertices: ",vertbad))
2284 assert(debug || topbad==[], str("Joint lengths too large at top or side edges: ",topbad))
2285 assert(debug || botbad==[], str("Joint lengths too large at bottom or side edges: ",botbad))
2286 assert(debug || topLbad==[], str("Joint length too large on the top face or side at edges: ", topLbad))
2287 assert(debug || botLbad==[], str("Joint length too large on the bottom face or side at edges: ", botLbad))
2288 assert(debug || topinbad==[], str("Joint length too large on the top face at edges: ", topinbad))
2289 assert(debug || botinbad==[], str("Joint length too large on the bottom face at edges: ", botinbad))
2290 let(
2291 // Entries in the next two lists have the form [edges, vnf] where
2292 // edges is a list [leftedge, rightedge, topedge, botedge]
2293 top_samples = [for(patch=top_patch) bezier_vnf_degenerate_patch(patch,splinesteps,reverse=false,return_edges=true) ],
2294 bot_samples = [for(patch=bot_patch) bezier_vnf_degenerate_patch(patch,splinesteps,reverse=true,return_edges=true) ],
2295 leftidx=0,
2296 rightidx=1,
2297 topidx=2,
2298 botidx=3,
2299 edge_points =
2300 [for(i=[0:N-1])
2301 let(
2302 top_edge = [ top_samples[i][1][rightidx], select(top_samples, i+1)[1][leftidx]],
2303 bot_edge = [ select(bot_samples, i+1)[1][leftidx], bot_samples[i][1][rightidx]],
2304 vert_edge = [ bot_samples[i][1][botidx], top_samples[i][1][botidx]]
2305 )
2306 each [top_edge, bot_edge, vert_edge] ],
2307 faces = [
2308 [for(i=[0:N-1]) each top_samples[i][1][topidx]],
2309 [for(i=[N-1:-1:0]) each reverse(bot_samples[i][1][topidx])],
2310 for(i=[0:N-1]) [
2311 bot_patch[i][4][4],
2312 select(bot_patch,i+1)[4][0],
2313 select(top_patch,i+1)[4][0],
2314 top_patch[i][4][4]
2315 ]
2316 ],
2317 top_collinear = is_collinear(faces[0]),
2318 bot_collinear = is_collinear(faces[1]),
2319 top_degen_ok = top_collinear && len(deduplicate(faces[0]))<=2,
2320 bot_degen_ok = bot_collinear && len(deduplicate(faces[1]))<=2,
2321 top_simple = top_degen_ok || (!top_collinear && is_path_simple(project_plane(faces[0],faces[0]),closed=true)),
2322 bot_simple = bot_degen_ok || (!bot_collinear && is_path_simple(project_plane(faces[1],faces[1]),closed=true)),
2323 // verify vertical edges
2324 verify_vert =
2325 [for(i=[0:N-1],j=[0:4])
2326 let(
2327 vline = concat(select(column(top_patch[i],j),2,4),
2328 select(column(bot_patch[i],j),2,4))
2329 )
2330 if (!is_collinear(vline)) [i,j]],
2331 //verify horiz edges
2332 verify_horiz=[for(i=[0:N-1], j=[0:4])
2333 let(
2334 hline_top = concat(select(top_patch[i][j],2,4), select(select(top_patch, i+1)[j],0,2)),
2335 hline_bot = concat(select(bot_patch[i][j],2,4), select(select(bot_patch, i+1)[j],0,2))
2336 )
2337 if (!is_collinear(hline_top) || !is_collinear(hline_bot)) [i,j]]
2338 )
2339 assert(debug || top_simple,
2340 "Roundovers interfere with each other on top face: either input is self intersecting or top joint length is too large")
2341 assert(debug || bot_simple,
2342 "Roundovers interfere with each other on bottom face: either input is self intersecting or top joint length is too large")
2343 assert(debug || (verify_vert==[] && verify_horiz==[]), "Curvature continuity failed")
2344 let(
2345 vnf = vnf_join([ each column(top_samples,0),
2346 each column(bot_samples,0),
2347 for(pts=edge_points) vnf_vertex_array(pts),
2348 debug ? vnf_from_polygons(faces,fast=true)
2349 : vnf_triangulate(vnf_from_polygons(faces))
2350 ])
2351 )
2352 debug ? [concat(top_patch, bot_patch), vnf] : vnf;
2353
2354
2355
2356// Converts a 2d path to a path on a cylinder at radius r
2357function _cyl_hole(r, path) =
2358 [for(point=path) cylindrical_to_xyz(concat([r],xscale(360/(2*PI*r),p=point)))];
2359
2360// Mask profile of 180 deg of a circle to round an edge
2361function _circle_mask(r) =
2362 let(eps=0.1)
2363
2364 fwd(r+.01,p=
2365 [
2366 [r+eps,0],
2367 each arc(r=r, angle=[0, 180]),
2368 [-r-eps,0],
2369 [-r-eps, r+3*eps],
2370 [r+eps, r+3*eps]
2371 ]);
2372
2373
2374// Module: bent_cutout_mask()
2375// Synopsis: Create a mask for making a round-edged cutout in a cylindrical shell.
2376// SynTags: Geom
2377// Topics: Rounding, Offsets
2378// See Also: offset_sweep(), convex_offset_extrude(), rounded_prism(), bent_cutout_mask(), join_prism()
2379// Usage:
2380// bent_cutout_mask(r|radius, thickness, path);
2381// Description:
2382// Creates a mask for cutting a round-edged hole out of a vertical cylindrical shell. The specified radius
2383// is the center radius of the cylindrical shell. The path needs to be sampled finely enough
2384// so that it can follow the curve of the cylinder. The thickness may need to be slighly oversized to
2385// handle the faceting of the cylinder. The path is wrapped around a cylinder, keeping the
2386// same dimensions that is has on the plane, with y axis mapping to the z axis and the x axis bending
2387// around the curve of the cylinder. The angular span of the path on the cylinder must be somewhat
2388// less than 180 degrees, and the path shouldn't have closely spaced points at concave points of high curvature because
2389// this will cause self-intersection in the mask polyhedron, resulting in CGAL failures.
2390// Arguments:
2391// r / radius = center radius of the cylindrical shell to cut a hole in
2392// thickness = thickness of cylindrical shell (may need to be slighly oversized)
2393// path = 2d path that defines the hole to cut
2394// Example: The mask as long pointed ends because this was the most efficient way to close off those ends.
2395// bent_cutout_mask(10, 1, apply(xscale(3),circle(r=3)),$fn=64);
2396// Example: An elliptical hole. Note the thickness is slightly increased to 1.05 compared to the actual thickness of 1.
2397// rot(-90) {
2398// $fn=128;
2399// difference(){
2400// cyl(r=10.5, h=10);
2401// cyl(r=9.5, h=11);
2402// bent_cutout_mask(10, 1.05, apply(xscale(3),circle(r=3)),
2403// $fn=64);
2404// }
2405// }
2406// Example: An elliptical hole in a thick cylinder
2407// rot(-90) {
2408// $fn=128;
2409// difference(){
2410// cyl(r=14.5, h=15);
2411// cyl(r=9.5, h=16);
2412// bent_cutout_mask(12, 5.1, apply(xscale(3),circle(r=3)));
2413// }
2414// }
2415// Example: Complex shape example
2416// rot(-90) {
2417// $fn=128;
2418// difference(){
2419// cyl(r=10.5, h=10, $fn=128);
2420// cyl(r=9.5, h=11, $fn=128);
2421// bent_cutout_mask(10, 1.05,
2422// apply(scale(3),
2423// supershape(step=2,m1=5, n1=0.3,n2=1.7)),$fn=32);
2424// }
2425// }
2426// Example: this shape is invalid due to self-intersections at the inner corners
2427// rot(-90) {
2428// $fn=128;
2429// difference(){
2430// cylinder(r=10.5, h=10,center=true);
2431// cylinder(r=9.5, h=11,center=true);
2432// bent_cutout_mask(10, 1.05,
2433// apply(scale(3),
2434// supershape(step=2,m1=5, n1=0.1,n2=1.7)),$fn=32);
2435// }
2436// }
2437// Example: increasing the step gives a valid shape, but the shape looks terrible with so few points.
2438// rot(-90) {
2439// $fn=128;
2440// difference(){
2441// cylinder(r=10.5, h=10,center=true);
2442// cylinder(r=9.5, h=11,center=true);
2443// bent_cutout_mask(10, 1.05,
2444// apply(scale(3),
2445// supershape(step=12,m1=5, n1=0.1,n2=1.7)),$fn=32);
2446// }
2447// }
2448// Example: uniform resampling produces a somewhat better result, but room remains for improvement. The lesson is that concave corners in your cutout cause trouble. To get a very good result we need to non-uniformly sample the supershape with more points at the star tips and few points at the inner corners.
2449// rot(-90) {
2450// $fn=128;
2451// difference(){
2452// cylinder(r=10.5, h=10,center=true);
2453// cylinder(r=9.5, h=11,center=true);
2454// bent_cutout_mask(10, 1.05,
2455// apply(scale(3), resample_path(
2456// supershape(step=1,m1=5, n1=0.10,n2=1.7),
2457// 60,closed=true)),
2458// $fn=32);
2459// }
2460// }
2461// Example: The cutout spans 177 degrees. If you decrease the tube radius to 2.5 the cutout spans over 180 degrees and the model fails.
2462// r=2.6; // Don't make this much smaller or it will fail
2463// rot(-90) {
2464// $fn=128;
2465// difference(){
2466// tube(or=r, wall=1, h=10, anchor=CENTER);
2467// bent_cutout_mask(r-0.5, 1.05,
2468// apply(scale(3),
2469// supershape(step=1,m1=5, n1=0.15,n2=1.7)),$fn=32);
2470// }
2471// }
2472// Example: A square hole is not as simple as it seems. The model valid, but wrong, because the square didn't have enough samples to follow the curvature of the cylinder.
2473// r=25;
2474// rot(-90) {
2475// $fn=128;
2476// difference(){
2477// tube(or=r, wall=2, h=35, anchor=BOT);
2478// bent_cutout_mask(r-1, 2.1, back(5,p=square([18,18])));
2479// }
2480// }
2481// Example: Adding additional points fixed this problem
2482// r=25;
2483// rot(-90) {
2484// $fn=128;
2485// difference(){
2486// tube(or=r, wall=2, h=35, anchor=BOT);
2487// bent_cutout_mask(r-1, 2.1,
2488// subdivide_path(back(5,p=square([18,18])),64,closed=true));
2489// }
2490// }
2491// Example: Rounding just the exterior corners of this star avoids the problems we had above with concave corners of the supershape, as long as we don't oversample the star.
2492// r=25;
2493// rot(-90) {
2494// $fn=128;
2495// difference(){
2496// tube(or=r, wall=2, h=35, anchor=BOT);
2497// bent_cutout_mask(r-1, 2.1,
2498// apply(back(15),
2499// subdivide_path(
2500// round_corners(star(n=7,ir=5,or=10),
2501// cut=flatten(repeat([0.5,0],7)),$fn=32),
2502// 14*15,closed=true)));
2503// }
2504// }
2505// Example(2D): Cutting a slot in a cylinder is tricky if you want rounded corners at the top. This slot profile has slightly angled top edges to blend into the top edge of the cylinder.
2506// function slot(slotwidth, slotheight, slotradius) = let(
2507// angle = 85,
2508// slot = round_corners(
2509// turtle([
2510// "right",
2511// "move", slotwidth,
2512// "left", angle,
2513// "move", 2*slotwidth,
2514// "right", angle,
2515// "move", slotheight,
2516// "left",
2517// "move", slotwidth,
2518// "left",
2519// "move", slotheight,
2520// "right", angle,
2521// "move", 2*slotwidth,
2522// "left", angle,
2523// "move", slotwidth
2524// ]),
2525// radius = [0,0,each repeat(slotradius,4),0,0], closed=false
2526// )
2527// ) apply(left(max(column(slot,0))/2)*fwd(min(column(slot,1))), slot);
2528// stroke(slot(15,29,7));
2529// Example: A cylindrical container with rounded edges and a rounded finger slot.
2530// function slot(slotwidth, slotheight, slotradius) = let(
2531// angle = 85,
2532// slot = round_corners(
2533// turtle([
2534// "right",
2535// "move", slotwidth,
2536// "left", angle,
2537// "move", 2*slotwidth,
2538// "right", angle,
2539// "move", slotheight,
2540// "left",
2541// "move", slotwidth,
2542// "left",
2543// "move", slotheight,
2544// "right", angle,
2545// "move", 2*slotwidth,
2546// "left", angle,
2547// "move", slotwidth
2548// ]),
2549// radius = [0,0,each repeat(slotradius,4),0,0], closed=false
2550// )
2551// ) apply(left(max(column(slot,0))/2)*fwd(min(column(slot,1))), slot);
2552// diam = 80;
2553// wall = 4;
2554// height = 40;
2555// rot(-90) {
2556// $fn=128;
2557// difference(){
2558// cyl(d=diam, rounding=wall/2, h=height, anchor=BOTTOM);
2559// up(wall)cyl(d=diam-2*wall, rounding1=wall, rounding2=-wall/2, h=height-wall+.01, anchor=BOTTOM);
2560// bent_cutout_mask(diam/2-wall/2, wall+.1, subdivide_path(apply(back(10),slot(15, 29, 7)),250));
2561// }
2562// }
2563function bent_cutout_mask(r, thickness, path, radius, convexity=10) = no_function("bent_cutout_mask");
2564module bent_cutout_mask(r, thickness, path, radius, convexity=10)
2565{
2566 no_children($children);
2567 r = get_radius(r1=r, r2=radius);
2568 dummy1=assert(is_def(r) && r>0,"Radius of the cylinder to bend around must be positive");
2569 path2 = force_path(path);
2570 dummy2=assert(is_path(path2,2),"Input path must be a 2D path")
2571 assert(r-thickness>0, "Thickness too large for radius")
2572 assert(thickness>0, "Thickness must be positive");
2573 fixpath = clockwise_polygon(path2);
2574 curvepoints = arc(d=thickness, angle = [-180,0]);
2575 profiles = [for(pt=curvepoints) _cyl_hole(r+pt.x,apply(xscale((r+pt.x)/r), offset(fixpath,delta=thickness/2+pt.y,check_valid=false,closed=true)))];
2576 pathx = column(fixpath,0);
2577 minangle = (min(pathx)-thickness/2)*360/(2*PI*r);
2578 maxangle = (max(pathx)+thickness/2)*360/(2*PI*r);
2579 mindist = (r+thickness/2)/cos((maxangle-minangle)/2);
2580 dummy3 = assert(maxangle-minangle<180,"Cutout angle span is too large. Must be smaller than 180.");
2581 zmean = mean(column(fixpath,1));
2582 innerzero = repeat([0,0,zmean], len(fixpath));
2583 outerpt = repeat( [1.5*mindist*cos((maxangle+minangle)/2),1.5*mindist*sin((maxangle+minangle)/2),zmean], len(fixpath));
2584 default_tag("remove")
2585 vnf_polyhedron(vnf_vertex_array([innerzero, each profiles, outerpt],col_wrap=true),convexity=convexity);
2586}
2587
2588
2589
2590/*
2591
2592join_prism To Do List:
2593
2594special handling for planar joins?
2595 offset method
2596 cut, radius?
2597Access to the derivative smoothing parameter?
2598
2599*/
2600
2601
2602
2603// Function&Module: join_prism()
2604// Synopsis: Join an arbitrary prism to a plane, sphere, cylinder or another arbitrary prism with a fillet.
2605// SynTags: Geom, VNF
2606// Topics: Rounding, Offsets
2607// See Also: offset_sweep(), convex_offset_extrude(), rounded_prism(), bent_cutout_mask(), join_prism()
2608// Usage: The two main forms with most common options
2609// join_prism(polygon, base, length=|height=|l=|h=, fillet=, [base_T=], [scale=], [prism_end_T=], [short=], ...) [ATTACHMENTS];
2610// join_prism(polygon, base, aux=, fillet=, [base_T=], [aux_T=], [scale=], [prism_end_T=], [short=], ...) [ATTACHMENTS];
2611// Usage: As function
2612// vnf = join_prism( ... );
2613// Description:
2614// This function creates a smooth fillet between one or both ends of an arbitrary prism and various other shapes: a plane, a sphere, a cylinder,
2615// or another arbitrary prism. The fillet is a continuous curvature rounding with a specified width/height. This module is very general
2616// and hence has a complex interface. The examples below form a tutorial on how to use `join_prism` that steps
2617// through the various options and how they affect the results. Be sure to check the examples for help understanding how the various options work.
2618// .
2619// When joining between planes this function produces similar results to {{rounded_prism()}}. This function works best when the prism
2620// cross section is a continuous shape with a high sampling rate and without sharp corners. If you have sharp corners you should consider
2621// giving them a small rounding first. When the prism cross section has concavities the fillet size will be limited by the curvature of those concavities.
2622// In contrast, {{rounded_prism()}} works best on a prism that has fewer points. A high sampling rate can lead to problems, and rounding
2623// over sharp corners leads to poor results.
2624// .
2625// You specify the prism by giving its cross section as a 2D path. The cross section will always be the orthogonal cross
2626// section of the prism. Depending on end conditions, the ends may not be perpendicular to the
2627// axis of the prism, but the cross section you give *is* always perpendicular to that cross section.
2628// Figure(3D,Big,NoScales,VPR=[74.6, 0, 329.7], VPT=[28.5524, 35.3006, 22.522], VPD=325.228): The layout and terminology used by `join_prism`. The "base object" is centered on the origin. The "auxiliary object" (if present) is some distance away so there is room for the "joiner prism" to connect the two objects. The blue line is the axis of the jointer prism. It will be at the origin of the shape you supply for defining that prism. The "root" point of the joiner prism is the point where the prism axis intersects the base. The prism end point is where the prism axis intersects the auxiliary object. If you don't give an auxiliary object then the prism end point is distance `length` along the axis from the root.
2629// aT = right(-10)*back(0)*up(75)*xrot(-35)*zrot(75);
2630// br = 17;
2631// ar = 15;
2632// xcyl(r=br, l=50, circum=true, $fn=64);
2633// multmatrix(aT)right(15)xcyl(r=ar,circum=true,l=50,$fn=64);
2634// %join_prism(circle(r=10), base = "cyl", base_r=br, aux="cyl", aux_r=ar, aux_T=aT,fillet=3);
2635// root = [-2.26667, 0, 17];
2636// rback = [15,0,25];
2637// endpt = [-7.55915, 0, 56.6937];
2638// endback = [10,0,55];
2639// stroke([root,endpt],
2640// width=1,endcap_width=3,endcaps="dot",endcap_color="red",color="blue",$fn=16);
2641// stroke(move(3*unit(rback-root), [rback,root]), endcap2="arrow2",width=1/2,$fn=16,color="black");
2642// down(0)right(4)color("black")move(rback)rot($vpr)text("prism root point",size=4);
2643// stroke(move(3*unit(endback-endpt), [endback,endpt]), endcap2="arrow2", width=1/2, $fn=16, color="black");
2644// down(2)right(4)color("black")move(endback)rot($vpr)text("prism end point",size=4);
2645// right(4)move(-20*[1,1])color("black")rot($vpr)text("base",size=8);
2646// up(83)right(-10)move(-20*[1,1])color("black")rot($vpr)text("aux",size=8);
2647// aend=[-13,13,30];
2648// ast=aend+10*[-1,1,0];
2649// stroke([ast,aend],endcap2="arrow2", width=1/2, color="black");
2650// left(2)move(ast)rot($vpr)color("black")text("joiner prism",size=5,anchor=RIGHT);
2651// Continues:
2652// You must include a base ("plane", "sphere", "cylinder", "cyl"), or a polygon describing the cross section of a base prism. If you specify a
2653// sphere or cylinder you must give `base_r` or `base_d` to specify the radius or diameter of the base object. If you choose a cylinder or a polygonal
2654// prism then the base object appears aligned with the X axis. In the case of the planar base, the
2655// joining prism will have one end of its axis at the origin. As shown above, the point where the joining prism attaches to its base is the "root" of the prism.
2656// If you use some other base shape, the root will be adjusted so that it is on the boundary of your shape. This happens by finding the intersection
2657// of the joiner prisms's axis and using that as the root. By default the prism axis is parallel to the Z axis.
2658// .
2659// You may give `base_T`, a rotation operator that will be applied to the base. This is
2660// useful to tilt a planar or cylindrical base. The `base_T` operator must be an origin-centered rotation like yrot(25).
2661// .
2662// You may optionally specify an auxiliary shape. When you do this, the joining prism connects the base to the auxiliary shape,
2663// which must be one of "none", "plane", "sphere", "cyl", or "cylinder". You can also set it to a polygon to create an arbitrary
2664// prism for the auxiliary shape. As is the case for the base, auxiliary cylinders and prisms appear oriented along the X axis.
2665// For a cylinder or sphere you must use `aux_r` or `aux_d` to specify the radius or diameter.
2666// The auxiliary shape appears centered on the origin and will most likely be invalid as an end location unless you translate it to a position
2667// away from the base object. The `aux_T` operator operates on the auxiliary object, and unlike `base_T` can be a rotation that includes translation
2668// operations (or is a non-centered rotation).
2669// .
2670// When you specify an auxiliary object, the joiner prism axis is initially the line connecting the origin (the base center point) to the auxiliary
2671// object center point. The joiner prism end point is determined analogously to how the root is determined, by intersecting the joiner
2672// prism axis with the auxiliary object. Note that this means that if `aux_T` is a rotation it will change the joiner prism root, because
2673// the rotated prism axis will intersect the base in a different location. If you do not give an auxiliary object then you must give
2674// the length/height parameter to specify the prism length. This gives the length of the prism measured from the root to the end point.
2675// Note that the joint with a curved base may significantly extend the length of the joiner prism: it's total length will often be larger than
2676// the length you request.
2677// .
2678// For the cylinder and spherical objects you may with to joint a prism to the concave surface. You can do this by setting a negative
2679// radius for the base or auxiliary object. When `base_r` is negative, and the joiner prism axis is vertical, the prism root will be **below** the
2680// XY plane. In this case it is actually possible to use the same object for base and aux and you can get a joiner prism that crosses a cylindrical
2681// or spherical hole.
2682// .
2683// When placing prisms inside a hole, an ambiguity can arise about how to identify the root and end of the joiner prism. The prism axis will have
2684// two intersections with a cylinder and both are potentially valid roots. When the auxiliary object is entirely inside the hole, or the auxiliary
2685// object is a sphere or cylinder with negative radius that intersections the base, both prism directions produce a valid
2686// joiner prism that meets the hole's concave surface, so two valid interpretations exist. By default, the longer prism will be returned.
2687// You can select the shorter prism by setting `short=true`. If you specify `short=true` when the base has a negative radius, but only one valid
2688// prism exists, you'll get an error, but it won't clearly identify that a bogus `short=true` was the real cause.
2689// .
2690// You can also alter your prism by using the `prism_end_T` operator which applies to the end point of the prism. It does not effect
2691// the root of the prism. The `prism_end_T` operator is applied in a coordinate system where the root of the
2692// prism is the origin, so if you set it to a rotation the prism base will stay rooted at the same location and the prism will rotate
2693// in the specified fashion. After `prism_end_T` is applied, the prism axis will probably be different and the resulting new end point will
2694// probably not be on the auxiliary object, or it will have changed the length of the prism. Therefore, the end point is recalculated
2695// to achieve the specified length (if aux is "none") or to contact the auxiliary object, if you have specified one. This means, for example,
2696// that setting `prism_end_T` to a scale operation won't change the result because it doesn't alter the prism axis.
2697// .
2698// The size of the fillets is determined by the fillet, `fillet_base`, and `fillet_aux` parameters. The fillet parameter will control both
2699// ends of the prism, or you can set the ends independently. The fillets must be nonnegative except when the prism joints a plane.
2700// In this case a negative fillet gives a roundover. In the case of no auxiliary object you can use `round_end` to round over the planar
2701// far end of the joiner prism. By default, the fillet is constructed using a method that produces a fillet with a uniform height along
2702// the joiner prism. This can be limiting when connectijng to objects with high curvature, so you can turn it off using the `uniform` option.
2703// See the figures below for an explanation of the uniform and non-uniform filleting methods.
2704// .
2705// The overlap is a potentially tricky parameter. It specifies how much extra material to
2706// create underneath the filleted prism so it overlaps the object that it joins to, ensuring valid unions.
2707// For joins to convex objects you can choose a small value, but when joining to a concave object the overlap may need to be
2708// very large to ensure that the base of the joiner prism is well-behaved. In such cases you may need to use an intersection
2709// remove excess base.
2710// Figure(2D,Med,NoAxes): Uniform fillet method. This image shows how the fillet we construct a uniform fillet. The pictures shows the cross section that is perpendicular to the prism. The blue curve represents the base object surface. The vertical line is the side of the prism. To construct a fillet we travel along the surface of the base, following the curve, until we have moved the fillet length, `a`. This defines the point `u`. We then construct a tangent line to the base and find its intersection, `v`, with the prism. Note that if the base is steeply curved, this tangent may fail to intersect, and the algorithm will fail with an error because `v` does not exist. Finally we locate `w` to be distance `a` above the point where the prism intersects the base object. The fillet is defined by the `[u,v,w]` triple and is shown in red. Note that with this method, the fillet is always height `a` above the base, so it makes a uniform curve parallel to the base object. However, when the base curvature is more extreme, point `v` may end up above point `w`, resulting in an invalid configuration. It also happens that point `v`, while below `w`, is very close to `w`, so the resulting fillet has an abrupt angle near `w` instead of a smooth transition.
2711// R=60;
2712// base = R*[cos(70),sin(70)];
2713// end = R*[cos(45),sin(45)];
2714// tang = [-sin(45),cos(45)];
2715// isect = line_intersection([base,back(1,base)], [end,end+tang]);
2716// toppt = base+[0,2*PI*R*25/360];
2717// bez = _smooth_bez_fill([toppt, isect,end], 0.8);
2718// color("red")
2719// stroke(bezier_curve(bez,30,endpoint=true), width=.5);
2720// color("blue"){
2721// stroke(arc(n=50,angle=[35,80], r=R), width=1);
2722// stroke([base, back(40,base)]);
2723// move(R*[cos(35),sin(35)])text("Base", size=5,anchor=BACK);
2724// back(1)move(base+[0,40]) text("Prism", size=5, anchor=FWD);
2725// }
2726// color([.3,1,.3]){
2727// right(2)move(toppt)text("w",size=5);
2728// right(2)move(end)text("u",size=5);
2729// stroke([isect+[1,1/4], isect+[16,4]], width=.5, endcap1="arrow2");
2730// move([16.5,3])move(isect)text("v",size=5);
2731// stroke([end,isect],dots=true);
2732// stroke([isect,toppt], dots=true);
2733// }
2734// color("black") {
2735// stroke(arc(n=50, angle=[45,70], r=R-3), color="black", width=.6, endcaps="arrow2");
2736// move( (R-10)*[cos(57.5),sin(57.5)]) text("a",size=5);
2737// left(3)move( base+[0,PI*R*25/360]) text("a", size=5,anchor=RIGHT);
2738// left(2)stroke( [base, toppt],endcaps="arrow2",width=.6);
2739// }
2740// Figure(2D,Med,NoAxes): Non-Uniform fillet method. This method differs because point `w` is found by moving the fillet distance `a` starting at the intersection point `v` instead of at the base surface. This means that the `[u,v,w]` triple is always in the correct order to produce a valid fillet. However, the height of the fillet above the surface will vary. When the base concave, point `v` is below the surface of the base, which in more extreme cases can produce a fillet that goes below the base surface. The uniform method is less likely to produce this kind of result. When the base surface is a plane, the uniform and non-uniform methods are identical.
2741// R=60;
2742// base = R*[cos(70),sin(70)];
2743// end = R*[cos(45),sin(45)];
2744// tang = [-sin(45),cos(45)];
2745// isect = line_intersection([base,back(1,base)], [end,end+tang]);
2746// toppt = isect+[0,2*PI*R*25/360];
2747// bez = _smooth_bez_fill([toppt, isect,end], 0.8);
2748// color("red")stroke(bezier_curve(bez,30,endpoint=true), width=.5);
2749// color("blue"){
2750// stroke(arc(n=50,angle=[35,80], r=R), width=1);
2751// stroke([base, back(40,base)]);
2752// move(R*[cos(35),sin(35)])text("Base", size=5,anchor=BACK);
2753// back(1)move(base+[0,40]) text("Prism", size=5, anchor=FWD);
2754// }
2755// color([.3,1,.3]){
2756// right(2)move(toppt)text("w",size=5);
2757// right(2)move(end)text("u",size=5);
2758// stroke([isect+[1,1/4], isect+[16,4]], width=.5, endcap1="arrow2");
2759// move([16.5,3])move(isect)text("v",size=5);
2760// stroke([end,isect],dots=true);
2761// stroke([isect,toppt], dots=true);
2762// }
2763// color("black") {
2764// stroke(arc(n=50, angle=[45,70], r=R-3), width=.6, endcaps="arrow2");
2765// move( (R-10)*[cos(57.5),sin(57.5)]) text("a",size=5);
2766// left(3)move( (isect+toppt)/2) text("a", size=5,anchor=RIGHT);
2767// left(2)stroke( [isect, toppt],endcaps="arrow2",width=.6);
2768// }
2769// Arguments:
2770// polygon = polygon giving prism cross section
2771// base = string specifying base object to join to ("plane","cyl","cylinder", "sphere") or a point list to use an arbitrary prism as the base.
2772// ---
2773// length / height / l / h = length/height of prism if aux=="none"
2774// scale = scale factor for prism far end. Default: 1
2775// prism_end_T = root-centered arbitrary transform to apply to the prism's far point. Default: IDENT
2776// short = flip prism direction for concave sphere or cylinder base, when there are two valid prisms. Default: false
2777// base_T = origin-centered rotation operator to apply to the base
2778// base_r / base_d = base radius or diameter if you picked sphere or cylinder
2779// aux = string specifying auxilary object to connect to ("none", "plane", "cyl", "cylinder", or "sphere") or a point list to use an arbitrary prism. Default: "none"
2780// aux_T = rotation operator that may include translation when aux is not "none" to apply to aux
2781// aux_r / aux_d = radius or diameter of auxiliary object if you picked sphere or cylinder
2782// n = number of segments in the fillet at both ends. Default: 15
2783// base_n = number of segments to use in fillet at the base
2784// aux_n = number of segments to use in fillet at the aux object
2785// end_n = number of segments to use in roundover at the end of prism with no aux object
2786// fillet = fillet for both ends of the prism (if applicable) Must be nonnegative except for joiner prisms with planar ends
2787// base_fillet = fillet for base end of prism
2788// aux_fillet = fillet for joint with aux object
2789// end_round = roundover of end of prism with no aux object
2790// overlap = amount of overlap of prism fillet into objects at both ends. Default: 1 for normal fillets, 0 for negative fillets and roundovers
2791// base_overlap = amount of overlap of prism fillet into the base object
2792// aux_overlap = amount of overlap of the prism fillet into aux object
2793// k = fillet curvature parameter for both ends of prism
2794// base_k = fillet curvature parameter for base end of prism
2795// end_k / aux_k = fillet curvature parameter for end of prism where the aux object is
2796// uniform = set to false to get non-uniform filleting at both ends (see Figures 2-3). Default: true
2797// base_uniform = set to false to get non-uniform filleting at the base
2798// aux_uniform = set to false to get non-uniform filleting at the auxiliary object
2799// debug = set to true to allow return of various cases where self-intersection was detected
2800// anchor = Translate so anchor point is at the origin. (module only) Default: "origin"
2801// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0
2802// orient = Vector to rotate top towards after spin (module only)
2803// atype = Select "hull" or "intersect" anchor types. (module only) Default: "hull"
2804// cp = Centerpoint for determining "intersect" anchors or centering the shape. Determintes the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. (module only) Default: "centroid"
2805// Named Anchors:
2806// "root" = Root point of the joiner prism, pointing out in the direction of the prism axis
2807// "end" = End point of the joiner prism, pointing out in the direction of the prism axis
2808// Example(3D,NoScales): Here is the simplest case, a circular prism with a specified length standing vertically on a plane.
2809// join_prism(circle(r=15,$fn=60),base="plane",
2810// length=18, fillet=3, n=12);
2811// cube([50,50,5],anchor=TOP);
2812// Example(3D,NoScales): Here we substitute an abitrary prism.
2813// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2814// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2815// join_prism(flower,base="plane",length=18, fillet=3, n=12);
2816// cube([50,50,5],anchor=TOP);
2817// Example(3D,NoScales): Here we apply a rotation of the prism, using prism_end_T, which rotates around the prism root. Note that aux_T will rotate around the origin, which is the same when the prism is joined to a plane.
2818// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2819// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2820// join_prism(flower,base="plane",length=18, fillet=3,
2821// n=12, prism_end_T=yrot(25));
2822// cube([50,50,5],anchor=TOP);
2823// Example(3D,NoScales): We can use `end_round` to get a roundover
2824// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2825// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2826// join_prism(flower,base="plane",length=18, fillet=3,
2827// n=12, prism_end_T=yrot(25), end_round=4);
2828// cube([50,50,5],anchor=TOP);
2829// Example(3D,NoScales): We can tilt the base plane by applying a base rotation. Note that because we did not tilt the prism, it still points upwards.
2830// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2831// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2832// join_prism(flower,base="plane",length=18, fillet=3,
2833// n=12, base_T=yrot(25));
2834// yrot(25)cube([50,50,5],anchor=TOP);
2835// Example(3D,NoScales): Next consider attaching the prism to a sphere. You must use a circumscribed sphere to avoid a lip or gap between the sphere and prism. Note that the prism is attached to the sphere's boundary above the origin and projects by the specified length away from the attachment point.
2836// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2837// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2838// join_prism(flower,base="sphere",base_r=30, length=18,
2839// fillet=3, n=12);
2840// spheroid(r=30,circum=true,$fn=64);
2841// Example(3D,NoScales): Rotating using the prism_end_T option rotates around the attachment point. Note that if you rotate too far, some points of the prism will miss the sphere, which is an error.
2842// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2843// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2844// join_prism(flower,base="sphere",base_r=30, length=18,
2845// fillet=3, n=12, prism_end_T=yrot(-15));
2846// spheroid(r=30,circum=true,$fn=64);
2847// Example(3D,NoScales): Rotating using the aux_T option rotates around the origin. You could get the same result in this case by rotating the whole model.
2848// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2849// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2850// join_prism(flower,base="sphere",base_r=30, length=18,
2851// fillet=3, n=12, aux_T=yrot(-45));
2852// spheroid(r=30,circum=true,$fn=64);
2853// Example(3D,NoScales): The origin in the prism cross section always aligns with the origin of the object you attach to. If you want to attach off center, then shift your prism cross section. If you shift too far so that parts of the prism miss the base object then you will get an error.
2854// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2855// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2856// join_prism(right(10,flower),base="sphere",base_r=30,
2857// length=18, fillet=3, n=12);
2858// spheroid(r=30,circum=true,$fn=64);
2859// Example(3D,NoScales): The third available base shape is the cylinder.
2860// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2861// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2862// join_prism(flower,base="cylinder",base_r=30,
2863// length=18, fillet=4, n=12);
2864// xcyl(r=30,l=75,circum=true,$fn=64);
2865// Example(3D,NoScales): You can rotate the cylinder the same way we rotated the plane.
2866// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2867// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2868// join_prism(flower,base="cylinder",base_r=30, length=18,
2869// fillet=4, n=12, base_T=zrot(33));
2870// zrot(33)xcyl(r=30,l=75,circum=true,$fn=64);
2871// Example(3D,NoScales): And you can rotate the prism around its attachment point with prism_end_T
2872// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2873// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2874// join_prism(flower,base="cylinder",base_r=30, length=18,
2875// fillet=4, n=12, prism_end_T=yrot(22));
2876// xcyl(r=30,l=75,circum=true,$fn=64);
2877// Example(3D,NoScales): Or you can rotate the prism around the origin with aux_T
2878// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2879// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2880// join_prism(flower,base="cylinder",base_r=30, length=18,
2881// fillet=4, n=12, aux_T=xrot(22));
2882// xcyl(r=30,l=75,circum=true,$fn=64);
2883// Example(3D,NoScales): Here's a prism where the scale changes
2884// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2885// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2886// join_prism(flower,base="cylinder",base_r=30, length=18,
2887// fillet=4, n=12,scale=.5);
2888// xcyl(r=30,l=75,circum=true,$fn=64);
2889// Example(3D,NoScales,VPD=190,VPR=[61.3,0,69.1],VPT=[41.8956,-9.49649,4.896]): Giving a negative radius attaches to the inside of a sphere or cylinder. Note you want the inscribed cylinder for the inner wall.
2890// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2891// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2892// join_prism(flower,base="cylinder",base_r=-30, length=18,
2893// fillet=4, n=12);
2894// bottom_half(z=-10)
2895// tube(ir=30,wall=3,l=74,$fn=64,orient=RIGHT,anchor=CENTER);
2896// Example(3D,NoScales,VPD=140,VPR=[72.5,0,73.3],VPT=[40.961,-19.8319,-3.03302]): A hidden problem lurks with concave attachments. The bottom of the prism does not follow the curvature of the base. Here you can see a gap. In some cases you can create a self-intersection in the prism.
2897// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2898// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2899// left_half(){
2900// join_prism(flower,base="cylinder",base_r=-30, length=18,
2901// fillet=4, n=12);
2902// bottom_half(z=-10)
2903// tube(ir=30,wall=3,l=74,$fn=64,orient=RIGHT,anchor=CENTER);
2904// }
2905// Example(3D,NoScales,VPD=140,VPR=[72.5,0,73.3],VPT=[40.961,-19.8319,-3.03302]): The solution to both problems is to increase the overlap parameter, but you may then have excess base that must be differenced or intersected away. In this case, an overlap of 2 is sufficient to eliminate the hole.
2906// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2907// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2908// left_half(){
2909// join_prism(flower,base="cylinder",base_r=-30, length=18,
2910// fillet=4, n=12, overlap=2);
2911// bottom_half(z=-10)
2912// tube(ir=30,wall=3,l=74,$fn=64,orient=RIGHT,anchor=CENTER);
2913// }
2914// Example(3D,NoScales,VPD=126,VPR=[76.7,0,111.1],VPT=[6.99093,2.52831,-14.8461]): Here is an example with a spherical base. This overlap is near the minimum required to eliminate the gap, but it creates a large excess structure around the base of the prism.
2915// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2916// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2917// left_half(){
2918// join_prism(flower,base="sphere",base_r=-30, length=18,
2919// fillet=4, n=12, overlap=7);
2920// bottom_half(z=-10) difference(){
2921// sphere(r=33,$fn=16);
2922// sphere(r=30,$fn=64);
2923// }
2924// }
2925// Example(3D,NoScales,VPD=126,VPR=[55,0,25],VPT=[1.23541,-1.80334,-16.9789]): Here is an example with a spherical base. This overlap is near the minimum required to eliminate the gap, but it creates a large excess structure around the base of the prism.
2926// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2927// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2928// intersection(){
2929// union(){
2930// join_prism(flower,base="sphere",base_r=-30, length=18,
2931// fillet=4, n=12, overlap=7);
2932// difference(){
2933// down(18)cuboid([68,68,30],anchor=TOP);
2934// sphere(r=30,$fn=64);
2935// }
2936// }
2937// sphere(r=33,$fn=16);
2938// }
2939// Example(3D,NoScales,VPD=126,VPR=[55,0,25],VPT=[1.23541,-1.80334,-16.9789]): As before, rotating with aux_T rotates around the origin.
2940// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2941// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2942// intersection(){
2943// union(){
2944// join_prism(flower,base="sphere",base_r=-30, length=18,
2945// fillet=4, n=12, overlap=7, aux_T=yrot(13));
2946// difference(){
2947// down(18)cuboid([68,68,30],anchor=TOP);
2948// sphere(r=30,$fn=64);
2949// }
2950// }
2951// sphere(r=33,$fn=16);
2952// }
2953// Example(3D,NoScales,VPD=102.06,VPR=[55,0,25],VPT=[3.96744,-2.80884,-19.9293]): Rotating with prism_end_T rotates around the attachment point. We shrank the prism to allow a significant rotation.
2954// flower = [for(theta=lerpn(0,360,180,endpoint=false))
2955// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
2956// intersection(){
2957// union(){
2958// join_prism(scale(.5,flower),base="sphere",base_r=-30,
2959// length=18, fillet=2, n=12, overlap=7,
2960// prism_end_T=yrot(25));
2961// difference(){
2962// down(23)cuboid([68,68,30],anchor=TOP);
2963// sphere(r=30,$fn=64);
2964// }
2965// }
2966// sphere(r=33,$fn=16);
2967// }
2968// Example(3D,NoScales,VPR=[65.5,0,105.3],VPT=[8.36329,13.0211,9.98397],VPD=237.091): You can create a prism that crosses the inside of a cylinder or sphere by giving the same negative radius twice and leaving both objects with the same center, as shown here.
2969// left_half(x=7){
2970// join_prism(circle(r=15),base="cylinder",base_r=-30, n=12,
2971// aux="cylinder", aux_r=-30, fillet=8, overlap=3);
2972// tube(ir=30,wall=5,l=74,$fn=64,orient=RIGHT,anchor=CENTER);
2973// }
2974// Example(3D,NoScales,VPR=[65.5,0,105.3],VPT=[8.36329,13.0211,9.98397],VPD=237.091): Here's a similar example with a plane for the auxiliary object. Note that we observe the 1 unit overlap on the top surface.
2975// left_half(x=7){
2976// join_prism(circle(r=15),base="cylinder",base_r=-30,
2977// aux="plane", fillet=8, n=12, overlap=3);
2978// tube(ir=30,wall=5,l=74,$fn=64,orient=RIGHT,anchor=CENTER);
2979// }
2980// Example(3D,NoScales,VPR=[65.5,0,105.3],VPT=[8.36329,13.0211,9.98397],VPD=237.091): We have tweaked the previous example just slightly by lowering the height of the plane. The result is a bit of a surprise: the prism flips upside down! This happens because there is an ambiguity in creating a prism between a plane and the inside of the cylinder. By default, this ambiguity is resolved by choosing the longer prism.
2981// left_half(x=7){
2982// join_prism(circle(r=15),base="cylinder",base_r=-30, n=12,
2983// aux="plane", aux_T=down(5), fillet=8, overlap=3);
2984// tube(ir=30,wall=5,l=74,$fn=64,orient=RIGHT,anchor=CENTER);
2985// }
2986// Example(3D,NoScales,VPR=[65.5,0,105.3],VPT=[8.36329,13.0211,9.98397],VPD=237.091): Adding `short=true` resolves the ambiguity of which prism to construct in the other way, by choosing the shorter option.
2987// left_half(x=7){
2988// join_prism(circle(r=15),base="cylinder",base_r=-30,
2989// aux="plane", aux_T=down(5), fillet=8,
2990// n=12, overlap=3, short=true);
2991// tube(ir=30,wall=5,l=74,$fn=64,orient=RIGHT,anchor=CENTER);
2992// }
2993// Example(3D,NoScales,VPR=[85.1,0,107.4],VPT=[8.36329,13.0211,9.98397],VPD=237.091): The problem does not arise in this case because the auxiliary object only allows one possible way to make the connection.
2994// left_half(x=7){
2995// join_prism(circle(r=15),base="cylinder",base_r=-30,
2996// aux="cylinder", aux_r=30, aux_T=up(20),
2997// fillet=8, n=12, overlap=3);
2998// tube(ir=30,wall=5,l=74,$fn=64,orient=RIGHT,anchor=CENTER);
2999// up(20)xcyl(r=30,l=74,$fn=64);
3000// }
3001// Example(3D,NoScales,VPT=[-1.23129,-3.61202,-0.249883],VPR=[87.9,0,295.7],VPD=213.382): When the aux cylinder is inside the base cylinder we can select the two options, shown here as red for the default and blue for the `short=true` case.
3002// color("red")
3003// join_prism(circle(r=5),base="cylinder",base_r=-30,
3004// aux="cyl",aux_r=10, aux_T=up(12), fillet=4,
3005// n=12, overlap=3, short=false);
3006// color("blue")
3007// join_prism(circle(r=5),base="cylinder",base_r=-30,
3008// aux="cyl",aux_r=10, aux_T=up(12), fillet=4,
3009// n=12, overlap=3, short=true);
3010// tube(ir=30,wall=5,$fn=64,l=18,orient=RIGHT,anchor=CENTER);
3011// up(12)xcyl(r=10, circum=true, l=18);
3012// Example(3D,NoScales,VPR=[94.9,0,106.7],VPT=[4.34503,1.48579,-2.32228],VPD=237.091): The same thing is true when you use a negative radius for the aux cylinder. This is the default long case.
3013// join_prism(circle(r=5,$fn=64),base="cylinder",base_r=-30,
3014// aux="cyl",aux_r=-10, aux_T=up(12), fillet=4,
3015// n=12, overlap=3, short=false);
3016// tube(ir=30,wall=5,l=24,$fn=64,orient=RIGHT,anchor=CENTER);
3017// up(12) top_half()
3018// tube(ir=10,wall=4,l=24,$fn=64,orient=RIGHT,anchor=CENTER);
3019// Example(3D,NoScales,VPR=[94.9,0,106.7],VPT=[4.34503,1.48579,-2.32228],VPD=237.091): And here is the short case:
3020// join_prism(circle(r=5,$fn=64),base="cylinder",base_r=-30,
3021// aux="cyl",aux_r=-10, aux_T=up(12), fillet=4,
3022// n=12, overlap=3, short=true);
3023// tube(ir=30,l=24,wall=5,$fn=64,orient=RIGHT,anchor=CENTER);
3024// up(12) bottom_half()
3025// tube(ir=10,wall=4,l=24,$fn=64,orient=RIGHT,anchor=CENTER);
3026// Example(3D,NoScales,VPR=[94.9,0,106.7],VPT=[0.138465,6.78002,24.2731],VPD=325.228): Another example where the cylinders overlap, with the long case here:
3027// auxT=up(40);
3028// join_prism(circle(r=5,$fn=64),base="cylinder",base_r=-30,
3029// aux="cyl",aux_r=-40, aux_T=auxT, fillet=4,
3030// n=12, overlap=3, short=false);
3031// tube(ir=30,wall=4,l=24,$fn=64,orient=RIGHT,anchor=CENTER);
3032// multmatrix(auxT)
3033// tube(ir=40,wall=4,l=24,$fn=64,orient=RIGHT,anchor=CENTER);
3034// Example(3D,NoScales,VPR=[94.9,0,106.7],VPT=[0.138465,6.78002,24.2731],VPD=325.228): And the short case:
3035// auxT=up(40);
3036// join_prism(circle(r=5,$fn=64),base="cylinder",base_r=-30,
3037// aux="cyl",aux_r=-40, aux_T=auxT, fillet=4,
3038// n=12, overlap=3, short=true);
3039// tube(ir=30,wall=4,l=24,$fn=64,orient=RIGHT,anchor=CENTER);
3040// multmatrix(auxT)
3041// tube(ir=40,wall=4,l=24,$fn=64,orient=RIGHT,anchor=CENTER);
3042// Example(3D,NoScales): Many of the preceeding examples feature a prism with a concave shape cross section. Concave regions can limit the amount of rounding that is possible. This occurs because the algorithm is not able to handle a fillet that intersects itself. Fillets on a convex prism always grow larger as they move away from the prism, so they cannot self intersect. This means that you can make the fillet as big as will fit on the base shape. The fillet will fail to fit if the tangent plane to the base at the fillet distance from the prism fails to intersect the prism. Here is an extreme example, almost the largest possible fillet to the convex elliptical convex prism.
3043// ellipse = ellipse([17,10],$fn=164);
3044// join_prism(ellipse,base="sphere",base_r=30, length=18,
3045// fillet=18, n=25, overlap=1);
3046// spheroid(r=30,circum=true, $fn=96);
3047// Example(3D,NoScales): This example shows a failed rounding attempt where the result is self-intersecting. Using the `debug=true` option makes it possible to view the result to understand what went wrong. Note that the concave corners have a crease where the fillet crosses itself. The error message will advise you to decrease the size of the fillet. You can also fix the problem by making your concave curves shallower.
3048// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3049// (15+2.5*sin(6*theta))*[cos(theta),sin(theta)]];
3050// join_prism(flower,base="cylinder",base_r=30, length=18,
3051// fillet=6, n=12, debug=true);
3052// Example(3D,NoScales): Your prism needs to be finely sampled enough to follow the contour of the base you are attaching it to. If it is not, you get a result like this. The fillet joints the prism smoothly, but makes a poor transition to the sphere.
3053// sq = rect(15);
3054// join_prism(sq, base="sphere", base_r=25,
3055// length=18, fillet=4, n=12);
3056// spheroid(r=25, circum=true, $fn=96);
3057// Example(3D,NoScales): To fix the problem, you must subdivide the polygon that defines the prism. But note that the join_prism method works poorly at sharp corners.
3058// sq = subdivide_path(rect(15),n=64);
3059// join_prism(sq, base="sphere", base_r=25,
3060// length=18, fillet=4, n=12);
3061// spheroid(r=25, circum=true,$fn=96);
3062// Example(3D,NoScales): In the previous example, a small rounding of the prism corners produces a nicer result.
3063// sq = subdivide_path(
3064// round_corners(rect(15),cut=.5,$fn=32),
3065// n=128);
3066// join_prism(sq, base="sphere", base_r=25,
3067// length=18, fillet=4, n=12);
3068// spheroid(r=25, circum=true,$fn=96);
3069// Example(3D,NoScales): The final option for specifying the base is to use an arbitrary prism, specified by a polygon. Note that the base prism is oriented to the RIGHT, so the attached prism remains Z oriented.
3070// ellipse = ellipse([17,10],$fn=164);
3071// join_prism(zrot(90,ellipse), base=2*ellipse, length=19,
3072// fillet=4, n=12);
3073// linear_sweep(2*ellipse,height=60, center=true, orient=RIGHT);
3074// Example(3D,NoScales): As usual, you can rotate around the attachment point using prism_end_T.
3075// ellipse = ellipse([17,10],$fn=164);
3076// join_prism(zrot(90,ellipse), base=2*ellipse, length=19,
3077// fillet=4, n=12, prism_end_T=yrot(22));
3078// linear_sweep(2*ellipse,height=60, center=true, orient=RIGHT);
3079// Example(3D,NoScales): And you can rotate around the origin with aux_T.
3080// ellipse = ellipse([17,10],$fn=164);
3081// join_prism(zrot(90,ellipse), base=2*ellipse, length=19,
3082// fillet=4, n=12, aux_T=yrot(22));
3083// linear_sweep(2*ellipse,height=60, center=true, orient=RIGHT);
3084// Example(3D,NoScales): The base prism can be a more complicated shape.
3085// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3086// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3087// join_prism(flower,base=1.4*flower, fillet=3,
3088// n=15, length=20);
3089// linear_sweep(1.4*flower,height=60,center=true,
3090// convexity=10,orient=RIGHT);
3091// Example(3D,NoScales): Here's an example with both prism_end_T and aux_T
3092// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3093// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3094// join_prism(flower,base=1.4*flower, length=20,
3095// prism_end_T=yrot(20),aux_T=xrot(10),
3096// fillet=3, n=25);
3097// linear_sweep(1.4*flower,height=60,center=true,
3098// convexity=10,orient=RIGHT);
3099// Example(3D,NoScales,VPR=[78,0,42],VPT=[12.45,-12.45,10.4],VPD=130): Instead of terminating your prism in a flat face perpendicular to its axis you can attach it to a second object. The simplest case is to connect to planar attachments. When connecting to a second object you must position and orient the second object using aux_T, which is now allowed to be a rotation and translation operator. The `length` parameter is no longer allowed.
3100// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3101// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3102// join_prism(flower,base="plane", fillet=4, n=12,
3103// aux="plane", aux_T=up(12));
3104// %up(12)cuboid([40,40,4],anchor=BOT);
3105// cuboid([40,40,4],anchor=TOP);
3106// Example(3D,NoScales,VPR=[78,0,42],VPT=[12.45,-12.45,10.4],VPD=130): Here's an example where the second object is rotated. Note that the prism will go from the origin to the origin point of the object. In this case because the rotation is applied first, the prism is vertical.
3107// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3108// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3109// aux_T = up(12)*xrot(-22);
3110// join_prism(flower,base="plane",fillet=4, n=12,
3111// aux="plane", aux_T=aux_T);
3112// multmatrix(aux_T)cuboid([42,42,4],anchor=BOT);
3113// cuboid([40,40,4],anchor=TOP);
3114// Example(3D,NoScales,VPR=[78,0,42],VPT=[12.45,-12.45,10.4],VPD=130): In this example, the aux_T transform moves the centerpoint (origin) of the aux object, and the resulting prism connects centerpoints, so it is no longer vertical.
3115// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3116// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3117// aux_T = xrot(-22)*up(12);
3118// join_prism(flower,base="plane",fillet=4, n=12,
3119// aux="plane", aux_T=aux_T);
3120// multmatrix(aux_T)cuboid([42,42,4],anchor=BOT);
3121// cuboid([43,43,4],anchor=TOP);
3122// Example(3D,NoScales,VPR=[78,0,42],VPT=[9.95,-9.98,13.0],VPD=142]): You can combine with base_T
3123// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3124// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3125// aux_T = xrot(-22)*up(22);
3126// base_T = xrot(5)*yrot(-12);
3127// join_prism(flower,base="plane",base_T=base_T,
3128// aux="plane",aux_T=aux_T, fillet=4, n=12);
3129// multmatrix(aux_T)cuboid([42,42,4],anchor=BOT);
3130// multmatrix(base_T)cuboid([45,45,4],anchor=TOP);
3131// Example(3D,NoScales,VPR=[76.6,0,29.4],VPT=[11.4009,-8.43978,16.1934],VPD=157.778): Using prism_end_T shifts the prism's end without tilting the plane, so the prism ends are not perpendicular to the prism axis.
3132// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3133// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3134// join_prism(flower,base="plane", prism_end_T=right(14),
3135// aux="plane",aux_T=up(24), fillet=4, n=12);
3136// right(7){
3137// %up(24)cuboid([65,42,4],anchor=BOT);
3138// cuboid([65,42,4],anchor=TOP);
3139// }
3140// Example(3D,NoAxes,NoScales,VPR=[101.9, 0, 205.6], VPT=[5.62846, -5.13283, 12.0751], VPD=102.06): Negative fillets give roundovers and are pemitted only for joints to planes. Note that overlap defaults to zero for negative fillets.
3141// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3142// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3143// aux_T = xrot(-22)*up(22);
3144// base_T = xrot(5)*yrot(-12);
3145// join_prism(flower,base="plane",base_T=base_T,
3146// aux="plane", aux_T=aux_T, fillet=-4,n=12);
3147// Example(3D,NoScales,VPR=[84,0,21],VPT=[13.6,-1,46.8],VPD=446): It works the same way with the other shapes, but make sure you move the shapes far enough apart that there is room for a prism.
3148// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3149// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3150// aux_T = up(85);
3151// base_T = xrot(5)*yrot(-12);
3152// join_prism(flower,base="cylinder",base_r=25, fillet=4, n=12,
3153// aux="sphere",aux_r=35,base_T=base_T, aux_T=aux_T);
3154// multmatrix(aux_T)spheroid(35,circum=true);
3155// multmatrix(base_T)xcyl(l=75,r=25,circum=true);
3156// Example(3D,NoScales,VPR=[84,0,21],VPT=[13.6,-1,46.8],VPD=446): Here we translate the sphere to the right and the prism goes with it
3157// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3158// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3159// aux_T = right(40)*up(85);
3160// join_prism(flower,base="cylinder",base_r=25, n=12,
3161// aux="sphere",aux_r=35, aux_T=aux_T, fillet=4);
3162// multmatrix(aux_T)spheroid(35,circum=true);
3163// xcyl(l=75,r=25,circum=true);
3164// Example(3D,NoScales,VPR=[84,0,21],VPT=[13.6,-1,46.8],VPD=446): This is the previous example with the prism_end_T transformation used to shift the far end of the prism away from the sphere center. Note that prism_end_T can be any transformation, but it just acts on the location of the prism endpoint to shift the direction the prism points.
3165// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3166// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3167// aux_T = right(40)*up(85);
3168// join_prism(flower,base="cylinder",base_r=25,
3169// prism_end_T=left(4), fillet=3, n=12,
3170// aux="sphere",aux_r=35, aux_T=aux_T);
3171// multmatrix(aux_T)spheroid(35,circum=true);
3172// xcyl(l=75,r=25,circum=true);
3173// Example(3D,NoScales,VPR=[96.9,0,157.5],VPT=[-7.77616,-2.272,37.9424],VPD=366.527): Here the base is a cylinder but the auxilary object is a generic prism, and the joiner prism has a scale factor.
3174// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3175// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3176// aux_T = up(85)*zrot(-75);
3177// ellipse = ellipse([17,10],$fn=164);
3178// join_prism(flower,base="cylinder",base_r=25,
3179// fillet=4, n=12,
3180// aux=ellipse, aux_T=aux_T,scale=.5);
3181// multmatrix(aux_T)
3182// linear_sweep(ellipse,orient=RIGHT,height=75,center=true);
3183// xcyl(l=75,r=25,circum=true,$fn=100);
3184// Example(3D,NoAxes,VPT=[10.0389,1.71153,26.4635],VPR=[89.3,0,39],VPD=237.091): Base and aux are both a general prism in this case.
3185// ellipse = ellipse([10,17]/2,$fn=96);
3186// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3187// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3188// aux_T=up(50);
3189// join_prism(ellipse,base=flower,aux_T=aux_T,aux=flower,
3190// fillet=3, n=12, prism_end_T=right(9));
3191// multmatrix(aux_T)
3192// linear_sweep(flower,height=60,center=true,orient=RIGHT);
3193// linear_sweep(flower,height=60,center=true,orient=RIGHT);
3194// Example(3D,NoAxes,VPT=[8.57543,0.531762,26.8046],VPR=[89.3,0,39],VPD=172.84): Shifting the joiner prism forward brings it close to a steeply curved edge of the auxiliary prism at the top. Note that a funny looking bump with a sharp corner has appeared in the fillet. This bump/corner is a result of the uniform filleting method running out of space. If we move the joiner prism farther forward, the algorithm fails completely.
3195// ellipse = ellipse([10,17]/2,$fn=96);
3196// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3197// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3198// aux_T=up(50);
3199// join_prism(ellipse,base=flower,aux_T=aux_T,aux=flower,
3200// fillet=3, n=12, prism_end_T=fwd(1.6));
3201// multmatrix(aux_T)
3202// linear_sweep(flower,height=60,center=true,orient=RIGHT);
3203// linear_sweep(flower,height=60,center=true,orient=RIGHT);
3204// Example(3D,NoAxes,VPT=[8.57543,0.531762,26.8046],VPR=[89.3,0,39],VPD=172.84): This is the same example as above but with uniform turned off. Note how the line the fillet makes on the joiner prism is not uniform, but the overall curved shape is more pleasing than the previous result, and we can bring the joiner prism a little farther forward and still construct a model.
3205// ellipse = ellipse([10,17]/2,$fn=96);
3206// flower = [for(theta=lerpn(0,360,180,endpoint=false))
3207// (15+1.3*sin(6*theta))*[cos(theta),sin(theta)]];
3208// aux_T=up(50);
3209// join_prism(ellipse,base=flower,aux_T=aux_T,aux=flower,
3210// fillet=3, n=12, prism_end_T=fwd(1.7),
3211// uniform=false);
3212// multmatrix(aux_T)
3213// linear_sweep(flower,height=60,center=true,orient=RIGHT);
3214// linear_sweep(flower,height=60,center=true,orient=RIGHT);
3215// Example(3D): Positioning a joiner prism as an attachment
3216// cuboid([20,30,40])
3217// attach(RIGHT,"root")
3218// join_prism(circle(r=8,$fn=32),
3219// l=10, base="plane", fillet=4);
3220module join_prism(polygon, base, base_r, base_d, base_T=IDENT,
3221 scale=1, prism_end_T=IDENT, short=false,
3222 length, l, height, h,
3223 aux="none", aux_T=IDENT, aux_r, aux_d,
3224 overlap, base_overlap,aux_overlap,
3225 n=15, base_n, end_n, aux_n,
3226 fillet, base_fillet,aux_fillet,end_round,
3227 k=0.7, base_k,aux_k,end_k,
3228 uniform=true, base_uniform, aux_uniform,
3229 debug=false, anchor="origin", extent=true, cp="centroid", atype="hull", orient=UP, spin=0,
3230 convexity=10)
3231{
3232 assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"");
3233 vnf_start_end = join_prism(polygon,base, base_r=base_r, base_d=base_d, base_T=base_T,
3234 scale=scale, prism_end_T=prism_end_T, short=short,
3235 length=length, l=l, height=height, h=h,
3236 aux=aux, aux_T=aux_T, aux_r=aux_r, aux_d=aux_d,
3237 overlap=overlap, base_overlap=base_overlap, aux_overlap=aux_overlap,
3238 n=n,base_n=base_n, end_n=end_n, aux_n=aux_n,
3239 fillet=fillet, base_fillet=base_fillet, aux_fillet=aux_fillet, end_round=end_round,
3240 k=k, base_k=base_k, aux_k=aux_k, end_k=end_k,
3241 uniform=uniform, base_uniform=base_uniform, aux_uniform=aux_uniform,
3242 debug=debug,
3243 return_axis=true
3244 );
3245 axis = vnf_start_end[2] - vnf_start_end[1];
3246 anchors = [
3247 named_anchor("root",vnf_start_end[1], -axis),
3248 named_anchor("end",vnf_start_end[2], axis)
3249 ];
3250 attachable(anchor,spin,orient,vnf=vnf_start_end[0], extent=atype=="hull", cp=cp, anchors=anchors) {
3251 vnf_polyhedron(vnf_start_end[0],convexity=convexity);
3252 children();
3253 }
3254}
3255
3256
3257
3258function join_prism(polygon, base, base_r, base_d, base_T=IDENT,
3259 scale=1, prism_end_T=IDENT, short=false,
3260 length, l, height, h,
3261 aux="none", aux_T=IDENT, aux_r, aux_d,
3262 overlap, base_overlap,aux_overlap,
3263 n=15, base_n, aux_n, end_n,
3264 fillet, base_fillet,aux_fillet,end_round,
3265 k=0.7, base_k,aux_k,end_k,
3266 uniform=true, base_uniform, aux_uniform,
3267 debug=false, return_axis=false) =
3268 let(
3269 objects=["cyl","cylinder","plane","sphere"],
3270 length = one_defined([h,height,l,length], "h,height,l,length", dflt=undef)
3271 )
3272 assert(is_path(polygon,2),"Prism polygon must be a 2d path")
3273 assert(is_rotation(base_T,3,centered=true),"Base transformation must be a rotation around the origin")
3274 assert(is_rotation(aux_T,3),"Aux transformation must be a rotation")
3275 assert(aux!="none" || is_rotation(aux_T,centered=true), "With no aux, aux_T must be a rotation centered on the origin")
3276 assert(is_matrix(prism_end_T,4), "Prism endpoint transformation is invalid")
3277 assert(aux!="none" || (is_num(length) && length>0),"With no aux must give positive length")
3278 assert(aux=="none" || is_undef(length), "length parameter allowed only when aux is \"none\"")
3279 assert(aux=="none" || is_path(aux,2) || in_list(aux,objects), "Unknown aux type")
3280 assert(is_path(base,2) || in_list(base,objects), "Unknown base type")
3281 assert(is_undef(length) || (is_num(length) && length>0), "Prism length must be positive")
3282 assert(is_num(scale) && scale>=0, "Prism scale must be non-negative")
3283 assert(num_defined([end_k,aux_k])<2, "Cannot define both end_k and aux_k")
3284 assert(num_defined([end_n,aux_n])<2, "Cannot define both end_n and aux_n")
3285 let(
3286 base_r = get_radius(r=base_r,d=base_d),
3287 aux_r = get_radius(r=aux_r,d=aux_d),
3288 base_k= first_defined([base_k,k]),
3289 aux_k = first_defined([end_k,aux_k,k]),
3290 aux_n = first_defined([end_n,aux_n,n]),
3291 base_n = first_defined([base_n,n]),
3292 base_fillet = one_defined([fillet,base_fillet],"fillet,base_fillet"),
3293 aux_fillet = aux=="none" ? one_defined([aux_fillet,u_mul(-1,end_round)],"aux_fillet,end_round",0)
3294 : one_defined([fillet,aux_fillet],"fillet,aux_fillet"),
3295 base_overlap = one_defined([base_overlap,overlap],"base_overlap,overlap",base_fillet>0?1:0),
3296 aux_overlap = one_defined([aux_overlap,overlap],"aux_overlap,overlap",aux_fillet>0?1:0),
3297 base_uniform = first_defined([base_uniform, uniform]),
3298 aux_uniform = first_defined([aux_uniform, uniform])
3299 )
3300 assert(is_num(base_fillet),"Must give a numeric fillet or base_fillet value")
3301 assert(base=="plane" || base_fillet>=0, "Fillet for non-planar base object must be nonnegative")
3302 assert(is_num(aux_fillet), "Must give numeric fillet or aux_fillet")
3303 assert(in_list(aux,["none","plane"]) || aux_fillet>=0, "Fillet for aux object must be nonnegative")
3304 assert(!in_list(base,["sphere","cyl","cylinder"]) || (is_num(base_r) && !approx(base_r,0)), str("Must give nonzero base_r with base ",base))
3305 assert(!in_list(aux,["sphere","cyl","cylinder"]) || (is_num(aux_r) && !approx(aux_r,0)), str("Must give nonzero aux_r with base ",base))
3306 assert(!short || (in_list(base,["sphere","cyl","cylinder"]) && base_r<0), "You can only set short to true if the base is a sphere or cylinder with radius<0")
3307 let(
3308 base_r=default(base_r,0),
3309 polygon=clockwise_polygon(polygon),
3310 start_center = CENTER,
3311 dir = aux=="none" ? apply(aux_T,UP)
3312 : apply(aux_T,CENTER) == CENTER ? apply(aux_T,UP)
3313 : apply(aux_T,CENTER),
3314 flip = short ? -1 : 1,
3315 start = base=="sphere" ?
3316 let( answer = _sphere_line_isect_best(abs(base_r),[CENTER,flip*dir], sign(base_r)*flip*dir))
3317 assert(answer,"Prism center doesn't intersect sphere (base)")
3318 answer
3319 : base=="cyl" || base=="cylinder" ?
3320 let(
3321 mapped = apply(yrot(90),[CENTER,flip*dir]),
3322 answer = _cyl_line_intersection(abs(base_r),mapped,sign(base_r)*mapped[1])
3323 )
3324 assert(answer,"Prism center doesn't intersect cylinder (base)")
3325 apply(yrot(-90),answer)
3326 : is_path(base) ?
3327 let(
3328 mapped = apply(yrot(90),[CENTER,flip*dir]),
3329 answer = _prism_line_isect(pair(base,wrap=true),mapped,mapped[1])[0]
3330 )
3331 assert(answer,"Prism center doesn't intersect prism (base)")
3332 apply(yrot(-90),answer)
3333 : start_center,
3334 aux_T = aux=="none" ? move(start)*prism_end_T*move(-start)*move(length*dir)*move(start)
3335 : aux_T,
3336 prism_end_T = aux=="none" ? IDENT : prism_end_T,
3337 aux = aux=="none" && aux_fillet!=0 ? "plane" : aux,
3338 end_center = apply(aux_T,CENTER),
3339 ndir = base_r<0 ? unit(start_center-start) : unit(end_center-start_center,UP),
3340 end_prelim = apply(move(start)*prism_end_T*move(-start),
3341 aux=="sphere" ?
3342 let( answer = _sphere_line_isect_best(abs(aux_r), [start,start+ndir], -sign(aux_r)*ndir))
3343 assert(answer,"Prism center doesn't intersect sphere (aux)")
3344 apply(aux_T,answer)
3345 : aux=="cyl" || aux=="cylinder" ?
3346 let(
3347 mapped = apply(yrot(90)*rot_inverse(aux_T),[start,start+ndir]),
3348 answer = _cyl_line_intersection(abs(aux_r),mapped, -sign(aux_r)*(mapped[1]-mapped[0]))
3349 )
3350 assert(answer,"Prism center doesn't intersect cylinder (aux)")
3351 apply(aux_T*yrot(-90),answer)
3352 : is_path(aux) ?
3353 let(
3354 mapped = apply(yrot(90),[start,start+ndir]),
3355 answer = _prism_line_isect(pair(aux,wrap=true),mapped,mapped[0]-mapped[1])[0]
3356 )
3357 assert(answer,"Prism center doesn't intersect prism (aux)")
3358 apply(aux_T*yrot(-90),answer)
3359 : end_center
3360 ),
3361 end = prism_end_T == IDENT ? end_prelim
3362 : aux=="sphere" ?
3363 let( answer = _sphere_line_isect_best(abs(aux_r), move(-end_center,[start,end_prelim]), -sign(aux_r)*(end_prelim-start)))
3364 assert(answer,"Prism center doesn't intersect sphere (aux)")
3365 answer+end_center
3366 : aux=="cyl" || aux=="cylinder" ?
3367 let(
3368 mapped = apply(yrot(90)*move(-end_center),[start,end_prelim]),
3369 answer = _cyl_line_intersection(abs(aux_r),mapped, -sign(aux_r)*(mapped[1]-mapped[0]))
3370 )
3371 assert(answer,"Prism center doesn't intersect cylinder (aux)")
3372 apply(move(end_center)*yrot(-90),answer)
3373 : is_path(aux) ?
3374 let(
3375 mapped = apply(yrot(90)*move(-end_center),[start,end_prelim]),
3376 answer = _prism_line_isect(pair(aux,wrap=true),mapped,mapped[0]-mapped[1])[0]
3377 )
3378 assert(answer,"Prism center doesn't intersect prism (aux)")
3379 apply(move(end_center)*yrot(-90),answer)
3380 : plane_line_intersection( plane_from_normal(apply(aux_T,UP), end_prelim),[start,end_prelim]),
3381 pangle = rot(from=UP, to=end-start),
3382 truetop = apply(move(start)*pangle,path3d(scale(scale,polygon),norm(start-end))),
3383 truebot = apply(move(start)*pangle,path3d(polygon)),
3384 base_trans = rot_inverse(base_T),
3385 base_top = apply(base_trans, truetop),
3386 base_bot = apply(base_trans, truebot),
3387 botmesh = apply(base_T,_prism_fillet("base", base, base_r, base_bot, base_top, base_fillet, base_k, n, base_overlap,base_uniform,debug)),
3388 aux_trans = rot_inverse(aux_T),
3389 aux_top = apply(aux_trans, reverse_polygon(truetop)),
3390 aux_bot = apply(aux_trans, reverse_polygon(truebot)),
3391 topmesh_reversed = _prism_fillet("aux",aux, aux_r, aux_top, aux_bot, aux_fillet, aux_k, n, aux_overlap,aux_uniform,debug),
3392 topmesh = apply(aux_T,[for(i=[len(topmesh_reversed)-1:-1:0]) reverse_polygon(topmesh_reversed[i])]),
3393 round_dir = select(topmesh,-1)-botmesh[0],
3394 roundings_cross = [for(i=idx(topmesh)) if (round_dir[i]*(truetop[i]-truebot[i])<0) i],
3395 vnf = vnf_vertex_array(concat(topmesh,botmesh),col_wrap=true, caps=true, reverse=true)
3396 )
3397 assert(debug || roundings_cross==[],"Roundings from the two ends cross on the prism: decrease size of roundings")
3398 return_axis ? [vnf,start,end] : vnf;
3399
3400function _fix_angle_list(list,ind=0, result=[]) =
3401 ind==0 ? _fix_angle_list(list,1,[list[0]])
3402 : ind==len(list) ? result
3403 : list[ind]-result[ind-1]>90 ? _fix_angle_list(list,ind+1,concat(result,[list[ind]-360]))
3404 : list[ind]-result[ind-1]<-90 ? _fix_angle_list(list,ind+1,concat(result,[list[ind]+360]))
3405 : _fix_angle_list(list,ind+1,concat(result,[list[ind]]));
3406
3407
3408
3409// intersection with cylinder of radius R oriented on Z axis, with infinite extent
3410// if ref is given, return point with larger inner product with ref.
3411function _cyl_line_intersection(R, line, ref) =
3412 let(
3413 line2d = path2d(line),
3414 cisect = circle_line_intersection(r=R, cp=[0,0], line=line2d)
3415 )
3416 len(cisect)<2 ? [] :
3417 let(
3418 linevec = line2d[1]-line2d[0],
3419 dz = line[1].z-line[0].z,
3420 pts = [for(pt=cisect)
3421 let(t = (pt-line2d[0])*linevec/(linevec*linevec)) // position parameter for line
3422 [pt.x,pt.y,dz * t + line[0].z]]
3423 )
3424 is_undef(ref) ? pts :
3425 let(
3426 dist = [for(pt=pts) ref*pt]
3427 )
3428 dist[0]>dist[1] ? pts[0] : pts[1];
3429
3430
3431function _sphere_line_isect_best(R, line, ref) =
3432 let(
3433 pts = sphere_line_intersection(abs(R), [0,0,0], line=line)
3434 )
3435 len(pts)<2 ? [] :
3436 let(
3437 dist = [for(pt=pts) ref*pt]
3438 )
3439 dist[0]>dist[1] ? pts[0] : pts[1];
3440
3441// First input is all the pairs of the polygon, e.g. pair(poly,wrap=true)
3442// Unlike the others this returns [point, ind, u], where point is the actual intersection
3443// point, ind ind and u are the segment index and u value. Prism is z-aligned.
3444function _prism_line_isect(poly_pairs, line, ref) =
3445 let(
3446 line2d = path2d(line),
3447 ref=point2d(ref),
3448 ilist = [for(j=idx(poly_pairs))
3449 let(segisect = _general_line_intersection(poly_pairs[j],line2d))
3450 if (segisect && segisect[1]>=-EPSILON && segisect[1]<=1+EPSILON)
3451 [segisect[0],j,segisect[1],segisect[0]*ref]]
3452 )
3453 len(ilist)==0 ? [] :
3454 let (
3455 ind = max_index(column(ilist,3)),
3456 isect2d = ilist[ind][0],
3457 isect_ind = ilist[ind][1],
3458 isect_u = ilist[ind][2],
3459 slope = (line[1].z-line[0].z)/norm(line[1]-line[0]),
3460 z = slope * norm(line2d[0]-isect2d) + line[0].z
3461 )
3462 [point3d(isect2d,z),isect_ind, isect_u];
3463
3464
3465function _prism_fillet(name, base, R, bot, top, d, k, N, overlap,uniform,debug) =
3466 base=="none" ? [bot]
3467 : base=="plane" ? _prism_fillet_plane(name,bot, top, d, k, N, overlap,debug)
3468 : base=="cyl" || base=="cylinder" ? _prism_fillet_cyl(name, R, bot, top, d, k, N, overlap,uniform,debug)
3469 : base=="sphere" ? _prism_fillet_sphere(name, R, bot, top, d, k, N, overlap,uniform,debug)
3470 : is_path(base,2) ? _prism_fillet_prism(name, base, bot, top, d, k, N, overlap,uniform,debug)
3471 : assert(false,"Unknown base type");
3472
3473function _prism_fillet_plane(name, bot, top, d, k, N, overlap,debug) =
3474 let(
3475 dir = sign(top[0].z-bot[0].z),
3476 isect = [for (i=idx(top)) plane_line_intersection([0,0,1,0], [top[i],bot[i]])],
3477 base_normal = -path3d(path_normals(path2d(isect), closed=true)),
3478 mesh = transpose([for(i=idx(top))
3479 let(
3480
3481 base_angle = vector_angle(top[i],isect[i],isect[i]+sign(d)*base_normal[i]),
3482 // joint length
3483 // d = r,
3484 r=abs(d)*tan(base_angle/2),
3485 // radius
3486 //d = r/tan(base_angle/2),
3487 // cut
3488 //r = r / (1/sin(base_angle/2) - 1),
3489 //d = r/tan(base_angle/2),
3490 prev = unit(top[i]-isect[i]),
3491 next = sign(d)*dir*base_normal[i],
3492 center = r/sin(base_angle/2) * unit(prev+next) + isect[i]
3493 )
3494 [
3495 each arc(N, cp=center, points = [isect[i]+prev*abs(d), isect[i]+next*d]),
3496 isect[i]+next*d+[0,0,-overlap*dir]
3497 ]
3498 ])
3499 )
3500 assert(debug || is_path_simple(path2d(select(mesh,-2)),closed=true),"Fillet doesn't fit: it intersects itself")
3501 mesh;
3502
3503function _prism_fillet_plane(name, bot, top, d, k, N, overlap,debug) =
3504 let(
3505 dir = sign(top[0].z-bot[0].z), // Negative if we are upside down, with "top" below "bot"
3506 isect = [for (i=idx(top)) plane_line_intersection([0,0,1,0], [top[i],bot[i]])]
3507 )
3508 d==0 ? [isect, if (overlap!=0) isect + overlap*dir*DOWN] :
3509 let(
3510 base_normal = -path3d(path_normals(path2d(isect), closed=true)),
3511 mesh = transpose([for(i=idx(top))
3512 assert(norm(top[i]-isect[i])>=d,"Prism is too short for fillet to fit")
3513 let(
3514 d_step = isect[i]+abs(d)*unit(top[i]-isect[i]),
3515 edgepoint = isect[i]+d*dir*base_normal[i],
3516 bez = _smooth_bez_fill([d_step, isect[i], edgepoint],k)
3517 )
3518 [
3519 each bezier_curve(bez,N,endpoint=true),
3520 if (overlap!=0) edgepoint + overlap*dir*DOWN
3521 ]
3522 ])
3523 )
3524 assert(debug || is_path_simple(path2d(select(mesh,-2)),closed=true),"Fillet doesn't fit: it intersects itself")
3525 mesh;
3526
3527
3528// This function was written for a z-aligned cylinder but the actual
3529// upstream assumption is an x-aligned cylinder, so input is rotated and
3530// output is un-rotated.
3531function _prism_fillet_cyl(name, R, bot, top, d, k, N, overlap, uniform, debug) =
3532 let(
3533 top = yrot(-90,top),
3534 bot = yrot(-90,bot),
3535 isect = [for (i=idx(top))
3536 let (cisect = _cyl_line_intersection(abs(R), [top[i],bot[i]], sign(R)*(top[i]-bot[i])))
3537 assert(cisect, str("Prism doesn't fully intersect cylinder (",name,")"))
3538 cisect
3539 ]
3540 )
3541 d==0 ? [
3542 isect,
3543 if (overlap!=0) [for(p=isect) point3d(unit(point2d(p))*(norm(point2d(p))-sign(R)*overlap),p.z)]
3544 ] :
3545 let(
3546 tangent = path_tangents(isect,closed=true),
3547 mesh = transpose([for(i=idx(top))
3548 assert(norm(top[i]-isect[i])>=d,str("Prism is too short for fillet to fit (",name,")"))
3549 let(
3550 dir = sign(R)*unit(cross([isect[i].x,isect[i].y,0],tangent[i])),
3551 zpart = d*dir.z,
3552 curvepart = d*norm(point2d(dir)),
3553 curveang = sign(cross(point2d(isect[i]),point2d(dir))) * curvepart * 180 / PI / abs(R),
3554 edgepoint = apply(up(zpart)*zrot(curveang), isect[i]),
3555 corner = plane_line_intersection(plane_from_normal([edgepoint.x,edgepoint.y,0], edgepoint),
3556 [isect[i],top[i]],
3557 bounded=false/*[R>0,true]*/),
3558 d_step = abs(d)*unit(top[i]-isect[i])+(uniform?isect[i]:corner)
3559 )
3560 assert(is_vector(corner,3),str("Fillet does not fit. Decrease size of fillet (",name,")."))
3561 assert(debug || R<0 || (d_step-corner)*(corner-isect[i])>=0,
3562 str("Unable to fit fillet, probably due to steep curvature of the cylinder (",name,")."))
3563 let(
3564 bez = _smooth_bez_fill([d_step,corner,edgepoint], k)
3565 )
3566 [
3567 each bezier_curve(bez, N, endpoint=true),
3568 if (overlap!=0) point3d(unit(point2d(edgepoint))*(norm(point2d(edgepoint))-sign(R)*overlap),edgepoint.z)
3569 ]
3570 ]),
3571 angle_list = _fix_angle_list([for(pt=select(mesh,-2)) atan2(pt.y,pt.x)]),
3572 z_list = [for(pt=select(mesh,-2)) pt.z],
3573 is_simple = debug || is_path_simple(hstack([angle_list,z_list]), closed=true)
3574 )
3575 assert(is_simple, str("Fillet doesn't fit: its edge is self-intersecting. Decrease size of roundover. (",name,")"))
3576 yrot(90,mesh);
3577
3578
3579
3580function _prism_fillet_sphere(name, R,bot, top, d, k, N, overlap, uniform, debug) =
3581 let(
3582 isect = [for (i=idx(top))
3583 let( isect_pt = _sphere_line_isect_best(abs(R), [top[i],bot[i]],sign(R)*(top[i]-bot[i])))
3584 assert(isect_pt, str("Prism doesn't fully intersect sphere (",name,")"))
3585 isect_pt
3586 ]
3587 )
3588 d==0 ? [isect,
3589 if (overlap!=0) [for(p=isect) p - overlap*sign(R)*unit(p)]
3590 ] :
3591 let(
3592 tangent = path_tangents(isect,closed=true),
3593 mesh = transpose([for(i=idx(top))
3594 assert(norm(top[i]-isect[i])>=d,str("Prism is too short for fillet to fit (",name,")"))
3595 let(
3596 dir = sign(R)*unit(cross(isect[i],tangent[i])),
3597 curveang = d * 180 / PI / R,
3598 edgepoint = rot(-curveang,v=tangent[i],p=isect[i]),
3599 corner = plane_line_intersection(plane_from_normal(edgepoint, edgepoint),
3600 [isect[i],top[i]],
3601 bounded=[R>0,true]),
3602 d_step = d*unit(top[i]-isect[i])+(uniform?isect[i]:corner)
3603 )
3604 assert(is_vector(corner,3),str("Fillet does not fit (",name,")"))
3605 assert(debug || R<0 || (d_step-corner)*(corner-isect[i])>0,
3606 str("Unable to fit fillet, probably due to steep curvature of the sphere (",name,")."))
3607 let(
3608 bez = _smooth_bez_fill([d_step,corner,edgepoint], k)
3609 )
3610 [
3611 each bezier_curve(bez, N, endpoint=true),
3612 if (overlap!=0) edgepoint - overlap*sign(R)*unit(edgepoint)
3613 ]
3614 ])
3615 )
3616 // this test will fail if the prism isn't "vertical". Project along prism direction?
3617 assert(debug || is_path_simple(path2d(select(mesh,-2)),closed=true),str("Fillet doesn't fit: it intersects itself (",name,")"))
3618 mesh;
3619
3620
3621
3622// Return an interpolated normal to the polygon at segment i, fraction u along the segment.
3623
3624function _getnormal(polygon,index,u,) =
3625 let(
3626 //flat=1/3,
3627 flat=1/8,
3628// flat=0,
3629 edge = (1-flat)/2,
3630 L=len(polygon),
3631 next_ind = posmod(index+1,L),
3632 prev_ind = posmod(index-1,L),
3633 this_normal = line_normal(select(polygon,index,index+1))
3634 )
3635 u > 1-edge ? lerp(this_normal,line_normal(select(polygon,index+1,index+2)), (u-edge-flat)/edge/2)
3636 : u < edge ? lerp(line_normal(select(polygon,index-1,index)),this_normal, 0.5+u/edge/2)
3637 : this_normal;
3638
3639
3640// Start at segment ind, position u on the polygon and find a point length units
3641// from that starting point. If dir<0 goes backwards through polygon segments
3642// and if dir>0 goes forwards through polygon segments.
3643// Returns [ point, ind, u] where point is the actual point desired.
3644function _polygon_step(poly, ind, u, dir, length) =
3645 let(ind = posmod(ind,len(poly)))
3646 u==0 && dir<0 ? _polygon_step(poly, ind-1, 1, dir, length)
3647 : u==1 && dir>0 ? _polygon_step(poly, ind+1, 0, dir, length)
3648 : let(
3649 seg = select(poly,ind,ind+1),
3650 seglen = norm(seg[1]-seg[0]),
3651 frac_needed = length / seglen
3652 )
3653 dir>0 ?
3654 ( (1-u) < frac_needed ? _polygon_step(poly,ind+1,0,dir,length-(1-u)*seglen)
3655 : [lerp(seg[0],seg[1],u+frac_needed),ind,u+frac_needed]
3656 )
3657 :
3658 ( u < frac_needed ? _polygon_step(poly,ind-1,1,dir,length-u*seglen)
3659 : [lerp(seg[0],seg[1],u-frac_needed),ind,u-frac_needed]
3660 );
3661
3662
3663// This function needs more error checking?
3664// Needs check for zero overlap case and zero joint case
3665function _prism_fillet_prism(name, basepoly, bot, top, d, k, N, overlap, uniform, debug)=
3666 let(
3667 top = yrot(-90,top),
3668 bot = yrot(-90,bot),
3669 basepoly = clockwise_polygon(basepoly),
3670 segpairs = pair(basepoly,wrap=true),
3671 isect_ind = [for (i=idx(top))
3672 let(isect = _prism_line_isect(segpairs, [top[i], bot[i]], top[i]))
3673 assert(isect, str("Prism doesn't fully intersect prism (",name,")"))
3674 isect
3675 ],
3676 isect=column(isect_ind,0),
3677 index = column(isect_ind,1),
3678 uval = column(isect_ind,2),
3679 tangent = path_tangents(isect,closed=true),
3680 mesh = transpose([for(i=idx(top))
3681 let(
3682 normal = point3d(_getnormal(basepoly,index[i],uval[i])),
3683 dir = unit(cross(normal,tangent[i])),
3684 zpart = d*dir.z,
3685 length_needed = d*norm(point2d(dir)),
3686 edgept2d = _polygon_step(basepoly, index[i], uval[i], sign(cross(point2d(dir),point2d(normal))), length_needed),
3687 edgepoint = point3d(edgept2d[0],isect[i].z+zpart),
3688 corner = plane_line_intersection(plane_from_normal(point3d(_getnormal(basepoly, edgept2d[1],edgept2d[2])),edgepoint),
3689 [top[i],isect[i]],
3690 bounded=false), // should be true!!! But fails to intersect if given true.
3691 d_step = abs(d)*unit(top[i]-isect[i])+(uniform?isect[i]:corner)
3692 )
3693 assert(is_vector(corner,3),str("Fillet does not fit. Decrease size of fillet (",name,")."))
3694 assert(debug || (top[i]-d_step)*(d_step-corner)>=0,
3695 str("Unable to fit fillet, probably due to steep curvature of the prism (",name,").",
3696 d_step," ",corner," ", edgepoint," ", isect[i]
3697 ))
3698 let(
3699 bez = _smooth_bez_fill([d_step,corner,edgepoint], k)
3700 )
3701 [
3702 each bezier_curve(bez, N, endpoint=true),
3703 if (overlap!=0) edgepoint-point3d(normal)*overlap
3704 ]
3705 ])
3706 )
3707 yrot(90,mesh);
3708
3709
3710// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap