-
Notifications
You must be signed in to change notification settings - Fork 115
/
skin.scad
4528 lines (4396 loc) · 267 KB
/
skin.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: skin.scad
// This file provides functions and modules that construct shapes from a list of cross sections.
// In the case of skin() you specify each cross sectional shape yourself, and the number of
// points can vary. The various forms of sweep use a fixed shape, which may follow a path, or
// be transformed in other ways to produce the list of cross sections. In all cases it is the
// user's responsibility to avoid creating a self-intersecting shape, which will produce
// cryptic CGAL errors. This file was inspired by list-comprehension-demos skin():
// - https://github.com/openscad/list-comprehension-demos/blob/master/skin.scad
// Includes:
// include <BOSL2/std.scad>
// FileGroup: Advanced Modeling
// FileSummary: Construct 3D shapes from 2D cross sections of the desired shape.
// FileFootnotes: STD=Included in std.scad
//////////////////////////////////////////////////////////////////////
// Section: Skin and sweep
// Function&Module: skin()
// Synopsis: Connect a sequence of arbitrary polygons into a 3D object.
// SynTags: VNF, Geom
// Topics: Extrusion, Skin
// See Also: vnf_vertex_array(), sweep(), linear_sweep(), rotate_sweep(), spiral_sweep(), path_sweep(), offset_sweep()
// Usage: As module:
// skin(profiles, slices, [z=], [refine=], [method=], [sampling=], [caps=], [closed=], [style=], [convexity=], [anchor=],[cp=],[spin=],[orient=],[atype=]) [ATTACHMENTS];
// Usage: As function:
// vnf = skin(profiles, slices, [z=], [refine=], [method=], [sampling=], [caps=], [closed=], [style=], [anchor=],[cp=],[spin=],[orient=],[atype=]);
// Description:
// Given a list of two or more path `profiles` in 3d space, produces faces to skin a surface between
// the profiles. Optionally the first and last profiles can have endcaps, or the first and last profiles
// can be connected together. Each profile should be roughly planar, but some variation is allowed.
// Each profile must rotate in the same clockwise direction. If called as a function, returns a
// [VNF structure](vnf.scad) `[VERTICES, FACES]`. If called as a module, creates a polyhedron
// of the skinned profiles.
// .
// The profiles can be specified either as a list of 3d curves or they can be specified as
// 2d curves with heights given in the `z` parameter. It is your responsibility to ensure
// that the resulting polyhedron is free from self-intersections, which would make it invalid
// and can result in cryptic CGAL errors upon rendering with a second object present, even though the polyhedron appears
// OK during preview or when rendered by itself. The order of points in your profiles must be
// consistent from slice to slice so that points match up without creating twists. You can specify
// profiles in any consistent order: if necessary, skin() will reverse the faces to ensure that the final
// result has clockwise faces as required by CGAL. Note that the face reversal test may give random results
// if you use skin to construct self-intersecting (invalid) polyhedra.
// .
// For this operation to be well-defined, the profiles must all have the same vertex count and
// we must assume that profiles are aligned so that vertex `i` links to vertex `i` on all polygons.
// Many interesting cases do not comply with this restriction. Two basic methods can handle
// these cases: either subdivide edges (insert additional points along edges)
// or duplicate vertcies (insert edges of length 0) so that both polygons have
// the same number of points.
// Duplicating vertices allows two distinct points in one polygon to connect to a single point
// in the other one, creating
// triangular faces. You can adjust non-matching polygons yourself
// either by resampling them using {{subdivide_path()}} or by duplicating vertices using
// `repeat_entries`. It is OK to pass a polygon that has the same vertex repeated, such as
// a square with 5 points (two of which are identical), so that it can match up to a pentagon.
// Such a combination would create a triangular face at the location of the duplicated vertex.
// Alternatively, `skin` provides methods (described below) for inserting additional vertices
// automatically to make incompatible paths match.
// .
// In order for skinned surfaces to look good it is usually necessary to use a fine sampling of
// points on all of the profiles, and a large number of extra interpolated slices between the
// profiles that you specify. It is generally best if the triangles forming your polyhedron
// are approximately equilateral. The `slices` parameter specifies the number of slices to insert
// between each pair of profiles, either a scalar to insert the same number everywhere, or a vector
// to insert a different number between each pair.
// .
// Resampling may occur, depending on the `method` parameter, to make profiles compatible.
// To force (possibly additional) resampling of the profiles to increase the point density you can set `refine=N`, which
// will multiply the number of points on your profile by `N`. You can choose between two resampling
// schemes using the `sampling` option, which you can set to `"length"` or `"segment"`.
// The length resampling method resamples proportional to length.
// The segment method divides each segment of a profile into the same number of points.
// This means that if you refine a profile with the "segment" method you will get N points
// on each edge, but if you refine a profile with the "length" method you will get new points
// distributed around the profile based on length, so small segments will get fewer new points than longer ones.
// A uniform division may be impossible, in which case the code computes an approximation, which may result
// in arbitrary distribution of extra points. See {{subdivide_path()}} for more details.
// Note that when dealing with continuous curves it is always better to adjust the
// sampling in your code to generate the desired sampling rather than using the `refine` argument.
// .
// You can choose from five methods for specifying alignment for incommensurate profiles.
// The available methods are `"distance"`, `"fast_distance"`, `"tangent"`, `"direct"` and `"reindex"`.
// It is useful to distinguish between continuous curves like a circle and discrete profiles
// like a hexagon or star, because the algorithms' suitability depend on this distinction.
// .
// The default method for aligning profiles is `method="direct"`.
// If you simply supply a list of compatible profiles it will link them up
// exactly as you have provided them. You may find that profiles you want to connect define the
// right shapes but the point lists don't start from points that you want aligned in your skinned
// polyhedron. You can correct this yourself using `reindex_polygon`, or you can use the "reindex"
// method which will look for the index choice that will minimize the length of all of the edges
// in the polyhedron—it will produce the least twisted possible result. This algorithm has quadratic
// run time so it can be slow with very large profiles.
// .
// When the profiles are incommensurate, the "direct" and "reindex" resample them to match. As noted above,
// for continuous input curves, it is better to generate your curves directly at the desired sample size,
// but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled
// to match the circle. When you use "direct" or "reindex" the default `sampling` value is
// of `sampling="length"` to approximate a uniform length sampling of the profile. This will generally
// produce the natural result for connecting two continuously sampled profiles or a continuous
// profile and a polygonal one. However depending on your particular case,
// `sampling="segment"` may produce a more pleasing result. These two approaches differ only when
// the segments of your input profiles have unequal length.
// .
// The "distance", "fast_distance" and "tangent" methods work by duplicating vertices to create
// triangular faces. In the skined object created by two polygons, every vertex of a polygon must
// have an edge that connects to some vertex on the other one. If you connect two squares this can be
// accomplished with four edges, but if you want to connect a square to a pentagon you must add a
// fifth edge for the "extra" vertex on the pentagon. You must now decide which vertex on the square to
// connect the "extra" edge to. How do you decide where to put that fifth edge? The "distance" method answers this
// question by using an optimization: it minimizes the total length of all the edges connecting
// the two polygons. This algorithm generally produces a good result when both profiles are discrete ones with
// a small number of vertices. It is computationally intensive (O(N^3)) and may be
// slow on large inputs. The resulting surfaces generally have curved faces, so be
// sure to select a sufficiently large value for `slices` and `refine`. Note that for
// this method, `sampling` must be set to `"segment"`, and hence this is the default setting.
// Using sampling by length would ignore the repeated vertices and ruin the alignment.
// The "fast_distance" method restricts the optimization by assuming that an edge should connect
// vertex 0 of the two polygons. This reduces the run time to O(N^2) and makes
// the method usable on profiles with more points if you take care to index the inputs to match.
// .
// The `"tangent"` method generally produces good results when
// connecting a discrete polygon to a convex, finely sampled curve. Given a polygon and a curve, consider one edge
// on the polygon. Find a plane passing through the edge that is tangent to the curve. The endpoints of the edge and
// the point of tangency define a triangular face in the output polyhedron. If you work your way around the polygon
// edges, you can establish a series of triangular faces in this way, with edges linking the polygon to the curve.
// You can then complete the edge assignment by connecting all the edges in between the triangular faces together,
// with many edges meeting at each polygon vertex. The result is an alternation of flat triangular faces with conical
// curves joining them. Another way to think about it is that it splits the points on the curve up into groups and
// connects all the points in one group to the same vertex on the polygon.
// .
// The "tangent" method may fail if the curved profile is non-convex, or doesn't have enough points to distinguish
// all of the tangent points from each other. The algorithm treats whichever input profile has fewer points as the polygon
// and the other one as the curve. Using `refine` with this method will have little effect on the model, so
// you should do it only for agreement with other profiles, and these models are linear, so extra slices also
// have no effect. For best efficiency set `refine=1` and `slices=0`. As with the "distance" method, refinement
// must be done using the "segment" sampling scheme to preserve alignment across duplicated points.
// Note that the "tangent" method produces similar results to the "distance" method on curved inputs. If this
// method fails due to concavity, "fast_distance" may be a good option.
// .
// It is possible to specify `method` and `refine` as arrays, but it is important to observe
// matching rules when you do this. If a pair of profiles is connected using "tangent" or "distance"
// then the `refine` values for those two profiles must be equal. If a profile is connected by
// a vertex duplicating method on one side and a resampling method on the other side, then
// `refine` must be set so that the resulting number of vertices matches the number that is
// used for the resampled profiles. The best way to avoid confusion is to ensure that the
// profiles connected by "direct" or "reindex" all have the same number of points and at the
// transition, the refined number of points matches.
// .
// Arguments:
// profiles = list of 2d or 3d profiles to be skinned. (If 2d must also give `z`.)
// slices = scalar or vector number of slices to insert between each pair of profiles. Set to zero to use only the profiles you provided. Recommend starting with a value around 10.
// ---
// refine = resample profiles to this number of points per edge. Can be a list to give a refinement for each profile. Recommend using a value above 10 when using the "distance" or "fast_distance" methods. Default: 1.
// sampling = sampling method to use with "direct" and "reindex" methods. Can be "length" or "segment". Ignored if any profile pair uses either the "distance", "fast_distance", or "tangent" methods. Default: "length".
// closed = set to true to connect first and last profile (to make a torus). Default: false
// caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false.
// method = method for connecting profiles, one of "distance", "fast_distance", "tangent", "direct" or "reindex". Default: "direct".
// z = array of height values for each profile if the profiles are 2d
// convexity = convexity setting for use with polyhedron. (module only) Default: 10
// anchor = Translate so anchor point is at the origin. Default: "origin"
// spin = Rotate this many degrees around Z axis after anchor. Default: 0
// orient = Vector to rotate top towards after spin
// atype = Select "hull" or "intersect" anchor types. Default: "hull"
// 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"
// style = vnf_vertex_array style. Default: "min_edge"
// Named Anchors:
// "origin" = The native position of the shape.
// Anchor Types:
// "hull" = Anchors to the virtual convex hull of the shape.
// "intersect" = Anchors to the surface of the shape.
// Example:
// skin([octagon(4), circle($fn=70,r=2)], z=[0,3], slices=10);
// Example: Rotating the pentagon place the zero index at different locations, giving a twist
// skin([rot(90,p=pentagon(4)), circle($fn=80,r=2)], z=[0,3], slices=10);
// Example: You can untwist it with the "reindex" method
// skin([rot(90,p=pentagon(4)), circle($fn=80,r=2)], z=[0,3], slices=10, method="reindex");
// Example: Offsetting the starting edge connects to circles in an interesting way:
// circ = circle($fn=80, r=3);
// skin([circ, rot(110,p=circ)], z=[0,5], slices=20);
// Example(FlatSpin,VPD=20):
// skin([ yrot(37,p=path3d(circle($fn=128, r=4))), path3d(square(3),3)], method="reindex",slices=10);
// Example(FlatSpin,VPD=16): Ellipses connected with twist
// ellipse = xscale(2.5,p=circle($fn=80));
// skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10);
// Example(FlatSpin,VPD=16): Ellipses connected without a twist. (Note ellipses stay in the same position: just the connecting edges are different.)
// ellipse = xscale(2.5,p=circle($fn=80));
// skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10, method="reindex");
// Example(FlatSpin,VPD=500):
// $fn=24;
// skin([
// yrot(0, p=yscale(2,p=path3d(circle(d=75)))),
// [[40,0,100], [35,-15,100], [20,-30,100],[0,-40,100],[-40,0,100],[0,40,100],[20,30,100], [35,15,100]]
// ],slices=10);
// Example(FlatSpin,VPD=600):
// $fn=48;
// skin([
// for (b=[0,90]) [
// for (a=[360:-360/$fn:0.01])
// point3d(polar_to_xy((100+50*cos((a+b)*2))/2,a),b/90*100)
// ]
// ], slices=20);
// Example: Vaccum connector example from list-comprehension-demos
// include <BOSL2/rounding.scad>
// $fn=32;
// base = round_corners(square([2,4],center=true), radius=0.5);
// skin([
// path3d(base,0),
// path3d(base,2),
// path3d(circle(r=0.5),3),
// path3d(circle(r=0.5),4),
// for(i=[0:2]) each [path3d(circle(r=0.6), i+4),
// path3d(circle(r=0.5), i+5)]
// ],slices=0);
// Example: Vaccum nozzle example from list-comprehension-demos, using "length" sampling (the default)
// xrot(90)down(1.5)
// difference() {
// skin(
// [square([2,.2],center=true),
// circle($fn=64,r=0.5)], z=[0,3],
// slices=40,sampling="length",method="reindex");
// skin(
// [square([1.9,.1],center=true),
// circle($fn=64,r=0.45)], z=[-.01,3.01],
// slices=40,sampling="length",method="reindex");
// }
// Example: Same thing with "segment" sampling
// xrot(90)down(1.5)
// difference() {
// skin(
// [square([2,.2],center=true),
// circle($fn=64,r=0.5)], z=[0,3],
// slices=40,sampling="segment",method="reindex");
// skin(
// [square([1.9,.1],center=true),
// circle($fn=64,r=0.45)], z=[-.01,3.01],
// slices=40,sampling="segment",method="reindex");
// }
// Example: Forma Candle Holder (from list-comprehension-demos)
// r = 50;
// height = 140;
// layers = 10;
// wallthickness = 5;
// holeradius = r - wallthickness;
// difference() {
// skin([for (i=[0:layers-1]) zrot(-30*i,p=path3d(hexagon(ir=r),i*height/layers))],slices=0);
// up(height/layers) cylinder(r=holeradius, h=height);
// }
// Example(FlatSpin,VPD=300): A box that is octagonal on the outside and circular on the inside
// height = 45;
// sub_base = octagon(d=71, rounding=2, $fn=128);
// base = octagon(d=75, rounding=2, $fn=128);
// interior = regular_ngon(n=len(base), d=60);
// right_half()
// skin([ sub_base, base, base, sub_base, interior], z=[0,2,height, height, 2], slices=0, refine=1, method="reindex");
// Example: Connecting a pentagon and circle with the "tangent" method produces large triangular faces and cone shaped corners.
// skin([pentagon(4), circle($fn=80,r=2)], z=[0,3], slices=10, method="tangent");
// Example: rounding corners of a square. Note that `$fn` makes the number of points constant, and avoiding the `rounding=0` case keeps everything simple. In this case, the connections between profiles are linear, so there is no benefit to setting `slices` bigger than zero.
// shapes = [for(i=[.01:.045:2])zrot(-i*180/2,cp=[-8,0,0],p=xrot(90,p=path3d(regular_ngon(n=4, side=4, rounding=i, $fn=64))))];
// rotate(180) skin( shapes, slices=0);
// Example: Here's a simplified version of the above, with `i=0` included. That first layer doesn't look good.
// shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)];
// skin(shapes, slices=0);
// Example: You can fix it by specifying "tangent" for the first method, but you still need "direct" for the rest.
// shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)];
// skin(shapes, slices=0, method=concat(["tangent"],repeat("direct",len(shapes)-2)));
// Example(FlatSpin,VPD=35): Connecting square to pentagon using "direct" method.
// skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10);
// Example(FlatSpin,VPD=35): Connecting square to shifted pentagon using "direct" method.
// skin([regular_ngon(n=4, r=4), right(4,p=regular_ngon(n=5,r=5))], z=[0,4], refine=10, slices=10);
// Example(FlatSpin,VPD=185): In this example reindexing does not fix the orientation of the triangle because it happens in 3d within skin(), so we have to reverse the triangle manually
// ellipse = yscale(3,circle(r=10, $fn=32));
// tri = move([-50/3,-9],[[0,0], [50,0], [0,27]]);
// skin([ellipse, reverse(tri)], z=[0,20], slices=20, method="reindex");
// Example(FlatSpin,VPD=185): You can get a nicer transition by rotating the polygons for better alignment. You have to resample yourself before calling `align_polygon`. The orientation is fixed so we do not need to reverse.
// ellipse = yscale(3,circle(r=10, $fn=32));
// tri = move([-50/3,-9],
// subdivide_path([[0,0], [50,0], [0,27]], 32));
// aligned = align_polygon(ellipse,tri, [0:5:180]);
// skin([ellipse, aligned], z=[0,20], slices=20);
// Example(FlatSpin,VPD=35): The "distance" method is a completely different approach.
// skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10, method="distance");
// Example(FlatSpin,VPD=35,VPT=[0,0,4]): Connecting pentagon to heptagon inserts two triangular faces on each side
// small = path3d(circle(r=3, $fn=5));
// big = up(2,p=yrot( 0,p=path3d(circle(r=3, $fn=7), 6)));
// skin([small,big],method="distance", slices=10, refine=10);
// Example(FlatSpin,VPD=35,VPT=[0,0,4]): But just a slight rotation of the top profile moves the two triangles to one end
// small = path3d(circle(r=3, $fn=5));
// big = up(2,p=yrot(14,p=path3d(circle(r=3, $fn=7), 6)));
// skin([small,big],method="distance", slices=10, refine=10);
// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Another "distance" example:
// off = [0,2];
// shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]);
// rshape = rot(180,cp=centroid(shape)+off, p=shape);
// skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15);
// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Slightly shifting the profile changes the optimal linkage
// off = [0,1];
// shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]);
// rshape = rot(180,cp=centroid(shape)+off, p=shape);
// skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15);
// Example(FlatSpin,VPD=444,VPT=[0,0,50]): This optimal solution doesn't look terrible:
// prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]);
// prof2 = path3d(regular_ngon(n=7, r=50),100);
// skin([prof1, prof2], method="distance", slices=10, refine=10);
// Example(FlatSpin,VPD=444,VPT=[0,0,50]): But this one looks better. The "distance" method doesn't find it because it uses two more edges, so it clearly has a higher total edge distance. We force it by doubling the first two vertices of one of the profiles.
// prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]);
// prof2 = path3d(regular_ngon(n=7, r=50),100);
// skin([repeat_entries(prof1,[2,2,1,1,1,1,1]),
// prof2],
// method="distance", slices=10, refine=10);
// Example(FlatSpin,VPD=80,VPT=[0,0,7]): The "distance" method will often produces results similar to the "tangent" method if you use it with a polygon and a curve, but the results can also look like this:
// skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))], method="distance", slices=0);
// Example(FlatSpin,VPD=80,VPT=[0,0,7]): Using the "tangent" method produces:
// skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))], method="tangent", slices=0);
// Example(FlatSpin,VPD=74): Torus using hexagons and pentagons, where `closed=true`
// hex = right(7,p=path3d(hexagon(r=3)));
// pent = right(7,p=path3d(pentagon(r=3)));
// N=5;
// skin(
// [for(i=[0:2*N-1]) yrot(360*i/2/N, p=(i%2==0 ? hex : pent))],
// refine=1,slices=0,method="distance",closed=true);
// Example: A smooth morph is achieved when you can calculate all the slices yourself. Since you provide all the slices, set `slices=0`.
// skin([for(n=[.1:.02:.5])
// yrot(n*60-.5*60,p=path3d(supershape(step=360/128,m1=5,n1=n, n2=1.7),5-10*n))],
// slices=0);
// Example: Another smooth supershape morph:
// skin([for(alpha=[-.2:.05:1.5])
// path3d(supershape(step=360/256,m1=7, n1=lerp(2,3,alpha),
// n2=lerp(8,4,alpha), n3=lerp(4,17,alpha)),alpha*5)],
// slices=0);
// Example: Several polygons connected using "distance"
// skin([regular_ngon(n=4, r=3),
// regular_ngon(n=6, r=3),
// regular_ngon(n=9, r=4),
// rot(17,p=regular_ngon(n=6, r=3)),
// rot(37,p=regular_ngon(n=4, r=3))],
// z=[0,2,4,6,9], method="distance", slices=10, refine=10);
// Example(FlatSpin,VPD=935,VPT=[75,0,123]): Vertex count of the polygon changes at every profile
// skin([
// for (ang = [0:10:90])
// rot([0,ang,0], cp=[200,0,0], p=path3d(circle(d=100,$fn=12-(ang/10))))
// ],method="distance",slices=10,refine=10);
// Example: Möbius Strip. This is a tricky model because when you work your way around to the connection, the direction of the profiles is flipped, so how can the proper geometry be created? The trick is to duplicate the first profile and turn the caps off. The model closes up and forms a valid polyhedron.
// skin([
// for (ang = [0:5:360])
// rot([0,ang,0], cp=[100,0,0], p=rot(ang/2, p=path3d(square([1,30],center=true))))
// ], caps=false, slices=0, refine=20);
// Example: This model of two scutoids packed together is based on https://www.thingiverse.com/thing:3024272 by mathgrrl
// sidelen = 10; // Side length of scutoid
// height = 25; // Height of scutoid
// angle = -15; // Angle (twists the entire form)
// push = -5; // Push (translates the base away from the top)
// flare = 1; // Flare (the two pieces will be different unless this is 1)
// midpoint = .5; // Height of the extra vertex (as a fraction of total height); the two pieces will be different unless this is .5)
// pushvec = rot(angle/2,p=push*RIGHT); // Push direction is the average of the top and bottom mating edges
// pent = path3d(apply(move(pushvec)*rot(angle),pentagon(side=sidelen,align_side=RIGHT,anchor="side0")));
// hex = path3d(hexagon(side=flare*sidelen, align_side=RIGHT, anchor="side0"),height);
// pentmate = path3d(pentagon(side=flare*sidelen,align_side=LEFT,anchor="side0"),height);
// // Native index would require mapping first and last vertices together, which is not allowed, so shift
// hexmate = list_rotate(
// path3d(apply(move(pushvec)*rot(angle),hexagon(side=sidelen,align_side=LEFT,anchor="side0"))),
// -1);
// join_vertex = lerp(
// mean(select(hex,1,2)), // midpoint of "extra" hex edge
// mean(select(hexmate,0,1)), // midpoint of "extra" hexmate edge
// midpoint);
// augpent = repeat_entries(pent, [1,2,1,1,1]); // Vertex 1 will split at the top forming a triangular face with the hexagon
// augpent_mate = repeat_entries(pentmate,[2,1,1,1,1]); // For mating pentagon it is vertex 0 that splits
// // Middle is the interpolation between top and bottom except for the join vertex, which is doubled because it splits
// middle = list_set(lerp(augpent,hex,midpoint),[1,2],[join_vertex,join_vertex]);
// middle_mate = list_set(lerp(hexmate,augpent_mate,midpoint), [0,1], [join_vertex,join_vertex]);
// skin([augpent,middle,hex], slices=10, refine=10, sampling="segment");
// color("green")skin([augpent_mate,middle_mate,hexmate], slices=10,refine=10, sampling="segment");
// Example: If you create a self-intersecting polyhedron the result is invalid. In some cases self-intersection may be obvous. Here is a more subtle example.
// skin([
// for (a = [0:30:180]) let(
// pos = [-60*sin(a), 0, a ],
// pos2 = [-60*sin(a+0.1), 0, a+0.1]
// ) move(pos,
// p=rot(from=UP, to=pos2-pos,
// p=path3d(circle(d=150))
// )
// )
// ],refine=1,slices=0);
// color("red") {
// zrot(25) fwd(130) xrot(75) {
// linear_extrude(height=0.1) {
// ydistribute(25) {
// text(text="BAD POLYHEDRONS!", size=20, halign="center", valign="center");
// text(text="CREASES MAKE", size=20, halign="center", valign="center");
// }
// }
// }
// up(160) zrot(25) fwd(130) xrot(75) {
// stroke(zrot(30, p=yscale(0.5, p=circle(d=120))),width=10,closed=true);
// }
// }
module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, style="min_edge", convexity=10,
anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull")
{
vnf = skin(profiles, slices, refine, method, sampling, caps, closed, z, style=style);
vnf_polyhedron(vnf,convexity=convexity,spin=spin,anchor=anchor,orient=orient,atype=atype,cp=cp)
children();
}
function skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, style="min_edge",
anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull") =
assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"")
assert(is_def(slices),"The slices argument must be specified.")
assert(is_list(profiles) && len(profiles)>1, "Must provide at least two profiles")
let(
profiles = [for(p=profiles) if (is_region(p) && len(p)==1) p[0] else p]
)
let( bad = [for(i=idx(profiles)) if (!(is_path(profiles[i]) && len(profiles[i])>2)) i])
assert(len(bad)==0, str("Profiles ",bad," are not a paths or have length less than 3"))
let(
profcount = len(profiles) - (closed?0:1),
legal_methods = ["direct","reindex","distance","fast_distance","tangent"],
caps = is_def(caps) ? caps :
closed ? false : true,
capsOK = is_bool(caps) || is_bool_list(caps,2),
fullcaps = is_bool(caps) ? [caps,caps] : caps,
refine = is_list(refine) ? refine : repeat(refine, len(profiles)),
slices = is_list(slices) ? slices : repeat(slices, profcount),
refineOK = [for(i=idx(refine)) if (refine[i]<=0 || !is_integer(refine[i])) i],
slicesOK = [for(i=idx(slices)) if (!is_integer(slices[i]) || slices[i]<0) i],
maxsize = max_length(profiles),
methodok = is_list(method) || in_list(method, legal_methods),
methodlistok = is_list(method) ? [for(i=idx(method)) if (!in_list(method[i], legal_methods)) i] : [],
method = is_string(method) ? repeat(method, profcount) : method,
// Define to be zero where a resampling method is used and 1 where a vertex duplicator is used
RESAMPLING = 0,
DUPLICATOR = 1,
method_type = [for(m = method) m=="direct" || m=="reindex" ? 0 : 1],
sampling = is_def(sampling) ? sampling :
in_list(DUPLICATOR,method_type) ? "segment" : "length"
)
assert(len(refine)==len(profiles), "refine list is the wrong length")
assert(len(slices)==profcount, str("slices list must have length ",profcount))
assert(slicesOK==[],str("slices must be nonnegative integers"))
assert(refineOK==[],str("refine must be postive integer"))
assert(methodok,str("method must be one of ",legal_methods,". Got ",method))
assert(methodlistok==[], str("method list contains invalid method at ",methodlistok))
assert(len(method) == profcount,"Method list is the wrong length")
assert(in_list(sampling,["length","segment"]), "sampling must be set to \"length\" or \"segment\"")
assert(sampling=="segment" || (!in_list("distance",method) && !in_list("fast_distance",method) && !in_list("tangent",method)), "sampling is set to \"length\" which is only allowed with methods \"direct\" and \"reindex\"")
assert(capsOK, "caps must be boolean or a list of two booleans")
assert(!closed || !caps, "Cannot make closed shape with caps")
let(
profile_dim=list_shape(profiles,2),
profiles_zcheck = (profile_dim != 2) || (profile_dim==2 && is_list(z) && len(z)==len(profiles)),
profiles_ok = (profile_dim==2 && is_list(z) && len(z)==len(profiles)) || profile_dim==3
)
assert(profiles_zcheck, "z parameter is invalid or has the wrong length.")
assert(profiles_ok,"Profiles must all be 3d or must all be 2d, with matching length z parameter.")
assert(is_undef(z) || profile_dim==2, "Do not specify z with 3d profiles")
assert(profile_dim==3 || len(z)==len(profiles),"Length of z does not match length of profiles.")
let(
// Adjoin Z coordinates to 2d profiles
profiles = profile_dim==3 ? profiles :
[for(i=idx(profiles)) path3d(profiles[i], z[i])],
// True length (not counting repeated vertices) of profiles after refinement
refined_len = [for(i=idx(profiles)) refine[i]*len(profiles[i])],
// Define this to be 1 if a profile is used on either side by a resampling method, zero otherwise.
profile_resampled = [for(i=idx(profiles))
1-(
i==0 ? method_type[0] * (closed? last(method_type) : 1) :
i==len(profiles)-1 ? last(method_type) * (closed ? select(method_type,-2) : 1) :
method_type[i] * method_type[i-1])],
parts = search(1,[1,for(i=[0:1:len(profile_resampled)-2]) profile_resampled[i]!=profile_resampled[i+1] ? 1 : 0],0),
plen = [for(i=idx(parts)) (i== len(parts)-1? len(refined_len) : parts[i+1]) - parts[i]],
max_list = [for(i=idx(parts)) each repeat(max(select(refined_len, parts[i], parts[i]+plen[i]-1)), plen[i])],
transition_profiles = [for(i=[(closed?0:1):1:profcount-1]) if (select(method_type,i-1) != method_type[i]) i],
badind = [for(tranprof=transition_profiles) if (refined_len[tranprof] != max_list[tranprof]) tranprof]
)
assert(badind==[],str("Profile length mismatch at method transition at indices ",badind," in skin()"))
let(
full_list = // If there are no duplicators then use more efficient where the whole input is treated together
!in_list(DUPLICATOR,method_type) ?
let(
resampled = [for(i=idx(profiles)) subdivide_path(profiles[i], max_list[i], method=sampling)],
fixedprof = [for(i=idx(profiles))
i==0 || method[i-1]=="direct" ? resampled[i]
: reindex_polygon(resampled[i-1],resampled[i])],
sliced = slice_profiles(fixedprof, slices, closed)
)
[!closed ? sliced : concat(sliced,[sliced[0]])]
: // There are duplicators, so use approach where each pair is treated separately
[for(i=[0:profcount-1])
let(
pair =
method[i]=="distance" ? _skin_distance_match(profiles[i],select(profiles,i+1)) :
method[i]=="fast_distance" ? _skin_aligned_distance_match(profiles[i], select(profiles,i+1)) :
method[i]=="tangent" ? _skin_tangent_match(profiles[i],select(profiles,i+1)) :
/*method[i]=="reindex" || method[i]=="direct" ?*/
let( p1 = subdivide_path(profiles[i],max_list[i], method=sampling),
p2 = subdivide_path(select(profiles,i+1),max_list[i], method=sampling)
) (method[i]=="direct" ? [p1,p2] : [p1, reindex_polygon(p1, p2)]),
nsamples = method_type[i]==RESAMPLING ? len(pair[0]) :
assert(refine[i]==select(refine,i+1),str("Refine value mismatch at indices ",[i,(i+1)%len(refine)],
". Method ",method[i]," requires equal values"))
refine[i] * len(pair[0])
)
subdivide_and_slice(pair,slices[i], nsamples, method=sampling)],
pvnf=vnf_join(
[for(i=idx(full_list))
vnf_vertex_array(full_list[i], cap1=i==0 && fullcaps[0], cap2=i==len(full_list)-1 && fullcaps[1],
col_wrap=true, style=style)]),
vnf = vnf_volume(pvnf)<0 ? vnf_reverse_faces(pvnf) : pvnf
)
reorient(anchor,spin,orient,vnf=vnf,p=vnf,extent=atype=="hull",cp=cp);
// Function&Module: linear_sweep()
// Synopsis: Create a linear extrusion from a path, with optional texturing.
// SynTags: VNF, Geom
// Topics: Extrusion, Textures, Sweep
// See Also: rotate_sweep(), sweep(), spiral_sweep(), path_sweep(), offset_sweep()
// Usage: As Module
// linear_sweep(region, [height], [center=], [slices=], [twist=], [scale=], [style=], [caps=], [convexity=]) [ATTACHMENTS];
// Usage: With Texturing
// linear_sweep(region, [height], [center=], texture=, [tex_size=]|[tex_reps=], [tex_depth=], [style=], [tex_samples=], ...) [ATTACHMENTS];
// Usage: As Function
// vnf = linear_sweep(region, [height], [center=], [slices=], [twist=], [scale=], [style=], [caps=]);
// vnf = linear_sweep(region, [height], [center=], texture=, [tex_size=]|[tex_reps=], [tex_depth=], [style=], [tex_samples=], ...);
// Description:
// If called as a module, creates a polyhedron that is the linear extrusion of the given 2D region or polygon.
// If called as a function, returns a VNF that can be used to generate a polyhedron of the linear extrusion
// of the given 2D region or polygon. The benefit of using this, over using `linear_extrude region(rgn)` is
// that it supports `anchor`, `spin`, `orient` and attachments. You can also make more refined
// twisted extrusions by using `maxseg` to subsample flat faces.
// .
// Anchoring for linear_sweep is based on the anchors for the swept region rather than from the polyhedron that is created. This can produce more
// predictable anchors for LEFT, RIGHT, FWD and BACK in many cases, but the anchors may only
// be aproximately correct for twisted objects, and corner anchors may point in unexpected directions in some cases.
// If you need anchors directly computed from the surface you can pass the vnf from linear_sweep
// to {{vnf_polyhedron()}}, which will compute anchors directly from the full VNF.
// Arguments:
// region = The 2D [Region](regions.scad) or polygon that is to be extruded.
// h / height / l / length = The height to extrude the region. Default: 1
// center = If true, the created polyhedron will be vertically centered. If false, it will be extruded upwards from the XY plane. Default: `false`
// ---
// twist = The number of degrees to rotate the top of the shape, clockwise around the Z axis, relative to the bottom. Default: 0
// scale = The amount to scale the top of the shape, in the X and Y directions, relative to the size of the bottom. Default: 1
// shift = The amount to shift the top of the shape, in the X and Y directions, relative to the position of the bottom. Default: [0,0]
// slices = The number of slices to divide the shape into along the Z axis, to allow refinement of detail, especially when working with a twist. Default: `twist/5`
// maxseg = If given, then any long segments of the region will be subdivided to be shorter than this length. This can refine twisting flat faces a lot. Default: `undef` (no subsampling)
// texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces. See {{texture()}} for what named textures are supported.
// tex_size = An optional 2D target size for the textures. Actual texture sizes will be scaled somewhat to evenly fit the available surface. Default: `[5,5]`
// tex_reps = If given instead of tex_size, a 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions on the extrusion.
// tex_inset = If numeric, lowers the texture into the surface by the specified proportion, e.g. 0.5 would lower it half way into the surface. If `true`, insets by exactly its full depth. Default: `false`
// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
// tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
// style = The style to use when triangulating the surface of the object. Valid values are `"default"`, `"alt"`, or `"quincunx"`.
// caps = If false do not create end caps. Can be a boolean vector. Default: true
// convexity = Max number of surfaces any single ray could pass through. Module use only.
// cp = Centerpoint for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: `"centroid"`
// atype = Set to "hull" or "intersect" to select anchor type. Default: "hull"
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
// Anchor Types:
// "hull" = Anchors to the virtual convex hull of the shape.
// "intersect" = Anchors to the surface of the shape.
// "bbox" = Anchors to the bounding box of the extruded shape.
// Named Anchors:
// "origin" = Centers the extruded shape vertically only, but keeps the original path positions in the X and Y. Oriented UP.
// "original_base" = Keeps the original path positions in the X and Y, but at the bottom of the extrusion. Oriented UP.
// Example: Extruding a Compound Region.
// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
// rgn2 = [square(30,center=false)];
// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
// mrgn = union(rgn1,rgn2);
// orgn = difference(mrgn,rgn3);
// linear_sweep(orgn,height=20,convexity=16);
// Example: With Twist, Scale, Shift, Slices and Maxseg.
// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
// rgn2 = [square(30,center=false)];
// rgn3 = [
// for (size=[10:10:20])
// apply(
// move([15,15]),
// square(size=size, center=true)
// )
// ];
// mrgn = union(rgn1,rgn2);
// orgn = difference(mrgn,rgn3);
// linear_sweep(
// orgn, height=50, maxseg=2, slices=40,
// twist=90, scale=0.5, shift=[10,5],
// convexity=16
// );
// Example: Anchors on an Extruded Region
// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
// rgn2 = [square(30,center=false)];
// rgn3 = [
// for (size=[10:10:20])
// apply(
// move([15,15]),
// rect(size=size)
// )
// ];
// mrgn = union(rgn1,rgn2);
// orgn = difference(mrgn,rgn3);
// linear_sweep(orgn,height=20,convexity=16)
// show_anchors();
// Example: "diamonds" texture.
// path = glued_circles(r=15, spread=40, tangent=45);
// linear_sweep(
// path, texture="diamonds", tex_size=[5,10],
// h=40, style="concave");
// Example: "pyramids" texture.
// linear_sweep(
// rect(50), texture="pyramids", tex_size=[10,10],
// h=40, style="convex");
// Example: "bricks_vnf" texture.
// path = glued_circles(r=15, spread=40, tangent=45);
// linear_sweep(
// path, texture="bricks_vnf", tex_size=[10,10],
// tex_depth=0.25, h=40);
// Example: User defined heightfield texture.
// path = ellipse(r=[20,10]);
// texture = [for (i=[0:9])
// [for (j=[0:9])
// 1/max(0.5,norm([i,j]-[5,5])) ]];
// linear_sweep(
// path, texture=texture, tex_size=[5,5],
// h=40, style="min_edge", anchor=BOT);
// Example: User defined VNF tile texture.
// path = ellipse(r=[20,10]);
// tex = let(n=16,m=0.25) [
// [
// each resample_path(path3d(square(1)),n),
// each move([0.5,0.5],
// p=path3d(circle(d=0.5,$fn=n),m)),
// [1/2,1/2,0],
// ], [
// for (i=[0:1:n-1]) each [
// [i,(i+1)%n,(i+3)%n+n],
// [i,(i+3)%n+n,(i+2)%n+n],
// [2*n,n+i,n+(i+1)%n],
// ]
// ]
// ];
// linear_sweep(path, texture=tex, tex_size=[5,5], h=40);
// Example: Textured with twist and scale.
// linear_sweep(regular_ngon(n=3, d=50),
// texture="rough", h=100, tex_depth=2,
// tex_size=[20,20], style="min_edge",
// convexity=10, scale=0.2, twist=120);
// Example: As Function
// path = glued_circles(r=15, spread=40, tangent=45);
// vnf = linear_sweep(
// path, h=40, texture="trunc_pyramids", tex_size=[5,5],
// tex_depth=1, style="convex");
// vnf_polyhedron(vnf, convexity=10);
// Example: VNF tile that has no top/bottom edges and produces a disconnected result
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
// z=[0,1/2,1],
// slices=0,
// caps=false);
// tile = move([0,1/2,2/3],yrot(90,shape));
// linear_sweep(circle(20), texture=tile,
// tex_size=[10,10],tex_depth=5,
// h=40,convexity=4);
// Example: The same tile from above, turned 90 degrees, creates problems at the ends, because the end cap is not a connected polygon. When the ends are disconnected you may find that some parts of the end cap are missing and spurious polygons included.
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
// z=[0,1/2,1],
// slices=0,
// caps=false);
// tile = move([1/2,1,2/3],xrot(90,shape));
// linear_sweep(circle(20), texture=tile,
// tex_size=[30,20],tex_depth=15,
// h=40,convexity=4);
// Example: This example shows some endcap polygons missing and a spurious triangle
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
// z=[0,1/2,1],
// slices=0,
// caps=false);
// tile = xscale(.5,move([1/2,1,2/3],xrot(90,shape)));
// doubletile = vnf_join([tile, right(.5,tile)]);
// linear_sweep(circle(20), texture=doubletile,
// tex_size=[45,45],tex_depth=15, h=40);
// Example: You can fix ends for disconnected cases using {{top_half()}} and {{bottom_half()}}
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
// z=[0,1/2,1],
// slices=0,
// caps=false);
// tile = move([1/2,1,2/3],xrot(90,shape));
// vnf_polyhedron(
// top_half(
// bottom_half(
// linear_sweep(circle(20), texture=tile,
// tex_size=[30,20],tex_depth=15,
// h=40.2,caps=false),
// z=20),
// z=-20));
module linear_sweep(
region, height, center,
twist=0, scale=1, shift=[0,0],
slices, maxseg, style="default", convexity, caps=true,
texture, tex_size=[5,5], tex_reps, tex_counts,
tex_inset=false, tex_rot=0,
tex_depth, tex_scale, tex_samples,
cp, atype="hull", h,l,length,
anchor, spin=0, orient=UP
) {
h = one_defined([h, height,l,length],"h,height,l,length",dflt=1);
region = force_region(region);
check = assert(is_region(region),"Input is not a region");
anchor = center==true? "origin" :
center == false? "original_base" :
default(anchor, "original_base");
vnf = linear_sweep(
region, height=h, style=style, caps=caps,
twist=twist, scale=scale, shift=shift,
texture=texture,
tex_size=tex_size,
tex_reps=tex_reps,
tex_counts=tex_counts,
tex_inset=tex_inset,
tex_rot=tex_rot,
tex_depth=tex_depth,
tex_samples=tex_samples,
slices=slices,
maxseg=maxseg,
anchor="origin"
);
anchors = [
named_anchor("original_base", [0,0,-h/2], UP)
];
cp = default(cp, "centroid");
geom = atype=="hull"? attach_geom(cp=cp, region=region, h=h, extent=true, shift=shift, scale=scale, twist=twist, anchors=anchors) :
atype=="intersect"? attach_geom(cp=cp, region=region, h=h, extent=false, shift=shift, scale=scale, twist=twist, anchors=anchors) :
atype=="bbox"?
let(
bounds = pointlist_bounds(flatten(region)),
size = bounds[1] - bounds[0],
midpt = (bounds[0] + bounds[1])/2
)
attach_geom(cp=[0,0,0], size=point3d(size,h), offset=point3d(midpt), shift=shift, scale=scale, twist=twist, anchors=anchors) :
assert(in_list(atype, ["hull","intersect","bbox"]), "Anchor type must be \"hull\", \"intersect\", or \"bbox\".");
attachable(anchor,spin,orient, geom=geom) {
vnf_polyhedron(vnf, convexity=convexity);
children();
}
}
function linear_sweep(
region, height, center,
twist=0, scale=1, shift=[0,0],
slices, maxseg, style="default", caps=true,
cp, atype="hull", h,
texture, tex_size=[5,5], tex_reps, tex_counts,
tex_inset=false, tex_rot=0,
tex_scale, tex_depth, tex_samples, h, l, length,
anchor, spin=0, orient=UP
) =
assert(num_defined([tex_reps,tex_counts])<2, "In linear_sweep() the 'tex_counts' parameter has been replaced by 'tex_reps'. You cannot give both.")
assert(num_defined([tex_scale,tex_depth])<2, "In linear_sweep() the 'tex_scale' parameter has been replaced by 'tex_depth'. You cannot give both.")
let(
region = force_region(region),
tex_reps = is_def(tex_counts)? echo("In linear_sweep() the 'tex_counts' parameter is deprecated and has been replaced by 'tex_reps'")tex_counts
: tex_reps,
tex_depth = is_def(tex_scale)? echo("In linear_sweep() the 'tex_scale' parameter is deprecated and has been replaced by 'tex_depth'")tex_scale
: default(tex_depth,1)
)
assert(is_region(region), "Input is not a region or polygon.")
assert(is_num(scale) || is_vector(scale))
assert(is_vector(shift, 2), str(shift))
assert(is_bool(caps) || is_bool_list(caps,2), "caps must be boolean or a list of two booleans")
let(
h = one_defined([h, height,l,length],"h,height,l,length",dflt=1)
)
!is_undef(texture)? _textured_linear_sweep(
region, h=h, caps=caps,
texture=texture, tex_size=tex_size,
counts=tex_reps, inset=tex_inset,
rot=tex_rot, tex_scale=tex_depth,
twist=twist, scale=scale, shift=shift,
style=style, samples=tex_samples,
anchor=anchor, spin=spin, orient=orient
) :
let(
caps = is_bool(caps) ? [caps,caps] : caps,
anchor = center==true? "origin" :
center == false? "original_base" :
default(anchor, "original_base"),
regions = region_parts(region),
slices = default(slices, max(1,ceil(abs(twist)/5))),
scale = is_num(scale)? [scale,scale] : point2d(scale),
topmat = move(shift) * scale(scale) * rot(-twist),
trgns = [
for (rgn = regions) [
for (path = rgn) let(
p = list_unwrap(path),
path = is_undef(maxseg)? p : [
for (seg = pair(p,true)) each
let( steps = ceil(norm(seg.y - seg.x) / maxseg) )
lerpn(seg.x, seg.y, steps, false)
]
) apply(topmat, path)
]
],
vnf = vnf_join([
for (rgn = regions)
for (pathnum = idx(rgn)) let(
p = list_unwrap(rgn[pathnum]),
path = is_undef(maxseg)? p : [
for (seg=pair(p,true)) each
let(steps=ceil(norm(seg.y-seg.x)/maxseg))
lerpn(seg.x, seg.y, steps, false)
],
verts = [
for (i=[0:1:slices]) let(
u = i / slices,
scl = lerp([1,1], scale, u),
ang = lerp(0, -twist, u),
off = lerp([0,0,-h/2], point3d(shift,h/2), u),
m = move(off) * scale(scl) * rot(ang)
) apply(m, path3d(path))
]
) vnf_vertex_array(verts, caps=false, col_wrap=true, style=style),
if (caps[0]) for (rgn = regions) vnf_from_region(rgn, down(h/2), reverse=true),
if (caps[1]) for (rgn = trgns) vnf_from_region(rgn, up(h/2), reverse=false)
]),
anchors = [
named_anchor("original_base", [0,0,-h/2], UP)
],
cp = default(cp, "centroid"),
geom = atype=="hull"? attach_geom(cp=cp, region=region, h=h, extent=true, shift=shift, scale=scale, twist=twist, anchors=anchors) :
atype=="intersect"? attach_geom(cp=cp, region=region, h=h, extent=false, shift=shift, scale=scale, twist=twist, anchors=anchors) :
atype=="bbox"?
let(
bounds = pointlist_bounds(flatten(region)),
size = bounds[1] - bounds[0],
midpt = (bounds[0] + bounds[1])/2
)
attach_geom(cp=[0,0,0], size=point3d(size,h), offset=point3d(midpt), shift=shift, scale=scale, twist=twist, anchors=anchors) :
assert(in_list(atype, ["hull","intersect","bbox"]), "Anchor type must be \"hull\", \"intersect\", or \"bbox\".")
) reorient(anchor,spin,orient, geom=geom, p=vnf);
// Function&Module: rotate_sweep()
// Synopsis: Create a surface of revolution from a path with optional texturing.
// SynTags: VNF, Geom
// Topics: Extrusion, Sweep, Revolution, Textures
// See Also: linear_sweep(), sweep(), spiral_sweep(), path_sweep(), offset_sweep()
// Usage: As Function
// vnf = rotate_sweep(shape, [angle], ...);
// Usage: As Module
// rotate_sweep(shape, [angle], ...) [ATTACHMENTS];
// Usage: With Texturing
// rotate_sweep(shape, texture=, [tex_size=]|[tex_reps=], [tex_depth=], [tex_samples=], [tex_rot=], [tex_inset=], ...) [ATTACHMENTS];
// Description:
// Takes a polygon or [region](regions.scad) and sweeps it in a rotation around the Z axis, with optional texturing.
// When called as a function, returns a [VNF](vnf.scad).
// When called as a module, creates the sweep as geometry.
// Arguments:
// shape = The polygon or [region](regions.scad) to sweep around the Z axis.
// angle = If given, specifies the number of degrees to sweep the shape around the Z axis, counterclockwise from the X+ axis. Default: 360 (full rotation)
// ---
// texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces. See {{texture()}} for what named textures are supported.
// tex_size = An optional 2D target size for the textures. Actual texture sizes will be scaled somewhat to evenly fit the available surface. Default: `[5,5]`
// tex_reps = If given instead of tex_size, a 2-vector giving the number of texture tile repetitions in the direction perpendicular to extrusion and in the direction parallel to extrusion.
// tex_inset = If numeric, lowers the texture into the surface by the specified proportion, e.g. 0.5 would lower it half way into the surface. If `true`, insets by exactly its full depth. Default: `false`
// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
// tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
// tex_taper = If given as a number, tapers the texture height to zero over the first and last given percentage of the path. If given as a lookup table with indices between 0 and 100, uses the percentage lookup table to ramp the texture heights. Default: `undef` (no taper)
// style = {{vnf_vertex_array()}} style. Default: "min_edge"
// closed = If false, and shape is given as a path, then the revolved path will be sealed to the axis of rotation with untextured caps. Default: `true`
// convexity = (Module only) Convexity setting for use with polyhedron. Default: 10
// 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"
// atype = Select "hull" or "intersect" anchor types. Default: "hull"
// anchor = Translate so anchor point is at the origin. Default: "origin"
// spin = Rotate this many degrees around Z axis after anchor. Default: 0
// orient = Vector to rotate top towards after spin (module only)
// Named Anchors:
// "origin" = The native position of the shape.
// Anchor Types:
// "hull" = Anchors to the virtual convex hull of the shape.
// "intersect" = Anchors to the surface of the shape.
// Example:
// rgn = [
// for (a = [0, 120, 240]) let(
// cp = polar_to_xy(15, a) + [30,0]
// ) each [
// move(cp, p=circle(r=10)),
// move(cp, p=hexagon(d=15)),
// ]
// ];
// rotate_sweep(rgn, angle=240);
// Example:
// rgn = right(30, p=union([for (a = [0, 90]) rot(a, p=rect([15,5]))]));
// rotate_sweep(rgn);
// Example:
// path = right(50, p=circle(d=40));
// rotate_sweep(path, texture="bricks_vnf", tex_size=[10,10], tex_depth=0.5, style="concave");
// Example:
// tex = [
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
// [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
// [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
// [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
// [0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1],
// [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
// [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
// [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
// [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
// ];
// path = arc(cp=[0,0], r=40, start=60, angle=-120);
// rotate_sweep(
// path, closed=false,
// texture=tex, tex_size=[20,20],
// tex_depth=1, style="concave");
// Example:
// include <BOSL2/beziers.scad>
// bezpath = [
// [15, 30], [10,15],
// [10, 0], [20, 10], [30,12],
// [30,-12], [20,-10], [10, 0],
// [10,-15], [15,-30]
// ];
// path = bezpath_curve(bezpath, splinesteps=32);
// rotate_sweep(
// path, closed=false,
// texture="diamonds", tex_size=[10,10],
// tex_depth=1, style="concave");
// Example:
// path = [
// [20, 30], [20, 20],
// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
// [20,-20], [20,-30],
// ];
// vnf = rotate_sweep(
// path, closed=false,
// texture="trunc_pyramids",
// tex_size=[5,5], tex_depth=1,
// style="convex");
// vnf_polyhedron(vnf, convexity=10);
// Example:
// rgn = [
// right(40, p=circle(d=50)),
// right(40, p=circle(d=40,$fn=6)),
// ];
// rotate_sweep(
// rgn, texture="diamonds",
// tex_size=[10,10], tex_depth=1,
// angle=240, style="concave");
// Example: Tapering off the ends of the texturing.
// path = [
// [20, 30], [20, 20],
// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
// [20,-20], [20,-30],
// ];
// rotate_sweep(
// path, closed=false,
// texture="trunc_pyramids",
// tex_size=[5,5], tex_depth=1,
// tex_taper=20,
// style="convex",
// convexity=10);
// Example: Tapering of textures via lookup table.
// path = [
// [20, 30], [20, 20],
// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
// [20,-20], [20,-30],
// ];
// rotate_sweep(
// path, closed=false,
// texture="trunc_pyramids",
// tex_size=[5,5], tex_depth=1,
// tex_taper=[[0,0], [10,0], [10.1,1], [100,1]],
// style="convex",
// convexity=10);
function rotate_sweep(
shape, angle=360,
texture, tex_size=[5,5], tex_counts, tex_reps,
tex_inset=false, tex_rot=0,