forked from RonaldoCMP/BOSL2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rounding.scad
2185 lines (2098 loc) · 130 KB
/
rounding.scad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/////////////////////////////////////////////////////////////////////
// LibFile: rounding.scad
// Routines to create rounded corners, with either circular rounding,
// or continuous curvature rounding with no sudden curvature transitions.
// Includes:
// include <BOSL2/std.scad>
// include <BOSL2/rounding.scad>
//////////////////////////////////////////////////////////////////////
include <beziers.scad>
include <structs.scad>
// Section: Functions
// Function: round_corners()
//
// Usage:
// rounded_path = round_corners(path, [method], [radius=], [cut=], [joint=], [closed=], [verbose=]);
//
// Description:
// Takes a 2D or 3D path as input and rounds each corner
// by a specified amount. The rounding at each point can be different and some points can have zero
// rounding. The `round_corners()` function supports three types of corner treatment: chamfers, circular rounding,
// and continuous curvature rounding using 4th order bezier curves. Circular rounding can produce a
// tactile "bump" where the curvature changes from flat to circular.
// See https://hackernoon.com/apples-icons-have-that-shape-for-a-very-good-reason-720d4e7c8a14
// .
// You select the type of rounding using the `method` parameter, which should be `"smooth"` to
// get continuous curvature rounding, `"circle"` to get circular rounding, or `"chamfer"` to get chamfers. The default is circle
// rounding. Each method accepts multiple options to specify the amount of rounding.
// .
// The `cut` parameter specifies the distance from the unrounded corner to the rounded tip, so how
// much of the corner to "cut" off. This can be easier to understand than setting a circular radius, which can be
// unexpectedly extreme when the corner is very sharp. It also allows a systematic specification of
// corner treatments that are the same size for all three methods.
// .
// The `joint` parameter specifies the distance
// away from the corner along the path where the roundover or chamfer should start. The figure below shows
// the cut and joint distances for a given roundover. This parameter is good for ensuring that your roundover will
// fit on the polygon, since you can easily tell whether adjacent corner treatments will interfere.
// .
// For circular rounding you can also use the `radius` parameter, which sets a circular rounding
// radius.
// .
// The `"smooth"` method rounding also has a parameter that specifies how smooth the curvature match
// is. This parameter, `k`, ranges from 0 to 1, with a default of 0.5. Larger values give a more
// abrupt transition and smaller ones a more gradual transition. If you set the value much higher
// than 0.8 the curvature changes abruptly enough that though it is theoretically continuous, it may
// not be continuous in practice. If you set it very small then the transition is so gradual that
// the length of the roundover may be extremely long.
// .
// If you select curves that are too large to fit the function will fail with an error. You can set `verbose=true` to
// get a message showing a list of scale factors you can apply to your rounding parameters so that the
// roundovers will fit on the curve. If the scale factors are larger than one
// then they indicate how much you can increase the curve sizes before collisions will occur.
// .
// The parameters `radius`, `cut`, `joint` and `k` can be numbers, which round every corner using the same parameters, or you
// can specify a list to round each corner with different parameters. If the curve is not closed then the first and last points
// of the curve are not rounded. In this case you can specify a full list of points anyway, and the endpoint values are ignored,
// or you can specify a list that has length len(path)-2, omitting the two dummy values.
// .
// If your input path includes collinear points you must use a cut or radius value of zero for those "corners". You can
// choose a nonzero joint parameter when the collinear points form a 180 degree angle. This will cause extra points to be inserted.
// If the collinear points form a spike (0 degree angle) then round_corners will fail.
// .
// Examples:
// * `method="circle", radius=2`:
// Rounds every point with circular, radius 2 roundover
// * `method="smooth", cut=2`:
// Rounds every point with continuous curvature rounding with a cut of 2, and a default 0.5 smoothing parameter
// * `method="smooth", cut=2, k=0.3`:
// Rounds every point with continuous curvature rounding with a cut of 2, and a very gentle 0.3 smoothness setting
// .
// The number of segments used for roundovers is determined by `$fa`, `$fs` and `$fn` as usual for
// circular roundovers. For continuous curvature roundovers `$fs` and `$fn` are used and `$fa` is
// ignored. Note that $fn is interpreted as the number of points on the roundover curve, which is
// not equivalent to its meaning for rounding circles because roundovers are usually small fractions
// of a circular arc. As usual, $fn overrides $fs. When doing continuous curvature rounding be sure to use lots of segments or the effect
// will be hidden by the discretization. Note that if you use $fn with "smooth" then $fn points are added at each corner, even
// if the "corner" is flat, with collinear points, so this guarantees a specific output length.
//
// Figure(2D,Med):
// h = 18;
// w = 12.6;
// example = [[0,0],[w,h],[2*w,0]];
// color("red")stroke(round_corners(example, joint=18, method="smooth",closed=false),width=.1);
// stroke(example, width=.1);
// color("green")stroke([[w,h], [w,h-cos(vector_angle(example)/2) *3/8*h]], width=.1);
// ll=lerp([w,h], [0,0],18/norm([w,h]-[0,0]) );
// color("blue")stroke(_shift_segment([[w,h], ll], -.7), width=.1);
// color("green")translate([w-.3,h-4])scale(.1)rotate(90)text("cut");
// color("blue")translate([w/2-1.1,h/2+.6]) scale(.1)rotate(90-vector_angle(example)/2)text("joint");
//
// Arguments:
// path = list of 2d or 3d points defining the path to be rounded.
// 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"
// ---
// radius = rounding radius, only compatible with `method="circle"`. Can be a number or vector.
// cut = rounding cut distance, compatible with all methods. Can be a number or vector.
// joint = rounding joint distance, compatible with `method="chamfer"` and `method="smooth"`. Can be a number or vector.
// k = continuous curvature smoothness parameter for `method="smooth"`. Can be a number or vector. Default: 0.5
// closed = if true treat the path as a closed polygon, otherwise treat it as open. Default: true.
// verbose = if true display rounding scale factors that show how close roundovers are to overlapping. Default: false
//
// Example(2D,Med): Standard circular roundover with radius the same at every point. Compare results at the different corners.
// $fn=36;
// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
// polygon(round_corners(shape, radius=1));
// color("red") down(.1) polygon(shape);
// Example(2D,Med): Circular roundover using the "cut" specification, the same at every corner.
// $fn=36;
// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
// polygon(round_corners(shape, cut=1));
// color("red") down(.1) polygon(shape);
// 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.
// $fn=36;
// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
// polygon(round_corners(shape, method="smooth", cut=1, k=0.7));
// color("red") down(.1) polygon(shape);
// Example(2D,Med): Continuous curvature roundover using "joint", for the last time the same at every corner. Notice how small the roundovers are.
// $fn=36;
// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
// polygon(round_corners(shape, method="smooth", joint=1, k=0.7));
// color("red") down(.1) polygon(shape);
// Example(2D,Med): Circular rounding, different at every corner, some corners left unrounded
// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
// radii = [1.8, 0, 2, 0.3, 1.2, 0];
// polygon(round_corners(shape, radius = radii,$fn=64));
// color("red") down(.1) polygon(shape);
// 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".
// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
// cuts = [1.5,0,2,0.3, 1.2, 0];
// k = [0.6, 0.5, 0.5, 0.7, 0.3, 0.5];
// polygon(round_corners(shape, method="smooth", cut=cuts, k=k, $fs=0.1));
// color("red") down(.1) polygon(shape);
// Example(2D,Med): Chamfers
// $fn=36;
// shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
// polygon(round_corners(shape, method="chamfer", cut=1));
// color("red") down(.1) polygon(shape);
// 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.
// ten = square(50);
// cut = 5;
// linear_extrude(height=14) {
// translate([25,25,0])text("C",size=30, valign="center", halign="center");
// translate([85,25,0])text("5",size=30, valign="center", halign="center");
// translate([85,85,0])text("3",size=30, valign="center", halign="center");
// translate([25,85,0])text("7",size=30, valign="center", halign="center");
// }
// linear_extrude(height=13) {
// polygon(round_corners(ten, cut=cut, $fn=96*4));
// translate([60,0,0])polygon(round_corners(ten, method="smooth", cut=cut, $fn=96));
// translate([60,60,0])polygon(round_corners(ten, method="smooth", cut=cut, k=0.32, $fn=96));
// translate([0,60,0])polygon(round_corners(ten, method="smooth", cut=cut, k=0.7, $fn=96));
// }
// Example(2D,Med): Rounding a path that is not closed in a three different ways.
// $fs=.1;
// $fa=1;
// zigzagx = [-10, 0, 10, 20, 29, 38, 46, 52, 59, 66, 72, 78, 83, 88, 92, 96, 99, 102, 112];
// zigzagy = concat([0], flatten(repeat([-10,10],8)), [-10,0]);
// zig = hstack(zigzagx,zigzagy);
// stroke(zig,width=1); // Original shape
// fwd(20) // Smooth size corners with a cut of 4 and curvature parameter 0.6
// stroke(round_corners(zig,cut=4, k=0.6, method="smooth", closed=false),width=1);
// fwd(40) // Smooth size corners with circular arcs and a cut of 4
// stroke(round_corners(zig,cut=4,closed=false, method="circle"),width=1);
// // Smooth size corners with a circular arc and radius 1.5 (close to maximum possible)
// fwd(60) // Note how the different points are cut back by different amounts
// stroke(round_corners(zig,radius=1.5,closed=false),width=1);
// Example(FlatSpin,VPD=42,VPT=[7.75,6.69,5.22]): Rounding some random 3D paths
// $fn=36;
// list1= [
// [2.887360, 4.03497, 6.372090],
// [5.682210, 9.37103, 0.783548],
// [7.808460, 4.39414, 1.843770],
// [0.941085, 5.30548, 4.467530],
// [1.860540, 9.81574, 6.497530],
// [6.938180, 7.21163, 5.794530]
// ];
// list2= [
// [1.079070, 4.74091, 6.900390],
// [8.775850, 4.42248, 6.651850],
// [5.947140, 9.17137, 6.156420],
// [0.662660, 6.95630, 5.884230],
// [6.564540, 8.86334, 9.953110],
// [5.420150, 4.91874, 3.866960]
// ];
// path_sweep(regular_ngon(n=36,or=.1),round_corners(list1,closed=false, method="smooth", cut = 0.65));
// right(6)
// path_sweep(regular_ngon(n=36,or=.1),round_corners(list2,closed=false, method="circle", cut = 0.75));
// Example(3D,Med): Rounding a spiral with increased rounding along the length
// // Construct a square spiral path in 3D
// $fn=36;
// square = [[0,0],[1,0],[1,1],[0,1]];
// spiral = flatten(repeat(concat(square,reverse(square)),5)); // Squares repeat 10 times, forward and backward
// squareind = [for(i=[0:9]) each [i,i,i,i]]; // Index of the square for each point
// z = count(40)*.2+squareind;
// path3d = hstack(spiral,z); // 3D spiral
// rounding = squareind/20;
// // Setting k=1 means curvature won't be continuous, but curves are as round as possible
// // Try changing the value to see the effect.
// rpath = round_corners(path3d, joint=rounding, k=1, method="smooth", closed=false);
// path_sweep( regular_ngon(n=36, or=.1), rpath);
// 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]
// $fn=64;
// path = [[0, 0],[10, 0],[20, 20],[30, -10]];
// debug_polygon(path);
// //polygon(round_corners(path,cut = [1,3,1,1], method="circle"));
// 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.
// $fn=64;
// path = [[0, 0],[10, 0],[20, 20],[30, -10]];
// polygon(round_corners(path,cut = [0.85,3*0.85,1,1], method="circle", verbose=true));
// 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.
// $fn=64;
// path = [[0, 0],[10, 0],[20, 20],[30, -10]];
// polygon(round_corners(path,cut = [0.85,3*0.85,2.13, 10.15], method="circle",verbose=true));
// 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:
// $fn=64;
// path = [[0, 0],[10, 0],[20, 20],[30, -10]];
// path_len = path_segment_lengths(path,closed=true);
// halflen = [for(i=idx(path)) min(select(path_len,i-1,i))/2];
// polygon(round_corners(path,joint = halflen, method="circle",verbose=true));
module round_corners(path, method="circle", radius, cut, joint, k, closed=true, verbose=false) {no_module();}
function round_corners(path, method="circle", radius, cut, joint, k, closed=true, verbose=false) =
assert(in_list(method,["circle", "smooth", "chamfer"]), "method must be one of \"circle\", \"smooth\" or \"chamfer\"")
let(
default_k = 0.5,
size=one_defined([radius, cut, joint], "radius,cut,joint"),
path = force_path(path),
size_ok = is_num(size) || len(size)==len(path) || (!closed && len(size)==len(path)-2),
k_ok = is_undef(k) || (method=="smooth" && (is_num(k) || len(k)==len(path) || (!closed && len(k)==len(path)-2))),
measure = is_def(radius) ? "radius" :
is_def(cut) ? "cut" : "joint"
)
assert(is_path(path,[2,3]), "input path must be a 2d or 3d path")
assert(len(path)>2,str("Path has length ",len(path),". Length must be 3 or more."))
assert(size_ok,str("Input ",measure," must be a number or list with length ",len(path), closed?"":str(" or ",len(path)-2)))
assert(k_ok,method=="smooth" ? str("Input k must be a number or list with length ",len(path), closed?"":str(" or ",len(path)-2)) :
"Input k is only allowed with method=\"smooth\"")
assert(method=="circle" || measure!="radius", "radius parameter allowed only with method=\"circle\"")
let(
parm = is_num(size) ? repeat(size, len(path)) :
len(size)<len(path) ? [0, each size, 0] :
size,
k = is_undef(k) ? repeat(default_k,len(path)) :
is_num(k) ? repeat(k, len(path)) :
len(k)<len(path) ? [0, each k, 0] :
k,
badparm = [for(i=idx(parm)) if(parm[i]<0)i],
badk = [for(i=idx(k)) if(k[i]<0 || k[i]>1)i]
)
assert(is_vector(parm) && badparm==[], str(measure," must be nonnegative"))
assert(is_vector(k) && badk==[], "k parameter must be in the interval [0,1]")
let(
// dk is a list of parameters, where distance is the joint length to move away from the corner
// "smooth" method: [distance, curvature]
// "circle" method: [distance, radius]
// "chamfer" method: [distance]
dk = [
for(i=[0:1:len(path)-1])
let(
pathbit = select(path,i-1,i+1),
angle = approx(pathbit[0],pathbit[1]) || approx(pathbit[1],pathbit[2]) ? undef
: vector_angle(select(path,i-1,i+1))/2
)
(!closed && (i==0 || i==len(path)-1)) ? [0] : // Force zeros at ends for non-closed
parm[i]==0 ? [0] : // If no rounding requested then don't try to compute parameters
assert(is_def(angle), str("Repeated point in path at index ",i," with nonzero rounding"))
assert(!approx(angle,0), closed && i==0 ? "Closing the path causes it to turn back on itself at the end" :
str("Path turns back on itself at index ",i," with nonzero rounding"))
(method=="chamfer" && measure=="joint")? [parm[i]] :
(method=="chamfer" && measure=="cut") ? [parm[i]/cos(angle)] :
(method=="smooth" && measure=="joint") ? [parm[i],k[i]] :
(method=="smooth" && measure=="cut") ? [8*parm[i]/cos(angle)/(1+4*k[i]),k[i]] :
(method=="circle" && measure=="radius")? [parm[i]/tan(angle), parm[i]] :
(method=="circle" && measure=="joint") ? [parm[i], parm[i]*tan(angle)] :
/*(method=="circle" && measure=="cut")*/ approx(angle,90) ? [INF] :
let( circ_radius = parm[i] / (1/sin(angle) - 1))
[circ_radius/tan(angle), circ_radius],
],
lengths = [for(i=[0:1:len(path)]) norm(select(path,i)-select(path,i-1))],
scalefactors = [
for(i=[0:1:len(path)-1])
if (closed || (i!=0 && i!=len(path)-1))
min(
lengths[i]/(select(dk,i-1)[0]+dk[i][0]),
lengths[i+1]/(dk[i][0]+select(dk,i+1)[0])
)
],
dummy = verbose ? echo("Roundover scale factors:",scalefactors) : 0
)
assert(min(scalefactors)>=1,str("Roundovers are too big for the path. If you multitply them by this vector they should fit: ",scalefactors))
[
for(i=[0:1:len(path)-1]) each
(dk[i][0] == 0)? [path[i]] :
(method=="smooth")? _bezcorner(select(path,i-1,i+1), dk[i]) :
(method=="chamfer") ? _chamfcorner(select(path,i-1,i+1), dk[i]) :
_circlecorner(select(path,i-1,i+1), dk[i])
];
// Computes the continuous curvature control points for a corner when given as
// input three points in a list defining the corner. The points must be
// equidistant from each other to produce the continuous curvature result.
// The output control points will include the 3 input points plus two
// interpolated points.
//
// k is the curvature parameter, ranging from 0 for very slow transition
// up to 1 for a sharp transition that doesn't have continuous curvature any more
function _smooth_bez_fill(points,k) = [
points[0],
lerp(points[1],points[0],k),
points[1],
lerp(points[1],points[2],k),
points[2],
];
// Computes the points of a continuous curvature roundover given as input
// the list of 3 points defining the corner and a parameter specification
//
// If parm is a scalar then it is treated as the curvature and the control
// points are calculated using _smooth_bez_fill. Otherwise, parm is assumed
// to be a pair [d,k] where d is the length of the curve. The length is
// calculated from the input point list and the control point list will not
// necessarily include points[0] or points[2] on its output.
//
// The number of points output is $fn if it is set. Otherwise $fs is used
// to calculate the point count.
function _bezcorner(points, parm) =
let(
P = is_list(parm)?
let(
d = parm[0],
k = parm[1],
prev = unit(points[0]-points[1]),
next = unit(points[2]-points[1])
) [
points[1]+d*prev,
points[1]+k*d*prev,
points[1],
points[1]+k*d*next,
points[1]+d*next
] : _smooth_bez_fill(points,parm),
N = max(3,$fn>0 ?$fn : ceil(bezier_segment_length(P)/$fs))
)
bezier_curve(P,N+1,endpoint=true);
function _chamfcorner(points, parm) =
let(
d = parm[0],
prev = unit(points[0]-points[1]),
next = unit(points[2]-points[1])
)
[points[1]+prev*d, points[1]+next*d];
function _circlecorner(points, parm) =
let(
angle = vector_angle(points)/2,
d = parm[0],
r = parm[1],
prev = unit(points[0]-points[1]),
next = unit(points[2]-points[1])
)
approx(angle,90) ? [points[1]+prev*d, points[1]+next*d] :
let(
center = r/sin(angle) * unit(prev+next)+points[1],
start = points[1]+prev*d,
end = points[1]+next*d
) // 90-angle is half the angle of the circular arc
arc(max(3,ceil((90-angle)/180*segs(r))), cp=center, points=[start,end]);
// Used by offset_sweep and convex_offset_extrude.
// Produce edge profile curve from the edge specification
// z_dir is the direction multiplier (1 to build up, -1 to build down)
function _rounding_offsets(edgespec,z_dir=1) =
let(
edgetype = struct_val(edgespec, "type"),
extra = struct_val(edgespec,"extra"),
N = struct_val(edgespec, "steps"),
r = struct_val(edgespec,"r"),
cut = struct_val(edgespec,"cut"),
k = struct_val(edgespec,"k"),
radius = in_list(edgetype,["circle","teardrop"])
? (is_def(cut) ? cut/(sqrt(2)-1) : r)
:edgetype=="chamfer"
? (is_def(cut) ? sqrt(2)*cut : r)
: undef,
chamf_angle = struct_val(edgespec, "angle"),
cheight = struct_val(edgespec, "chamfer_height"),
cwidth = struct_val(edgespec, "chamfer_width"),
chamf_width = first_defined([!all_defined([cut,chamf_angle]) ? undef : cut/cos(chamf_angle),
cwidth,
!all_defined([cheight,chamf_angle]) ? undef : cheight*tan(chamf_angle)]),
chamf_height = first_defined([
!all_defined([cut,chamf_angle]) ? undef : cut/sin(chamf_angle),
cheight,
!all_defined([cwidth, chamf_angle]) ? undef : cwidth/tan(chamf_angle)]),
joint = first_defined([
struct_val(edgespec,"joint"),
all_defined([cut,k]) ? 16*cut/sqrt(2)/(1+4*k) : undef
]),
points = struct_val(edgespec, "points"),
argsOK = in_list(edgetype,["circle","teardrop"])? is_def(radius) :
edgetype == "chamfer"? chamf_angle>0 && chamf_angle<90 && num_defined([chamf_height,chamf_width])==2 :
edgetype == "smooth"? num_defined([k,joint])==2 :
edgetype == "profile"? points[0]==[0,0] :
false
)
assert(argsOK,str("Invalid specification with type ",edgetype))
let(
offsets =
edgetype == "profile"? scale([-1,z_dir], p=list_tail(points)) :
edgetype == "chamfer"? chamf_width==0 && chamf_height==0? [] : [[-chamf_width,z_dir*abs(chamf_height)]] :
edgetype == "teardrop"? (
radius==0? [] : concat(
[for(i=[1:N]) [radius*(cos(i*45/N)-1),z_dir*abs(radius)* sin(i*45/N)]],
[[-2*radius*(1-sqrt(2)/2), z_dir*abs(radius)]]
)
) :
edgetype == "circle"? radius==0? [] : [for(i=[1:N]) [radius*(cos(i*90/N)-1), z_dir*abs(radius)*sin(i*90/N)]] :
/* smooth */ joint==0 ? [] :
list_tail(
_bezcorner([[0,0],[0,z_dir*abs(joint)],[-joint,z_dir*abs(joint)]], k, $fn=N+2)
)
)
quant(extra > 0? concat(offsets, [last(offsets)+[0,z_dir*extra]]) : offsets, 1/1024);
// Function: smooth_path()
// Usage:
// smoothed = smooth_path(path, [tangents], <size=|relsize=>, [splinesteps=], [closed=], [uniform=]);
// Description:
// Smooths the input path using a cubic spline. Every segment of the path will be replaced by a cubic curve
// with `splinesteps` points. The cubic interpolation will pass through every input point on the path
// and will match the tangents at every point. If you do not specify tangents they will be computed using
// path_tangents with uniform=false by default. Note that setting uniform to true with non-uniform
// sampling may be desirable in some cases but tends to produces curves that overshoot the point on the path.
// .
// The size or relsize parameter determines how far the curve can bend away from
// the input path. In the case where the curve has a single hump, the size specifies the exact distance
// between the specified path and the curve. If you give relsize then it is relative to the segment
// length (e.g. 0.05 means 5% of the segment length). In 2d when the spline may make an S-curve,
// in which case the size parameter specifies the sum of the deviations of the two peaks of the curve. In 3-space
// the bezier curve may have three extrema: two maxima and one minimum. In this case the size specifies
// the sum of the maxima minus the minimum. At a given segment there is a maximum size: if your size
// value is too large it will be rounded down. See also path_to_bezier().
// Arguments:
// path = path to smooth
// tangents = tangents constraining curve direction at each point. Default: computed automatically
// ---
// relsize = relative size specification for the curve, a number or vector. Default: 0.1
// size = absolute size specification for the curve, a number or vector
// uniform = set to true to compute tangents with uniform=true. Default: false
// closed = true if the curve is closed. Default: false.
// Example(2D): Original path in green, smoothed path in yellow:
// color("green")stroke(square(4), width=0.1);
// stroke(smooth_path(square(4),size=0.4), width=0.1);
// Example(2D): Closing the path changes the end tangents
// polygon(smooth_path(square(4),size=0.4,closed=true));
// Example(2D): Turning on uniform tangent calculation also changes the end derivatives:
// color("green")stroke(square(4), width=0.1);
// stroke(smooth_path(square(4),size=0.4,uniform=true), width=0.1);
// Example(2D): Here's a wide rectangle. Using size means all edges bulge the same amount, regardless of their length.
// color("green")stroke(square([10,4]), closed=true, width=0.1);
// stroke(smooth_path(square([10,4]),size=1,closed=true),width=0.1);
// Example(2D): Here's a wide rectangle. With relsize the bulge is proportional to the side length.
// color("green")stroke(square([10,4]), closed=true, width=0.1);
// stroke(smooth_path(square([10,4]),relsize=0.1,closed=true),width=0.1);
// Example(2D): Here's a wide rectangle. Settting uniform to true biases the tangents to aline more with the line sides
// color("green")stroke(square([10,4]), closed=true, width=0.1);
// stroke(smooth_path(square([10,4]),uniform=true,relsize=0.1,closed=true),width=0.1);
// Example(2D): A more interesting shape:
// path = [[0,0], [4,0], [7,14], [-3,12]];
// polygon(smooth_path(path,size=1,closed=true));
// Example(2D): Here's the square again with less smoothing.
// polygon(smooth_path(square(4), size=.25,closed=true));
// Example(2D): Here's the square with a size that's too big to achieve, so you get the maximum possible curve:
// color("green")stroke(square(4), width=0.1,closed=true);
// stroke(smooth_path(square(4), size=4, closed=true),closed=true,width=.1);
// Example(2D): You can alter the shape of the curve by specifying your own arbitrary tangent values
// polygon(smooth_path(square(4),tangents=1.25*[[-2,-1], [-4,1], [1,2], [6,-1]],size=0.4,closed=true));
// Example(2D): Or you can give a different size for each segment
// polygon(smooth_path(square(4),size = [.4, .05, 1, .3],closed=true));
// Example(FlatSpin,VPD=35,VPT=[4.5,4.5,1]): Works on 3d paths as well
// path = [[0,0,0],[3,3,2],[6,0,1],[9,9,0]];
// stroke(smooth_path(path,relsize=.1),width=.3);
// 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
// pts = [[-3.3, 1.7], [-3.7, -2.2], [3.8, -4.8], [-0.9, -2.4]];
// stroke(smooth_path(pts, uniform=true, relsize=0.1),width=.1);
// color("red")move_copies(pts)circle(r=.15,$fn=12);
// Example(2D): With the default of uniform false no overshoot occurs. Note that the shape of the curve is quite different.
// pts = [[-3.3, 1.7], [-3.7, -2.2], [3.8, -4.8], [-0.9, -2.4]];
// stroke(smooth_path(pts, uniform=false, relsize=0.1),width=.1);
// color("red")move_copies(pts)circle(r=.15,$fn=12);
module smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed=false) {no_module();}
function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed=false) =
let (
bez = path_to_bezier(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=closed)
)
bezier_path(bez,splinesteps=splinesteps);
function _scalar_to_vector(value,length,varname) =
is_vector(value)
? assert(len(value)==length, str(varname," must be length ",length))
value
: assert(is_num(value), str(varname, " must be a numerical value"))
repeat(value, length);
// Function: path_join()
// Usage:
// joined_path = path_join(paths, [joint], [k=], [relocate=], [closed=]);
// Description:
// Connect a sequence of paths together into a single path with optional rounding
// applied at the joints. By default the first path is taken as specified and subsequent paths are
// translated into position so that each path starts where the previous path ended.
// If you set relocate to false then this relocation is skipped.
// You specify rounding using the `joint` parameter, which specifies the distance away from the corner
// where the roundover should start. The path_join function may remove many path points to cut the path
// back by the joint length. Rounding is using continous curvature 4th order bezier splines and
// the parameter `k` specifies how smooth the curvature match is. This parameter ranges from 0 to 1 with
// a default of 0.5. Use a larger k value to get a curve that is bigger for the same joint value. When
// k=1 the curve may be similar to a circle if your curves are symmetric. As the path is built up, the joint
// parameter applies to the growing path, so if you pick a large joint parameter it may interact with the
// previous path sections.
// .
// The rounding is created by extending the two clipped paths to define a corner point. If the extensions of
// the paths do not intersect, the function issues an error. When closed=true the final path should actually close
// the shape, repeating the starting point of the shape. If it does not, then the rounding will fill the gap.
// .
// The number of segments in the roundovers is set based on $fn and $fs. If you use $fn it specifies the number of
// segments in the roundover, regardless of its angular extent.
// Arguments:
// paths = list of paths to join
// joint = joint distance, either a number, a pair (giving the previous and next joint distance) or a list of numbers and pairs. Default: 0
// ---
// k = curvature parameter, either a number or vector. Default: 0.5
// relocate = set to false to prevent paths from being arranged tail to head. Default: true
// closed = set to true to round the junction between the last and first paths. Default: false
// Example(2D): Connection of 3 simple paths.
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// stroke(path_join([horiz, vert, -horiz]));
// Example(2D): Adding curvature with joint of 3
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// stroke(path_join([horiz, vert, -horiz],joint=3,$fn=16));
// Example(2D): Setting k=1 increases the amount of curvature
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// stroke(path_join([horiz, vert, -horiz],joint=3,k=1,$fn=16));
// Example(2D): Specifying pairs of joint values at a path joint creates an asymmetric curve
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// stroke(path_join([horiz, vert, -horiz],joint=[[4,1],[1,4]],$fn=16),width=.3);
// Example(2D): A closed square
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// stroke(path_join([horiz, vert, -horiz, -vert],joint=3,k=1,closed=true,$fn=16),closed=true);
// Example(2D): Different curve at each corner by changing the joint size
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// stroke(path_join([horiz, vert, -horiz, -vert],joint=[3,0,1,2],k=1,closed=true,$fn=16),closed=true,width=0.4);
// 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.
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// stroke(path_join([horiz, vert, -horiz, -vert],joint=3,k=[1,.5,0,.7],closed=true,$fn=16),closed=true,width=0.4);
// 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
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// stroke(path_join([horiz, vert, -horiz, -vert],joint=7,k=.4,closed=true,$fn=16),closed=true);
// Example(2D): Unlike round_corners, we can add curves onto curves.
// $fn=64;
// myarc = arc(width=20, thickness=5 );
// stroke(path_join(repeat(myarc,3), joint=4));
// Example(2D): Here we make a closed shape from two arcs and round the sharp tips
// arc1 = arc(width=20, thickness=4,$fn=75);
// arc2 = reverse(arc(width=20, thickness=2,$fn=75));
// stroke(path_join([arc1,arc2]),width=.3); // Join without rounding
// color("red")stroke(path_join([arc1,arc2], 3,k=1,closed=true), width=.3,closed=true,$fn=12); // Join with rounding
// Example(2D): Combining arcs with segments
// arc1 = arc(width=20, thickness=4,$fn=75);
// arc2 = reverse(arc(width=20, thickness=2,$fn=75));
// vpath = [[0,0],[0,-5]];
// stroke(path_join([arc1,vpath,arc2,reverse(vpath)]),width=.2);
// color("red")stroke(path_join([arc1,vpath,arc2,reverse(vpath)], [1,2,2,1],k=1,closed=true), width=.2,closed=true,$fn=12);
// 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.
// p1 = [[0,0],[2,0]];
// p2 = [[3,1],[1,3]];
// p3 = [[0,3],[-1,1]];
// color("red")stroke(path_join([p1,p2,p3], joint=0, relocate=false,closed=true),width=.3,$fn=12);
// for(x=[p1,p2,p3]) stroke(x,width=.3);
// 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.
// horiz = [[0,0],[10,0]];
// vert = [[0,0],[0,10]];
// h2 = [[0,-3],[10,0]];
// color("red")stroke(path_join([horiz, vert, -h2],closed=true,joint=3,$fn=25),closed=true,width=.5);
// stroke(path_join([horiz, vert, -h2]),width=.3);
// Example(2D): With a single path with closed=true the start and end junction is rounded.
// tri = regular_ngon(n=3, r=7);
// stroke(path_join([tri], joint=3,closed=true,$fn=12),closed=true,width=.5);
module path_join(paths,joint=0,k=0.5,relocate=true,closed=false) { no_module();}
function path_join(paths,joint=0,k=0.5,relocate=true,closed=false)=
assert(is_list(paths),"Input paths must be a list of paths")
let(
paths = [for(i=idx(paths)) force_path(paths[i],str("paths[",i,"]"))],
badpath = [for(j=idx(paths)) if (!is_path(paths[j])) j]
)
assert(badpath==[], str("Entries in paths are not valid paths: ",badpath))
len(paths)==0 ? [] :
len(paths)==1 && !closed ? paths[0] :
let(
paths = !closed || len(paths)>1
? paths
: [close_path(paths[0])],
N = len(paths) + (closed?0:-1),
k = _scalar_to_vector(k,N),
repjoint = is_num(joint) || (is_vector(joint,2) && len(paths)!=3),
joint = repjoint ? repeat(joint,N) : joint
)
assert(all_nonnegative(k), "k must be nonnegative")
assert(len(joint)==N,str("Input joint must be scalar or length ",N))
let(
bad_j = [for(j=idx(joint)) if (!is_num(joint[j]) && !is_vector(joint[j],2)) j]
)
assert(bad_j==[], str("Invalid joint values at indices ",bad_j))
let(result=_path_join(paths,joint,k, relocate=relocate, closed=closed))
closed ? cleanup_path(result) : result;
function _path_join(paths,joint,k=0.5,i=0,result=[],relocate=true,closed=false) =
let(
result = result==[] ? paths[0] : result,
loop = i==len(paths)-1,
revresult = reverse(result),
nextpath = loop ? result
: relocate ? move(revresult[0]-paths[i+1][0], p=paths[i+1])
: paths[i+1],
d_first = is_vector(joint[i]) ? joint[i][0] : joint[i],
d_next = is_vector(joint[i]) ? joint[i][1] : joint[i]
)
assert(d_first>=0 && d_next>=0, str("Joint value negative when adding path ",i+1))
assert(d_first<path_length(revresult),str("Path ",i," is too short for specified cut distance ",d_first))
assert(d_next<path_length(nextpath), str("Path ",i+1," is too short for specified cut distance ",d_next))
let(
firstcut = _path_cut_points(revresult, d_first, direction=true),
nextcut = _path_cut_points(nextpath, d_next, direction=true)
)
assert(!loop || nextcut[1] < len(revresult)-1-firstcut[1], "Path is too short to close the loop")
let(
first_dir=firstcut[2],
next_dir=nextcut[2],
corner = line_intersection([firstcut[0], firstcut[0]-first_dir], [nextcut[0], nextcut[0]-next_dir],RAY,RAY)
)
assert(is_def(corner), str("Curve directions at cut points don't intersect in a corner when ",
loop?"closing the path":str("adding path ",i+1)))
let(
bezpts = _smooth_bez_fill([firstcut[0], corner, nextcut[0]],k[i]),
N = max(3,$fn>0 ?$fn : ceil(bezier_segment_length(bezpts)/$fs)),
bezpath = approx(firstcut[0],corner) && approx(corner,nextcut[0])
? []
: bezier_curve(bezpts,N),
new_result = [each select(result,loop?nextcut[1]:0,len(revresult)-1-firstcut[1]),
each bezpath,
nextcut[0],
if (!loop) each list_tail(nextpath,nextcut[1])
]
)
i==len(paths)-(closed?1:2)
? new_result
: _path_join(paths,joint,k,i+1,new_result, relocate,closed);
// Function&Module: offset_sweep()
// Usage: most common module arguments. See Arguments list below for more.
// offset_sweep(path, <height|h|l>, [bottom], [top], [offset=], [convexity=],...) [attachments]
// Usage: most common function arguments. See Arguments list below for more.
// vnf = offset_sweep(path, <height|h|l>, [bottom], [top], [offset=], ...)
// Description:
// 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.
// 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
// provide end treatments such as roundovers.
// The height of the resulting object can be specified using the `height` argument, in which case `height` must be larger than the combined height
// of the end treatments. If you omit `height` then the object height will be the height of just the top and bottom end treatments.
// .
// The path is shifted by `offset()` multiple times in sequence
// to produce the final shape (not multiple shifts from one parent), so coarse definition of the input path will degrade
// from the successive shifts. If the result seems rough or strange try increasing the number of points you use for
// your input. If you get unexpected corners in your result you may have forgotten to set `$fn` or `$fa` and `$fs`.
// Be aware that large numbers of points (especially when check_valid is true) can lead to lengthy run times. If your
// 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
// disabling the validity check when it is needed can generate invalid polyhedra that will produce CGAL errors upon
// rendering. Such validity errors will also occur if you specify a self-intersecting shape.
// The offset profile is quantized to 1/1024 steps to avoid failures in offset() that can occur with very tiny offsets.
// .
// The build-in profiles are: circular rounding, teardrop rounding, continuous curvature rounding, and chamfer.
// Also note that when a rounding radius is negative the rounding will flare outwards. The easiest way to specify
// the profile is by using the profile helper functions. These functions take profile parameters, as well as some
// general settings and translate them into a profile specification, with error checking on your input. The description below
// describes the helper functions and the parameters specific to each function. Below that is a description of the generic
// settings that you can optionally use with all of the helper functions.
// .
// - profile: os_profile(points)
// 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.
// - circle: os_circle(r|cut). Define circular rounding either by specifying the radius or cut distance.
// - 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.
// - 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.
// - 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.
// - 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.
// .
// 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.
// - 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
// - check_valid: passed to offset(). Default: true
// - quality: passed to offset(). Default: 1
// - steps: Number of vertical steps to use for the profile. (Not used by os_profile). Default: 16
// - 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"
// .
// Many of the arguments are described as setting "default" values because they establish settings which may be overridden by
// the top and bottom profile specifications.
// .
// You will generally want to use the above helper functions to generate the profiles.
// The profile specification is a list of pairs of keywords and values, e.g. ["for","offset_sweep","r",12, type, "circle"]. The keywords are
// - "for" - must appear first in the list and have the value "offset_sweep"
// - "type" - type of rounding to apply, one of "circle", "teardrop", "chamfer", "smooth", or "profile" (Default: "circle")
// - "r" - the radius of the roundover, which may be zero for no roundover, or negative to round or flare outward. Default: 0
// - "cut" - the cut distance for the roundover or chamfer, which may be negative for flares
// - "chamfer_width" - the width of a chamfer
// - "chamfer_height" - the height of a chamfer
// - "angle" - the chamfer angle, measured from the vertical (so zero is vertical, 90 is horizontal). Default: 45
// - "joint" - the joint distance for a "smooth" roundover
// - "k" - the curvature smoothness parameter for "smooth" roundovers, a value in [0,1]. Default: 0.75
// - "points" - point list for use with the "profile" type
// - "extra" - extra height added for unions/differences. This makes the shape taller than the requested height. (Default: 0)
// - "check_valid" - passed to offset. Default: true.
// - "quality" - passed to offset. Default: 1.
// - "steps" - number of vertical steps to use for the roundover. Default: 16.
// - "offset" - select "round" (r=), "delta" (delta=), or "chamfer" offset type for offset. Default: "round"
// .
// Note that if you set the "offset" parameter to "chamfer" then every exterior corner turns from one vertex into two vertices with
// each offset operation. Since the offsets are done one after another, each on the output of the previous one, this leads to
// exponential growth in the number of vertices. This can lead to long run times or yield models that
// run out of recursion depth and give a cryptic error. Furthermore, the generated vertices are distributed non-uniformly. Generally you
// will get a similar or better looking model with fewer vertices using "round" instead of
// "chamfer". Use the "chamfer" style offset only in cases where the number of steps is very small or just one (such as when using
// the `os_chamfer` profile type).
//
// Arguments:
// path = 2d path (list of points) to extrude
// height / l / h = total height (including rounded portions, but not extra sections) of the output. Default: combined height of top and bottom end treatments.
// bottom = rounding spec for the bottom end
// top = rounding spec for the top end.
// ---
// offset = default offset, `"round"` or `"delta"`. Default: `"round"`
// steps = default step count. Default: 16
// quality = default quality. Default: 1
// check_valid = default check_valid. Default: true.
// extra = default extra height. Default: 0
// cut = default cut value.
// chamfer_width = default width value for chamfers.
// chamfer_height = default height value for chamfers.
// angle = default angle for chamfers. Default: 45
// joint = default joint value for smooth roundover.
// k = default curvature parameter value for "smooth" roundover
// convexity = convexity setting for use with polyhedron. (module only) Default: 10
// anchor = Translate so anchor point is at the origin. (module only) Default: "origin"
// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0
// orient = Vector to rotate top towards after spin (module only)
// extent = use extent method for computing anchors. (module only) Default: false
// cp = set centerpoint for anchor computation. (module only) Default: object centroid
// Example: Rounding a star shaped prism with postive radius values
// star = star(5, r=22, ir=13);
// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
// offset_sweep(rounded_star, height=20, bottom=os_circle(r=4), top=os_circle(r=1), steps=15);
// Example: Rounding a star shaped prism with negative radius values. The starting shape has no corners, so the value of `$fn` does not matter.
// star = star(5, r=22, ir=13);
// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=36);
// offset_sweep(rounded_star, height=20, bottom=os_circle(r=-4), top=os_circle(r=-1), steps=15);
// 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.
// triangle = [[0,0],[10,0],[5,10]];
// offset_sweep(triangle, height=6, bottom = os_circle(r=-2),steps=4);
// Example: Can improve the result by increasing $fn
// $fn=12;
// triangle = [[0,0],[10,0],[5,10]];
// offset_sweep(triangle, height=6, bottom = os_circle(r=-2),steps=4);
// Example: Using $fa and $fs works too; it produces a different looking triangulation of the rounded corner
// $fa=1;$fs=0.3;
// triangle = [[0,0],[10,0],[5,10]];
// offset_sweep(triangle, height=6, bottom = os_circle(r=-2),steps=4);
// 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.
// star = star(5, r=22, ir=13);
// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
// offset_sweep(rounded_star, height=20, bottom=os_teardrop(r=4), top=os_chamfer(width=4),$fn=64);
// 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.
// square = square(1);
// rsquare = round_corners(square, method="smooth", cut=0.1, k=0.7, $fn=36);
// end_spec = os_smooth(cut=0.1, k=0.7, steps=22);
// offset_sweep(rsquare, height=1, bottom=end_spec, top=end_spec);
// Example: A nice rounded box, with a teardrop base and circular rounded interior and top
// box = square([255,50]);
// rbox = round_corners(box, method="smooth", cut=4, $fn=12);
// thickness = 2;
// difference(){
// offset_sweep(rbox, height=50, check_valid=false, steps=22, bottom=os_teardrop(r=2), top=os_circle(r=1));
// up(thickness)
// offset_sweep(offset(rbox, r=-thickness, closed=true,check_valid=false),
// height=48, steps=22, check_valid=false, bottom=os_circle(r=4), top=os_circle(r=-1,extra=1));
// }
// 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.
// smallbox = square([75,50]);
// roundbox = round_corners(smallbox, method="smooth", cut=4, $fn=12);
// thickness=4;
// height=50;
// back_half(y=25, s=200)
// difference(){
// offset_sweep(roundbox, height=height, bottom=["for","offset_sweep","r",10,"type","teardrop"],
// top=["for","offset_sweep","r",2], steps = 22, check_valid=false);
// up(thickness)
// offset_sweep(offset(roundbox, r=-thickness, closed=true),
// height=height-thickness, steps=22,
// bottom=["for","offset_sweep","r",6],
// top=["for","offset_sweep","type","chamfer","angle",30,"chamfer_height",-3,"extra",1,"check_valid",false]);
// }
// Example: A box with multiple sections and rounded dividers
// thickness = 2;
// box = square([255,50]);
// cutpoints = [0, 125, 190, 255];
// rbox = round_corners(box, method="smooth", cut=4, $fn=12);
// back_half(y=25, s=700)
// difference(){
// offset_sweep(rbox, height=50, check_valid=false, steps=22, bottom=os_teardrop(r=2), top=os_circle(r=1));
// up(thickness)
// for(i=[0:2]){
// ofs = i==1 ? 2 : 0;
// hole = round_corners([[cutpoints[i]-ofs,0], [cutpoints[i]-ofs,50], [cutpoints[i+1]+ofs, 50], [cutpoints[i+1]+ofs,0]],
// method="smooth", cut=4, $fn=36);
// offset_sweep(offset(hole, r=-thickness, closed=true,check_valid=false),
// height=48, steps=22, check_valid=false, bottom=os_circle(r=4), top=os_circle(r=-1,extra=1));
// }
// }
// Example: Star shaped box
// star = star(5, r=22, ir=13);
// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
// thickness = 2;
// ht=20;
// difference(){
// offset_sweep(rounded_star, height=ht, bottom=["for","offset_sweep","r",4], top=["for","offset_sweep","r",1], steps=15);
// up(thickness)
// offset_sweep(offset(rounded_star,r=-thickness,closed=true),
// height=ht-thickness, check_valid=false,
// bottom=os_circle(r=7), top=os_circle(r=-1, extra=1),$fn=40);
// }
// Example: A profile defined by an arbitrary sequence of points.
// star = star(5, r=22, ir=13);
// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
// profile = os_profile(points=[[0,0],[.3,.1],[.6,.3],[.9,.9], [1.2, 2.7],[.8,2.7],[.8,3]]);
// offset_sweep(reverse(rounded_star), height=20, top=profile, bottom=profile, $fn=32);
// Example: Parabolic rounding
// star = star(5, r=22, ir=13);
// rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24);
// offset_sweep(rounded_star, height=20, top=os_profile(points=[for(r=[0:.1:2])[sqr(r),r]]),
// bottom=os_profile(points=[for(r=[0:.2:5])[-sqrt(r),r]]),$fn=32);
// Example: This example uses a sine wave offset profile. Note that we give no specification for the bottom, so it is straight.
// sq = [[0,0],[20,0],[20,20],[0,20]];
// sinwave = os_profile(points=[for(theta=[0:5:720]) [4*sin(theta), theta/700*15]]);
// offset_sweep(sq, height=20, top=sinwave, $fn=32);
// Example: The same as the previous example but `offset="delta"`
// sq = [[0,0],[20,0],[20,20],[0,20]];
// sinwave = os_profile(points=[for(theta=[0:5:720]) [4*sin(theta), theta/700*15]]);
// offset_sweep(sq, height=20, top=sinwave, offset="delta");
// 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.
// rhex = round_corners(hexagon(side=10), method="smooth", joint=2, $fs=0.2);
// back_half()
// difference(){
// offset_sweep(rhex, height=10, bottom=os_teardrop(r=2), top=os_teardrop(r=-4, extra=0.2));
// up(1)
// offset_sweep(offset(rhex,r=-1), height=9.5, bottom=os_circle(r=2), top=os_teardrop(r=-4));
// }
// Example: Using os_mask to create ogee profiles:
// ogee = mask2d_ogee([
// "xstep",1, "ystep",1, // Starting shoulder.
// "fillet",5, "round",5, // S-curve.
// "ystep",1, // Ending shoulder.
// ]);
// star = star(5, r=220, ir=130);
// rounded_star = round_corners(star, cut=flatten(repeat([5,0],5)), $fn=24);
// offset_sweep(rounded_star, height=100, top=os_mask(ogee), bottom=os_mask(ogee,out=true));
// This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce
// the inputs for the polyhedron module.
function _make_offset_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, offsetind=0,
vertexcount=0, vertices=[], faces=[] )=
offsetind==len(offsets)? (
let(
bottom = count(len(path),vertexcount),
oriented_bottom = !flip_faces? bottom : reverse(bottom)
) [vertices, concat(faces,[oriented_bottom])]
) : (
let(
this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0],
delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef,
r = offset_type=="round"? this_offset : undef,
do_chamfer = offset_type == "chamfer"
)
let(
vertices_faces = offset(
path, r=r, delta=delta, chamfer = do_chamfer, closed=true,
check_valid=check_valid, quality=quality,
return_faces=true,
firstface_index=vertexcount,
flip_faces=flip_faces
)
)
_make_offset_polyhedron(
vertices_faces[0], offsets, offset_type,
flip_faces, quality, check_valid,
offsetind+1, vertexcount+len(path),
vertices=concat(
vertices,
path3d(vertices_faces[0],offsets[offsetind][1])
),
faces=concat(faces, vertices_faces[1])
)
);
function _struct_valid(spec, func, name) =
spec==[] ? true :
assert(is_list(spec) && len(spec)>=2 && spec[0]=="for",str("Specification for \"", name, "\" is an invalid structure"))
assert(spec[1]==func, str("Specification for \"",name,"\" is for a different function (",func,")"));
function offset_sweep(
path, height,
bottom=[], top=[],
h, l,
offset="round", r=0, steps=16,
quality=1, check_valid=true,
extra=0,
cut=undef, chamfer_width=undef, chamfer_height=undef,
joint=undef, k=0.75, angle=45
) =
let(
argspec = [
["for",""],
["r",r],
["extra",extra],
["type","circle"],
["check_valid",check_valid],
["quality",quality],
["steps",steps],
["offset",offset],
["chamfer_width",chamfer_width],
["chamfer_height",chamfer_height],
["angle",angle],
["cut",cut],
["joint",joint],
["k", k],
["points", []],
],
path = force_path(path)
)
assert(is_path(path,2), "Input path must be a 2D path")
let(
clockwise = is_polygon_clockwise(path),
dummy1 = _struct_valid(top,"offset_sweep","top"),
dummy2 = _struct_valid(bottom,"offset_sweep","bottom"),
top = struct_set(argspec, top, grow=false),
bottom = struct_set(argspec, bottom, grow=false),
// This code does not work. It hits the error in _make_offset_polyhedron from offset being wrong
// before this code executes. Had to move the test into _make_offset_polyhedron, which is ugly since it's in the loop
offsetsok = in_list(struct_val(top, "offset"),["round","delta","chamfer"])
&& in_list(struct_val(bottom, "offset"),["round","delta","chamfer"])
)
assert(offsetsok,"Offsets must be one of \"round\", \"delta\", or \"chamfer\"")
let(
offsets_bot = _rounding_offsets(bottom, -1),
offsets_top = _rounding_offsets(top, 1),
dummy = (struct_val(top,"offset")=="chamfer" && len(offsets_top)>5)
|| (struct_val(bottom,"offset")=="chamfer" && len(offsets_bot)>5)
? 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.")
: 0,
// "Extra" height enlarges the result beyond the requested height, so subtract it
bottom_height = len(offsets_bot)==0 ? 0 : abs(last(offsets_bot)[1]) - struct_val(bottom,"extra"),
top_height = len(offsets_top)==0 ? 0 : abs(last(offsets_top)[1]) - struct_val(top,"extra"),
height = one_defined([l,h,height], "l,h,height", dflt=u_add(bottom_height,top_height)),
middle = height-bottom_height-top_height
)
assert(height>0, "Height must be positive")
assert(middle>=0, str("Specified end treatments (bottom height = ",bottom_height,
" top_height = ",top_height,") are too large for extrusion height (",height,")"
)
)