forked from BelfrySCAD/BOSL2
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvnf.scad
More file actions
2828 lines (2668 loc) · 135 KB
/
vnf.scad
File metadata and controls
2828 lines (2668 loc) · 135 KB
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: vnf.scad
// The Vertices'N'Faces structure (VNF) holds the data used by polyhedron() to construct objects: a vertex
// list and a list of faces. This library makes it easier to construct polyhedra by providing
// functions to construct, merge, and modify VNF data, while avoiding common pitfalls such as
// reversed faces. It can find faults in your polyhedrons. This file is for low level manipulation
// of lists of vertices and faces: it can perform some simple transformations on VNF structures
// but *cannot* perform boolean operations on the polyhedrons represented by VNFs.
// Includes:
// include <BOSL2/std.scad>
// FileGroup: Advanced Modeling
// FileSummary: Vertices 'n' Faces structure. Makes polyhedron() easier to use.
// FileFootnotes: STD=Included in std.scad
//////////////////////////////////////////////////////////////////////
_BOSL2_VNF = is_undef(_BOSL2_STD) && (is_undef(BOSL2_NO_STD_WARNING) || !BOSL2_NO_STD_WARNING) ?
echo("Warning: vnf.scad included without std.scad; dependencies may be missing\nSet BOSL2_NO_STD_WARNING = true to mute this warning.") true : true;
// Section: Creating Polyhedrons with VNF Structures
// VNF stands for "Vertices'N'Faces". VNF structures are 2-item lists, `[VERTICES,FACES]` where the
// first item is a list of vertex points, and the second is a list of face indices into the vertex
// list. Each VNF is self contained, with face indices referring only to its own vertex list.
// You can construct a `polyhedron()` in parts by describing each part in a self-contained VNF, then
// merge the various VNFs to get the completed polyhedron vertex list and faces.
/// Constant: EMPTY_VNF
/// Description:
/// The empty VNF data structure. Equal to `[[],[]]`.
EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
// Function&Module: vnf_vertex_array()
// Synopsis: Returns a VNF structure from a rectangular vertex list.
// SynTags: VNF, Geom
// Topics: VNF Generators, Lists, Textures
// See Also: vnf_tri_array(), vnf_join(), vnf_from_polygons(), vnf_from_region()
// Usage:
// vnf = vnf_vertex_array(points, [caps=], [cap1=], [cap2=], [style=], [reverse=], [col_wrap=], [row_wrap=], [triangulate=]);
// vnf_vertex_array(points, [caps=], [cap1=], [cap2=], [style=], [reverse=], [col_wrap=], [row_wrap=], [triangulate=],...) [ATTACHMENTS];
// Description:
// Creates a VNF structure from a rectangular vertex list, creating edges that connect the adjacent vertices in the vertex list
// and creating the faces defined by those edges. You can optionally create the edges and faces to wrap the last column
// back to the first column, or wrap the last row to the first. Endcaps can be added to either
// the first and/or last rows. The style parameter determines how the quadrilaterals are divided into
// triangles. The styles are:
// * "default" — arbitrary, systematic subdivision in the same direction
// * "alt" — uniform subdivision in the other (alternate) direction
// * "flip1" — arbitrary division that alternates the direction adjacent pairs of quadrilaterals.
// * "flip2" — the alternating division that is the opposite of "flip1".
// * "min_edge" — subdivide each quadrilateral on its shorter edge, so the division may not be uniform across the shape
// * "min_area" — creates the triangulation with the minimal area.
// * "quincunx" — adds a vertex in the center of each quadrilateral and creates four triangles
// * "convex" — choose the locally convex division
// * "concave" — choose the locally concave division
// * "quad" — makes quadrilateral edges, which may not be coplanar, relying on OpensCAD to decide how to handle them.
// Degenerate faces are not included in the output, but if this results in unused vertices, those unused vertices do still appear in the output.
// .
// The vertex list *must* be a rectangular array. If rows of points are generated based on a radius and one of
// special variables `$fs` or `$fa`, the number of points may not be constant from row to row, causing the
// array to be non-rectangular. Consider using `$fn` instead, or use {{vnf_tri_array()}} to create a VNF
// object from a non-rectangular array.
// .
// You can apply a texture to the vertex array VNF using the usual texture parameters.
// See [Texturing](skin.scad#section-texturing) for more details on how textures work.
// The top left corner of the texture tile is aligned with `points[0][0]`, and the the X and Y directions correspond to `points[y][x]`.
// In practice, it is probably easiest to observe the result and apply a suitable texture tile rotation by setting `tex_rot` if the result
// is not what you wanted. The reference scale of your point data is also taken from the square at the [0][0] corner. This determines
// the meaning of `tex_size` and it also affects the vertical texture scale. The size of the texture tiles is proportional to the point
// spacing of the location where they are placed, so if the points are closer together, you get small texture elements. The specified `tex_depth`
// is correct at the `points[0][0]` but would be different at places in the point array where the scale is different. This
// differs from {{rotate_sweep()}}, which uses a uniform resampling of the curve you specify.
// .
// The vertical scale of texture elements adjusts based on the size of the grid square where it is placed. By default, the height is scaled by the average
// of the width and height of the texture element. You can disable this scaling by setting `tex_scaling="const"`, which results
// in a constant height that does not vary with the grid spacing.
// .
// The point data for `vnf_vertex_array()` is resampled using bilinear interpolation to match the required point density of the tile count, but the
// sampling is based on the grid, not on the distance between points. If you want to
// avoid resampling, match the point data to the required point number for your tile count. For height field textures this means
// the number of data points must equal the tile count times the number of entries in the tile minus `tex_skip` plus `tex_extra`.
// Note that `tex_extra` defaults to 1 along dimensions that are not wrapped. For a VNF tile you need to have the the point
// count equal to the tile count times tex_samples, plus one if wrapping is disabled.
// .
// For creating the texture, `vnf_vertex_array()` uses normals to the surface that it estimates from the surface data itself.
// If you have more accurate normals or need the normals to take particular values, you can pass an array of normals
// using the `normals` parameter.
// .
// You can set `return_edges=true` to return the paths of the four edges of the output. In this case the return value
// is `[vnf,edgelist]` where edgelist is [left (column 0 of points), right (last column of points), top (points[0]), bottom (last(points)]. If a given
// edge does not exist then it will be the empty list in the output. An edge only exists it is not capped and not wrapped. The main
// need for this feature is when you have added a texture and need a way to interface the shape with something else. In this case you cannot
// easily determine the edges yourself from the input point list. edges are not easily
// Arguments:
// points = A list of vertices to divide into columns and rows.
// ---
// caps = If true, add endcap faces to the first **and** last rows.
// cap1 = If true, add an endcap face to the first row.
// cap2 = If true, add an endcap face to the last row.
// col_wrap = If true, add faces to connect the last column to the first.
// row_wrap = If true, add faces to connect the last row to the first.
// reverse = If true, reverse all face normals.
// style = The style of subdividing the quads into faces. Valid options are "default", "alt", "flip1", "flip2", "min_edge", "min_area", "quincunx", "convex" and "concave".
// triangulate = If true, triangulates endcaps to resolve possible CGAL issues. This can be an expensive operation if the endcaps are complex. Default: false
// convexity = (module) Max number of times a line could intersect a wall of the shape.
// 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 at `points[0][0]`. Actual texture sizes are scaled somewhat to evenly fit the available surface.
// tex_reps = If given instead of tex_size, a 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions.
// 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_extra = number of extra lines of a hightfield texture to add at the end. Can be a scalar or 2-vector to give x and y values. Default: 1
// tex_skip = number of lines of a heightfield texture to skip when starting. Can be a scalar or two vector to give x and y values. Default: 0
// sidecaps = if `col_wrap==false` this controls whether to cap any floating ends of a VNF tile on the texture. Does not affect the main texture surface. Ignored it doesn't apply. Default: false
// sidecap1 = set sidecap only for the `points[][0]` edge of the output
// sidecap2 = set sidecap only for the `points[][max]` edge of the output
// tex_scaling = set to "const" to disable grid size vertical scaling of the texture. Default: "default"
// normals = array of normal vectors to each point in the point array for more accurate texture height calculation
// return_edges = if true return [vnf,edgelist] where edgelist is the paths of four edges, [left (column 0 of points), right (last column of points), top (points[0]), bottom (last(points)]. Default: false
// cp = (module) 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"
// anchor = (module) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
// spin = (module) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = (module) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
// atype = (module) Select "hull" or "intersect" anchor type. Default: "hull"
// Anchor Types:
// "hull" = Anchors to the virtual convex hull of the shape.
// "intersect" = Anchors to the surface of the shape.
// Named Anchors:
// "origin" = Anchor at the origin, oriented UP.
// Example(3D):
// vnf = vnf_vertex_array(
// points=[
// for (h = [0:5:179]) [
// for (t = [0:5:359])
// cylindrical_to_xyz(100 + 12 * cos((h/2 + t)*6), t, h)
// ]
// ],
// col_wrap=true, caps=true, reverse=true, style="alt"
// );
// vnf_polyhedron(vnf);
// Example(3D,NoAxes,ThrownTogether,VPD=183): Open shape made from a three arcs.
// rows = [
// for(h=[-20:20:20])
// path3d(arc(r=40-abs(h), angle=280, 10), h)
// ];
// vnf = vnf_vertex_array(rows, reverse=true);
// vnf_polyhedron(vnf);
// color("green") vnf_wireframe(vnf);
// Example(3D,NoAxes,ThrownTogether,VPD=183): Open shape made from a three arcs, with `row_wrap=true`.
// rows = [
// for(h=[-20:20:20])
// path3d(arc(r=40-abs(h), angle=280, 10), h)
// ];
// vnf = vnf_vertex_array(rows, reverse=true, row_wrap=true);
// vnf_polyhedron(vnf);
// color("green") vnf_wireframe(vnf);
// Example(3D,NoAxes,ThrownTogether,VPD=183): Open shape made from a three arcs, with `col_wrap=true`.
// rows = [
// for(h=[-20:20:20])
// path3d(arc(r=40-abs(h), angle=280, 10), h)
// ];
// vnf = vnf_vertex_array(rows, reverse=true, col_wrap=true);
// vnf_polyhedron(vnf);
// color("green") vnf_wireframe(vnf);
// Example(3D,NoAxes,ThrownTogether,VPD=183): Open shape made from a three arcs, with `caps=true` and `col_wrap=true`.
// rows = [
// for(h=[-20:20:20])
// path3d(arc(r=40-abs(h), angle=280, 10), h)
// ];
// vnf = vnf_vertex_array(rows, reverse=true, caps=true, col_wrap=true);
// vnf_polyhedron(vnf);
// color("green") vnf_wireframe(vnf);
// Example(3D): Both `col_wrap` and `row_wrap` are true to make a torus.
// vnf = vnf_vertex_array(
// points=[
// for (a=[0:5:359])
// apply(
// zrot(a) * right(30) * xrot(90),
// path3d(circle(d=20))
// )
// ],
// col_wrap=true, row_wrap=true, reverse=true
// );
// vnf_polyhedron(vnf);
// Example(3D): Möbius Strip. Note that `row_wrap` is not used, and the first and last profile copies are the same.
// vnf = vnf_vertex_array(
// points=[
// for (a=[0:5:360]) apply(
// zrot(a) * right(30) * xrot(90) * zrot(a/2+60),
// path3d(square([1,10], center=true))
// )
// ],
// col_wrap=true, reverse=true
// );
// vnf_polyhedron(vnf);
// Example(3D): Assembling a Polyhedron from Multiple Parts
// wall_points = [
// for (a = [-90:2:90]) apply(
// up(a) * scale([1-0.1*cos(a*6),1-0.1*cos((a+90)*6),1]),
// path3d(circle(d=100))
// )
// ];
// cap = [
// for (a = [0:0.01:1+EPSILON]) apply(
// up(90-5*sin(a*360*2)) * scale([a,a,1]),
// wall_points[0]
// )
// ];
// cap1 = [for (p=cap) down(90, p=zscale(-1, p=p))];
// cap2 = [for (p=cap) up(90, p=p)];
// vnf1 = vnf_vertex_array(points=wall_points, col_wrap=true);
// vnf2 = vnf_vertex_array(points=cap1, col_wrap=true);
// vnf3 = vnf_vertex_array(points=cap2, col_wrap=true, reverse=true);
// vnf_polyhedron([vnf1, vnf2, vnf3]);
// Example(3D): Building a Multi-Stage Cylindrical Ramp
// include <BOSL2/rounding.scad>
// major_r = 50;
// groove_profile = [
// [-10,0], each arc(points=[[-7,0],[0,-3],[7,0]]), [10,0]
// ];
// ramp_profile = [ [-10,25], [90,25], [180,5], [190,5] ];
// rgroove = apply(right(major_r) * xrot(90), path3d(groove_profile));
// rprofile = round_corners(ramp_profile, radius=20, closed=false, $fn=72);
// vnf = vnf_vertex_array([
// for (a = [ramp_profile[0].x : 1 : last(ramp_profile).x]) let(
// z = lookup(a,rprofile),
// m = zrot(a) * up(z)
// )
// apply(m, [ [rgroove[0].x,0,-z], each rgroove, [last(rgroove).x,0,-z] ])
// ], caps=true, col_wrap=true, reverse=true);
// vnf_polyhedron(vnf, convexity=8);
// Example(3D,NoAxes,VPR=[73,0,27],VPD=260,VPT=[0,0,42]): This vase shape cannot be constructed with rotational or linear sweeps. Using a vertex array to create a stack of polygons is the most practical way to make this and many other shapes. The cross-section is a rounded 9-pointed star that changes size and rotates back and forth as it rises in the z direction.
// include <BOSL2/rounding.scad>
//
// vprofile =
// smooth_path([[25,0], [35,8], [45,20], [40,40], [25,50], [30,65], [32,70], [37,80]],
// relsize=1, method="corners");
// ridgepd = 20; // z period of star point wiggle
// ridgeamp = 5; // amplitude of star point wiggle
// polystack = [
// for(p=vprofile) let(r=p.x, z=p.y)
// path3d(
// smooth_path(
// zrot(ridgeamp*sin(360*z/ridgepd), p=star(11, or=r+ridgeamp, ir=r-ridgeamp)),
// relsize=0.6, splinesteps=5, method="corners", closed=true),
// z)
// ];
// vnf_polyhedron(vnf_vertex_array(polystack, col_wrap=true, caps=true));
// Example(3D,NoAxes,VPR=[73,0,27],VPD=260,VPT=[0,0,42]): The previous vase shape with a pebbly texture, simply by adding `texture="dots"` to the `vnf_vertex_array()` call. Because textures are spread over grid units and not measurement units, the data points in the polygon stack should be uniformly spaced.
// include <BOSL2/rounding.scad>
//
// vprofile = resample_path(
// smooth_path([[25,0], [35,8], [45,20], [40,40], [25,50], [30,65], [32,70], [37,80]],
// relsize=1, method="corners"),
// 81, closed=false);
// ridgepd = 20; // z period of star point wiggle
// ridgeamp = 5; // amplitude of star point wiggle
// polystack = [
// for(p=vprofile) let(r=p.x, z=p.y)
// path3d(
// smooth_path(
// zrot(ridgeamp*sin(360*z/ridgepd), p=star(11, or=r+ridgeamp, ir=r-ridgeamp)),
// relsize=0.6, splinesteps=5, method="corners", closed=true),
// z)
// ];
// vnf_polyhedron(vnf_vertex_array(polystack, col_wrap=true, caps=true,
// texture="dots", tex_samples=1, tex_size=5));
// Example(3D,Med,NoAxes,VPR=[0,0,0],VPD=126.00,VPT=[-0.35,-0.54,4.09]): This point array defines a simple square, but with a non-uniform grid.
// pts = [for(x=[-1:.1:1])
// [for(y=[-1:.1:1])
// zrot(45*min([abs(x-1),abs(x+1),abs(y-1),abs(y+1)]),
// 20*[x,y,0])]];
// vnf=vnf_vertex_array(pts);
// color("blue") vnf_wireframe(vnf,width=.2);
// Example(3D,Med,NoAxes,VPR=[0,0,0],VPD=126.00,VPT=[-0.35,-0.54,4.09]): The non-uniform grid gives rise to a non-uniform texturing, showing the effect of the uniformity and distribution of the points when creating a texture.
// pts = [for(x=[-1:.1:1])
// [for(y=[-1:.1:1])
// zrot(45*min([abs(x-1),abs(x+1),abs(y-1),abs(y+1)]),
// 20*[x,y,0])]];
// vnf_vertex_array(pts,texture="dots",tex_reps=15);
// Example(3D,Med,NoAxes,VPD=300,VPT=[48,48,0]): Here is another example showing the effect of nonuniform sampling. Here is a surface with a wrinkle in both x and y directions, using location data generated by {{smooth_path()}}, which uses beziers. Bezier curves have non-uniformly distributed points, indicated by the red dots along each edge, which results in a non-uniform texture tiling.
// include <BOSL2/rounding.scad>
//
// xprofile = smooth_path([[0,0,0], [25,0,0], [49,0,-10], [51,0,10], [75,0,0], [100,0,0]],
// relsize=1, method="corners", splinesteps=4);
// yprofile = smooth_path([[0,0,0], [0,25,0], [0,49,-10], [0,51,10], [0,75,0], [0,100,0]],
// relsize=1, method="corners", splinesteps=4);
// polystack = [
// for(xp=xprofile) [
// for(yp=yprofile) [xp.x, yp.y, xp.z+yp.z]
// ]
// ];
// vnf_vertex_array(polystack, texture="checkers", tex_depth=2, tex_reps=[8,8]);
// color("red") {
// for(p=xprofile) translate(p-[0,4,0]) sphere(1.5);
// for(p=yprofile) translate(p-[4,0,0]) sphere(1.5);
// }
// Example(3D,Med,NoAxes,VPD=300,VPT=[48,48,0]): By passing the spline curves into {{resample_path()}}, we can get a uniform distribution of the x and y profile points, as shown by the red dots, which results in a uniform texture tiling.
// include <BOSL2/rounding.scad>
//
// xprof = smooth_path([[0,0,0], [25,0,0], [49,0,-10], [51,0,10], [75,0,0], [100,0,0]],
// relsize=1, method="corners", splinesteps=4);
// yprof = smooth_path([[0,0,0], [0,25,0], [0,49,-10], [0,51,10], [0,75,0], [0,100,0]],
// relsize=1, method="corners", splinesteps=4);
// xprofile = resample_path(xprof, len(xprof), closed=false);
// yprofile = resample_path(yprof, len(yprof), closed=false);
// polystack = [
// for(xp=xprofile) [
// for(yp=yprofile) [xp.x, yp.y, xp.z+yp.z]
// ]
// ];
// vnf_vertex_array(polystack, texture="checkers", tex_depth=2, tex_reps=[8,8]);
// color("red") {
// for(p=xprofile) translate(p-[0,4,0]) sphere(1.5);
// for(p=yprofile) translate(p-[4,0,0]) sphere(1.5);
// }
module vnf_vertex_array(
points,
caps, cap1, cap2,
col_wrap=false,
row_wrap=false,
reverse=false,
style="default",
triangulate = false,
texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0,
tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2, tex_scaling="default",
convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull")
{
vnf = vnf_vertex_array(points=points, caps=caps, cap1=cap1, cap2=cap2,
col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse, style=style,triangulate=triangulate, tex_scaling=tex_scaling,
texture=texture, tex_reps=tex_reps, tex_size=tex_size, tex_samples=tex_samples, tex_inset=tex_inset, tex_rot=tex_rot,
tex_depth=tex_depth, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps,sidecap1=sidecap1,sidecap2=sidecap2
);
vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) children();
}
function vnf_vertex_array(
points,
caps, cap1, cap2,
col_wrap=false,
row_wrap=false,
reverse=false,
style="default",
triangulate = false, return_edges=false,
texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, tex_scaling="default",
tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2, normals
) =
assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2","quad"]))
assert(is_matrix(points[0], n=3),"\nPoint array has the wrong shape or points are not 3d.")
assert(is_consistent(points), "\nNon-rectangular or invalid point array (vnf_tri_array() may work).")
assert(is_bool(triangulate))
is_def(texture) ?
_textured_point_array(points=points, texture=texture, tex_reps=tex_reps, tex_size=tex_size,
tex_inset=tex_inset, tex_samples=tex_samples, tex_rot=tex_rot, tex_scaling=tex_scaling, return_edges=return_edges,
col_wrap=col_wrap, row_wrap=row_wrap, tex_depth=tex_depth, caps=caps, cap1=cap1, cap2=cap2, reverse=reverse,
style=style, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps, sidecap1=sidecap1, sidecap2=sidecap2,normals=normals,triangulate=triangulate)
:
assert(!(any([caps,cap1,cap2]) && !col_wrap), "\ncol_wrap must be true if caps are requested (without texture).")
assert(!(any([caps,cap1,cap2]) && row_wrap), "\nCannot combine caps with row_wrap (without texture).")
let(
pts = flatten(points),
pcnt = len(pts),
rows = len(points),
cols = len(points[0])
)
rows<=1 || cols<=1 ? EMPTY_VNF :
let(
cap1 = first_defined([cap1,caps,false]),
cap2 = first_defined([cap2,caps,false]),
colcnt = cols - (col_wrap?0:1),
rowcnt = rows - (row_wrap?0:1),
verts = [
each pts,
if (style=="quincunx")
for (r = [0:1:rowcnt-1], c = [0:1:colcnt-1])
let(
i1 = ((r+0)%rows)*cols + ((c+0)%cols),
i2 = ((r+1)%rows)*cols + ((c+0)%cols),
i3 = ((r+1)%rows)*cols + ((c+1)%cols),
i4 = ((r+0)%rows)*cols + ((c+1)%cols)
)
mean([pts[i1], pts[i2], pts[i3], pts[i4]])
],
allfaces = [
if (cap1) count(cols,reverse=!reverse),
if (cap2) count(cols,(rows-1)*cols, reverse=reverse),
for (r = [0:1:rowcnt-1], c=[0:1:colcnt-1])
each
let(
i1 = ((r+0)%rows)*cols + ((c+0)%cols),
i2 = ((r+1)%rows)*cols + ((c+0)%cols),
i3 = ((r+1)%rows)*cols + ((c+1)%cols),
i4 = ((r+0)%rows)*cols + ((c+1)%cols),
faces =
style=="quincunx"?
let(i5 = pcnt + r*colcnt + c)
[[i1,i5,i2],[i2,i5,i3],[i3,i5,i4],[i4,i5,i1]]
: style=="min_area"?
let(
area42 = norm(cross(pts[i2]-pts[i1], pts[i4]-pts[i1]))+norm(cross(pts[i4]-pts[i3], pts[i2]-pts[i3])),
area13 = norm(cross(pts[i1]-pts[i4], pts[i3]-pts[i4]))+norm(cross(pts[i3]-pts[i2], pts[i1]-pts[i2])),
minarea_edge = area42 < area13 + _EPSILON
? [[i1,i4,i2],[i2,i4,i3]]
: [[i1,i3,i2],[i1,i4,i3]]
)
minarea_edge
: style=="min_edge"?
let(
d42=norm(pts[i4]-pts[i2]),
d13=norm(pts[i1]-pts[i3]),
shortedge = d42<d13+_EPSILON
? [[i1,i4,i2],[i2,i4,i3]]
: [[i1,i3,i2],[i1,i4,i3]]
)
shortedge
: style=="convex"?
let( // Find normal for 3 of the points. Is the other point above or below?
n = (reverse?-1:1)*cross(pts[i2]-pts[i1],pts[i3]-pts[i1]),
convexfaces = n==0
? [[i1,i4,i3]]
: n*pts[i4] > n*pts[i1]
? [[i1,i4,i2],[i2,i4,i3]]
: [[i1,i3,i2],[i1,i4,i3]]
)
convexfaces
: style=="concave"?
let( // Find normal for 3 of the points. Is the other point above or below?
n = (reverse?-1:1)*cross(pts[i2]-pts[i1],pts[i3]-pts[i1]),
concavefaces = n==0
? [[i1,i4,i3]]
: n*pts[i4] <= n*pts[i1]
? [[i1,i4,i2],[i2,i4,i3]]
: [[i1,i3,i2],[i1,i4,i3]]
)
concavefaces
: style=="quad" ? [[i1,i2,i3,i4]]
: style=="alt" || (style=="flip1" && ((r+c)%2==0)) || (style=="flip2" && ((r+c)%2==1)) || (style=="random" && rands(0,1,1)[0]<.5)?
[[i1,i4,i2],[i2,i4,i3]]
: [[i1,i3,i2],[i1,i4,i3]],
// remove degenerate faces
culled_faces= [for(face=faces)
if (norm(cross(verts[face[1]]-verts[face[0]],
verts[face[2]]-verts[face[0]]))>_EPSILON)
face
],
rfaces = reverse? [for (face=culled_faces) reverse(face)] : culled_faces
)
rfaces,
],
vnf = [verts, allfaces],
tvnf = triangulate? vnf_triangulate(vnf) : vnf
)
!return_edges ? tvnf
: [tvnf, [
if (!col_wrap) deduplicate(column(points,0)) else [],
if (!col_wrap) deduplicate(column(points, len(points[0])-1)) else [],
if (!cap1 && !row_wrap) deduplicate(points[0]) else [],
if (!cap2 && !row_wrap) deduplicate(last(points)) else []
]
];
// Function&Module: vnf_tri_array()
// Synopsis: Returns a VNF from an array of points. The array need not be rectangular.
// SynTags: VNF
// Topics: VNF Generators, Lists
// See Also: vnf_vertex_array(), vnf_join(), vnf_from_polygons(), vnf_merge_points()
// Usage:
// vnf = vnf_tri_array(points, [caps=], [cap1=], [cap2=], [reverse=], [col_wrap=], [row_wrap=], [limit_bunching=])
// vnf_tri_array(points, [caps=], [cap1=], [cap2=], [reverse=], [col_wrap=], [row_wrap=], [limit_bunching=],...) [ATTACHMENTS];
// Description:
// Produces a VNF from an array of points where each row length can differ from the adjacent rows by
// any amount. This enables the construction of triangular or even irregular VNF patches. The
// resulting VNF can be wrapped along the rows by setting `row_wrap` to true, and wrapped along
// columns by setting `col_wrap` to true. It is possible to do both at once.
// If `row_wrap` is false or not provided, end caps can be generated across the top and/or bottom rows.
// .
// The algorithm starts with the first point on each row and recursively walks around finding the
// minimum-length edge to make each new triangle face. This may result in several triangles being
// connected to one vertex. When triangulating two rows that happen to be equal length, the result is
// equivalent to {{vnf_vertex_array()}} using the "min_edge" style. If you already have a rectangular
// vertex list (equal length rows), you should use `vnf_vertex_array()` if you need a different
// triangulation style.
// .
// Because the algorithm seeks the minimum-length new edge to generate triangles between two
// unequal-lengthy rows of vertices, there are cases where this can causing bunching of several
// triangles sharing a single vertex, if several successive points of one row are closest to a single
// point on the other row. Example 6 demonstrates this. If the two rows are equal in length, this
// doesn't happen. The `limit_bunching` parameter, by default, limits the number of *additional*
// triangles that would normally be generated to the difference between the row lengths. Example 6
// demonstrates the effect of disabling this limit.
// .
// If you need to merge two VNF arrays that share edges using `vnf_join()` you can remove the
// duplicated vertices using `vnf_merge_points()`.
// Arguments:
// points = List of point lists for each row.
// ---
// caps = If true, add endcap faces to the first **and** last rows.
// cap1 = If true, add an endcap face to the first row.
// cap2 = If true, add an endcap face to the last row.
// col_wrap = If true, add faces to connect the last column to the first.
// row_wrap = If true, add faces to connect the last row to the first.
// reverse = If true, reverse all face normals.
// limit_buncthing = If true, when triangulating between two rows of unequal length, then limit the number of additional triangles that would normally share a vertex. Ignored when the two row lengths are equal. If false, a vertex can be shared by unlimited triangles. Default: true
// convexity = (module) Max number of times a line could intersect a wall of the shape.
// cp = (module) 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"
// anchor = (module) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
// spin = (module) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = (module) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
// atype = (module) Select "hull" or "intersect" anchor type. Default: "hull"
// Anchor Types:
// "hull" = Anchors to the virtual convex hull of the shape.
// "intersect" = Anchors to the surface of the shape.
// Named Anchors:
// "origin" = Anchor at the origin, oriented UP.
// Example(3D,NoAxes): Each row has one more point than the preceeding one.
// pts = [for(y=[1:1:10]) [for(x=[0:y-1]) [x,y,y]]];
// vnf = vnf_tri_array(pts);
// vnf_wireframe(vnf,width=0.1);
// color("red")move_copies(flatten(pts)) sphere(r=.15,$fn=9);
// Example(3D,NoAxes): Each row has two more points than the preceeding one.
// pts = [for(y=[0:2:10]) [for(x=[-y/2:y/2]) [x,y,y]]];
// vnf = vnf_tri_array(pts);
// vnf_wireframe(vnf,width=0.1);
// color("red")move_copies(flatten(pts)) sphere(r=.15,$fn=9);
// Example(3D): Merging two VNFs to construct a cone with one point length change between rows.
// pts1 = [for(z=[0:10]) path3d(arc(3+z,r=z/2+1, angle=[0,180]),10-z)];
// pts2 = [for(z=[0:10]) path3d(arc(3+z,r=z/2+1, angle=[180,360]),10-z)];
// vnf = vnf_join([vnf_tri_array(pts1),
// vnf_tri_array(pts2)]);
// color("green")vnf_wireframe(vnf,width=0.1);
// vnf_polyhedron(vnf);
// Example(3D): Cone with length change two between rows
// pts1 = [for(z=[0:1:10]) path3d(arc(3+2*z,r=z/2+1, angle=[0,180]),10-z)];
// pts2 = [for(z=[0:1:10]) path3d(arc(3+2*z,r=z/2+1, angle=[180,360]),10-z)];
// vnf = vnf_join([vnf_tri_array(pts1),
// vnf_tri_array(pts2)]);
// color("green")vnf_wireframe(vnf,width=0.1);
// vnf_polyhedron(vnf);
// Example(3D,NoAxes): The number of points per row can change irregularly by any amount.
// lens = [10,9,8,6,4,8,2,5,3,10,4];
// pts = [for(y=idx(lens)) lerpn([-lens[y],y,y],[lens[y],y,y],lens[y])];
// vnf = vnf_tri_array(pts);
// vnf_wireframe(vnf,width=0.1);
// color("red")move_copies(flatten(pts)) sphere(r=.15,$fn=9);
// Example(3D,Med,NoAxes,Edges,VPR=[29,0,341],VPD=45,VPT=[11,5,2]): The default parameter `limit_bunching=true` prevents too many triangles from sharing a single vertex in one row, if several points of one row happen to be closest to a single point on another row. In the left figure, `limit_bunching=false`, causing an endpoint on each row to get many triangles from the other row, because the algorithm seeks the shortest triangle leg distance once the first two points of each row are connected. This doesn't happen if both rows are the same length. The figure on the right uses the default `limit_bunching=true`, forcing the triangulation to stop adding too many triangles to the same vertex.
// pts = [
// [[5,0,0], [4,0,1.4], [3,0,2], [2,0,1.4], [1,0,0]],
// [[14,10,0], [12,9,5], [9,8,7], [6,7,7], [3,6,5], [0,5,0]]
// ];
// vnf_tri_array(pts, limit_bunching=false);
// right(10) vnf_tri_array(pts);
// Example(3D,NoAxes,Edges,VPR=[65,0,25],VPD=380,Med): Model of a cymbal with roughly same-size facets, using a different number of points for each concentric ring of vertices.
// bez = [
// [[0,26], [35,26], [29,0], [80,16], [102,0]], //top
// [[99,-1], [79,15], [28,-1], [34,25], [-1,25]] // bottom
// ];
// points = [
// for(b=bez)
// for(u=[0.01:0.04:1]) let(
// bzp = bezier_points(b,u),
// r = bzp[0],
// n = max(3, round(360 / (6/r * 180/PI)))
// ) path3d(regular_ngon(n, r=r), bzp[1])
// ];
// vnf = vnf_tri_array(points, reverse=true, col_wrap=true, caps=true);
// color("brown") difference() {
// vnf_polyhedron(vnf);
// cylinder(30, d=8);
// }
module vnf_tri_array(
points,
caps, cap1, cap2,
col_wrap=false,
row_wrap=false,
reverse=false,
limit_bunching=true,
convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull")
{
vnf = vnf_tri_array(points=points, caps=caps, cap1=cap1, cap2=cap2,
col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse,
limit_bunching = limit_bunching);
vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) children();
}
function vnf_tri_array(
points,
caps, cap1, cap2,
col_wrap=false,
row_wrap=false,
reverse=false,
limit_bunching=true
) =
assert(!(any([caps,cap1,cap2]) && row_wrap), "\nCannot combine caps with row_wrap.")
let(
plen = len(points),
// append first vertex of each polygon to its end if wrapping columns
st = col_wrap ? [
for(i=[0:plen-1])
points[i][0] != points[i][len(points[i])-1]
? concat(points[i], [points[i][0]])
: points[i]
] : points,
addcol = col_wrap ? len(st[0])-len(points[0]) : 0,
rowstarts = [ for(i=[0:plen-1]) len(st[i]) ],
capfirst = first_defined([cap1,caps,false]),
caplast = first_defined([cap2,caps,false]),
pcumlen = [0, each cumsum(rowstarts)],
faces = [
// close first end
if (capfirst)
if (reverse) [ for(i=[0:rowstarts[0]-1-addcol]) i ]
else [ for(i=[rowstarts[0]-1-addcol:-1:0]) i ],
// triangulate between the two polygons
for(i = [0:plen-2+(row_wrap?1:0)])
let(
j = (i+1)%plen,
max_extra_edges = limit_bunching ? max(1, abs(len(st[i])-len(st[j]))) : INF
) each _lofttri(st[i], st[j], pcumlen[i], pcumlen[j], rowstarts[i], rowstarts[j], reverse, trimax=max_extra_edges),
// close up the last end
if (caplast)
if (reverse) [ for(i=[pcumlen[plen]-1-addcol:-1:pcumlen[plen-1]]) i ]
else [ for(i=[pcumlen[plen-1]:pcumlen[plen]-1-addcol]) i ]
]
) [flatten(st), faces];
/*
Recursively triangulate between two 3D paths (which can be different
in length by any amount), starting at index 0 and generate a list of
triangles with minimum new-side length.
The first side of the first triangle always connects the two first
vertices of each path.
To triangulate between two closed paths, the first and last vertices
must be the same.
Parameters:
p1 = first path, an array of [x,y,z] vertices
p2 = second path, an array of [x,y,z] vertices
i1offset = index offset of first vertex in the first path
(sum of any prior path lengths)
i2offset = index offset of first vertex in the second path
(sum of any prior path lengths)
n1 = number of vertices in first path
n2 = number of vertices in second path
reverse = if true, assume a polygon path goes around
counterclockwise with respect to the direction from
p1 to p2 (right hand rule), clockwise if false
Other parameters are for internal use:
trilist[] = array of triangles to return
i1 = vertex index on p1 of the next triangle
i2 = vertex index on p2 of the next triangle
tricount1 = number extra triangles generated on vertex i2 to row p1
tricount2 = number extra triangles generated on vertex i1 to row p2
trimax = max number of extra triangles that can be created on any point in a row
(next triangle vertex found can be on either p1 or p2, depending
on which triangle is smaller.)
Returns an array of triangles using vertex indices offset by
i1offset and i2offset
*/
function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], i1=0, i2=0, tricount1=0, tricount2=0, trimax=INF) = n1!=n2 ?
// unequal row lengths
let(
t1 = i1 < n1 ? i1+1 : n1, // test point 1
t2 = i2 < n2 ? i2+1 : n2, // test point 2
//dum=echo(str("i1=",i1," i2=",i2," t1=",t1," t2=",t2," n1=",n1," n2=",n2, " p1[t1]=",p1[t1]," p2[i2]=",p2[i2])),
d12 = t2>=n2 ? 9e+9 : norm(p2[t2]-p1[i1]), // distance from i1 to t2
d21 = t1>=n1 ? 9e+9 : norm(p1[t1]-p2[i2]), // distance from i2 to t1
//dum2=echo(str(" d12=",d12," d21=",d21," tricounts=",tricount1,",",tricount2)),
userow = d12<d21 ? (tricount1<trimax ? 2 : 1) : (tricount2<trimax ? 1 : 2),
newt = userow==1 ? (t1<n1?t1:i1) : (t2<n2?t2:i2),
newofft = userow==2 ? i2offset+newt : i1offset+newt,
tc1 = d12<d21 && tricount1<trimax ? tricount1+1 : 0,
tc2 = d21<d12 && tricount2<trimax ? tricount2+1 : 0,
triangle = reverse ?
[i1offset+i1, i2offset+i2, newofft] :
[i2offset+i2, i1offset+i1, newofft]
) t1>=n1 && t2>=n2 ? trilist :
_lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse, concat(trilist, [triangle]),
userow==1 ? (t1>=n1?i1:t1) : i1, userow==2 ? (t2>=n2?i2:t2) : i2, tc1, tc2, trimax)
: // equal row lengths
let(n=n1, i=i1,
t = i < n ? i+1 : n, // test point
d12 = t>=n ? 9e+9 : norm(p2[t]-p1[i]), // distance from p1 to new p2
d21 = t>=n ? 9e+9 : norm(p1[t]-p2[i]), // distance from p2 to new p1
triangle1 = reverse ?
[i1offset+i, i2offset+i, d12<d21 ? i2offset+t : i1offset+t] :
[i2offset+i, i1offset+i, d12<d21 ? i2offset+t : i1offset+t],
triangle2 = reverse ?
[i2offset+t, i1offset+t, d12<d21 ? i1offset+i : i2offset+i] :
[i1offset+t, i2offset+t, d12<d21 ? i1offset+i : i2offset+i]
) t>=n ? trilist :
_lofttri(p1, p2, i1offset, i2offset, n, n, reverse, concat(trilist, [triangle1, triangle2]), t, t, 0,0,trimax);
// Function: vnf_join()
// Synopsis: Returns a single VNF structure from a list of VNF structures.
// SynTags: VNF
// Topics: VNF Generators, Lists
// See Also: vnf_tri_array(), vnf_vertex_array(), vnf_from_polygons(), vnf_from_region()
// Usage:
// vnf = vnf_join([VNF, VNF, VNF, ...]);
// Description:
// Given a list of VNF structures, merges them all into a single VNF structure.
// Combines all the points of the input VNFs and labels the faces appropriately.
// All the points in the input VNFs appear in the output, even if they are
// duplicated. It is valid to repeat points in a VNF, but if you
// with to remove the duplicates that occur along joined edges, use {{vnf_merge_points()}}.
// .
// This is a tool for manipulating polyhedron data. It is for
// building up a full polyhedron from partial polyhedra.
// It is *not* a union operator for VNFs. The VNFs to be joined must not intersect each other,
// except at edges, otherwise the result is an invalid polyhedron. Also, the
// result must not have any other illegal polyhedron characteristics, such as creating
// more than two faces sharing the same edge.
// If you want a valid result it is your responsibility to ensure that the polyhedron
// has no holes, no intersecting faces or edges, and obeys all the requirements
// that CGAL expects.
// .
// For example, if you combine two pyramids to try to make an octahedron, the result is
// invalid because of the two internal faces created by the pyramid bases. A valid
// use would be to build a cube missing one face and a pyramid missing its base and
// then join them into a cube with a point.
// Arguments:
// vnfs = a list of the VNFs to joint into one VNF.
// Example(3D,VPR=[60,0,26],VPD=55,VPT=[5.6,-5.3,9.8]): Here is a VNF where the top face is missing. It is not a valid polyhedron like this, but we can use it as a building block to make a polyhedron.
// bottom = vnf_vertex_array([path3d(rect(8)), path3d(rect(5),4)],col_wrap=true,cap1=true);
// vnf_polyhedron(bottom);
// Example(3D,VPR=[60,0,26],VPD=55,VPT=[5.6,-5.3,9.8]): Here is a VNF that also has a missing face.
// triangle = yrot(-90,path3d(regular_ngon(n=3,side=5,anchor=LEFT)));
// top = up(4,vnf_vertex_array([list_set(right(2.5,triangle),0,[0,0,7]),
// right(6,triangle)
// ], col_wrap=true, cap2=true));
// vnf_polyhedron(zrot(90,top));
// Example(3D,VPR=[60,0,26],VPD=55,VPT=[5.6,-5.3,9.8]): Using vnf_join combines the two VNFs into a single VNF. Note that they share an edge. But the result still isn't closed, so it is not yet a valid polyhedron.
// bottom = vnf_vertex_array([path3d(rect(8)), path3d(rect(5),4)],col_wrap=true,cap1=true);
// triangle = yrot(-90,path3d(regular_ngon(n=3,side=5,anchor=LEFT)));
// top = up(4,vnf_vertex_array([list_set(right(2.5,triangle),0,[0,0,7]),
// right(6,triangle)
// ], col_wrap=true, cap2=true));
// full = vnf_join([bottom,zrot(90,top)]);
// vnf_polyhedron(full);
// Example(3D,VPR=[60,0,26],VPD=55,VPT=[5.6,-5.3,9.8]): If we add enough pieces, and the pieces are all consistent with each other, then we can arrive at a valid polyhedron like this one. To be valid you need to meet all the CGAL requirements: every edge has exactly two faces, all faces are in clockwise order, no intersections of edges.
// bottom = vnf_vertex_array([path3d(rect(8)), path3d(rect(5),4)],col_wrap=true,cap1=true);
// triangle = yrot(-90,path3d(regular_ngon(n=3,side=5,anchor=LEFT)));
// top = up(4,vnf_vertex_array([list_set(right(2.5,triangle),0,[0,0,7]),
// right(6,triangle)
// ], col_wrap=true, cap2=true));
// full = vnf_join([bottom,
// for(theta=[0:90:359]) zrot(theta,top)
// ]);
// vnf_polyhedron(full);
// Example(3D): The vnf_join function is not a union operator for polyhedra. If any faces intersect, like they do in this example where we combine the faces of two cubes, the result is invalid and results in CGAL errors when you add more objects into the model.
// cube1 = cube(5);
// cube2 = move([2,2,2],cube1);
// badvnf = vnf_join([cube1,cube2]);
// vnf_polyhedron(badvnf);
// right(2.5)up(3)color("red")
// text3d("Invalid",size=1,anchor=CENTER,
// orient=FRONT,h=.1);
function vnf_join(vnfs) =
assert(is_vnf_list(vnfs) , "\nInput must be a list of VNFs.")
len(vnfs)==1 ? vnfs[0]
:
let (
offs = cumsum([ 0, for (vnf = vnfs) len(vnf[0]) ]),
verts = [for (vnf=vnfs) each vnf[0]],
faces =
[ for (i = idx(vnfs))
let( faces = vnfs[i][1] )
for (face = faces)
if ( len(face) >= 3 )
[ for (j = face)
assert( j>=0 && j<len(vnfs[i][0]),
str("\nVNF number ", i, " has a face indexing an nonexistent vertex.") )
offs[i] + j ]
]
)
[verts,faces];
// Function: vnf_from_polygons()
// Synopsis: Returns a VNF from a list of 3D polygons.
// SynTags: VNF
// Topics: VNF Generators, Lists
// See Also: vnf_tri_array(), vnf_join(), vnf_vertex_array(), vnf_from_region()
// Usage:
// vnf = vnf_from_polygons(polygons, [eps]);
// Description:
// Given a list of 3D polygons, produces a VNF containing those polygons.
// It is up to the caller to make sure that the points are in the correct order to make the face
// normals point outward. No checking for duplicate vertices is done. If you want to
// remove duplicate vertices use {{vnf_merge_points()}}. Polygons with zero area are discarded from the face list by default.
// If you give non-coplanar faces an error is displayed. These checks increase run time by about 2x for triangular polygons, but
// about 10x for pentagons; the checks can be disabled by setting fast=true.
// Arguments:
// polygons = The list of 3D polygons to turn into a VNF
// fast = Set to true to skip area and coplanarity checks for increased speed. Default: false
// eps = Polygons with area smaller than this are discarded. Default: 1e-9
// Example(3D,VPR=[60,0,40]): Construction of a dodecahedron from pentagon faces.
// dihedral = 2*atan(PHI); // dodecahedron face dihedral
// rpenta = 10; // pentagon face radius
// edge = 2*rpenta*sin(36); // edge length
// inrad = 0.5*edge * PHI*PHI/sqrt(3-PHI); // inner radius
// face3d = path3d(pentagon(rpenta), inrad); // single face
// facepoints = [
// face3d,
// for(a=[36:72:360]) zrot(a, yrot(180-dihedral, face3d)),
// for(a=[36:72:360]) zrot(a, yrot(360-dihedral, face3d)),
// yrot(180, face3d)
// ];
// vnf = vnf_from_polygons(facepoints, fast=true);
// vnf_polyhedron(vnf);
function vnf_from_polygons(polygons,fast=false,eps=_EPSILON) =
assert(is_list(polygons) && is_path(polygons[0]),"\nInput should be a list of polygons.")
let(
offs = cumsum([0, for(p=polygons) len(p)]),
faces = [for(i=idx(polygons))
let(
area=fast ? 1 : polygon_area(polygons[i]),
dummy=assert(is_def(area) || is_collinear(polygons[i],eps=eps),str("\nPolygon ", i, " is not coplanar."))
)
if (is_def(area) && area > eps)
[for (j=idx(polygons[i])) offs[i]+j]
]
)
[flatten(polygons), faces];
function _path_path_closest_vertices(path1,path2) =
let(
dists = [for (i=idx(path1)) let(j=closest_point(path1[i],path2)) [j,norm(path2[j]-path1[i])]],
i1 = min_index(column(dists,1)),
i2 = dists[i1][0]
) [dists[i1][1], i1, i2];
function _join_paths_at_vertices(path1,path2,v1,v2) =
let(
repeat_start = !approx(path1[v1],path2[v2]),
path1 = clockwise_polygon(list_rotate(path1,v1)),
path2 = ccw_polygon(list_rotate(path2,v2))
)
[
each path1,
if (repeat_start) path1[0],
each path2,
if (repeat_start) path2[0],
];
/// Internal Function: _cleave_connected_region(region, eps)
/// Description:
/// Given a region that is connected and has its outer border in region[0],
/// produces a overlapping connected path to join internal holes to
/// the outer border without adding points. Output is a single non-simple polygon.
/// Requirements:
/// It expects that all region paths be simple closed paths, with region[0] CW and
/// the other paths CCW and encircled by region[0]. The input region paths are also
/// supposed to be disjoint except for common vertices and common edges but with
/// no crossings. It may return `undef` if these conditions are not met.
/// This function implements an extension of the algorithm discussed in:
/// https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
function _cleave_connected_region(region, eps=_EPSILON) =
len(region)==1 ? region[0] :
let(
outer = deduplicate(region[0]), //
holes = [for(i=[1:1:len(region)-1]) // deduplication possibly unneeded
deduplicate( region[i] ) ], //
extridx = [for(li=holes) max_index(column(li,0)) ],
// the right extreme vertex for each hole sorted by decreasing x values
extremes = sort( [for(i=idx(holes)) [ i, extridx[i], -holes[i][extridx[i]].x] ], idx=2 )
)
_polyHoles(outer, holes, extremes, eps, 0);
// connect the hole paths one at a time to the outer path.
// 'extremes' is the list of the right extreme vertex of each hole sorted by decreasing abscissas
// see: _cleave_connected_region(region, eps)
function _polyHoles(outer, holes, extremes, eps=_EPSILON, n=0) =
let(
extr = extremes[n], //
hole = holes[extr[0]], // hole path to bridge to the outer path
ipt = extr[1], // index of the hole point with maximum abscissa
brdg = _bridge(hole[ipt], outer, eps) // the index of a point in outer to bridge hole[ipt] to
)
brdg == undef ? undef :
let(
l = len(outer),
lh = len(hole),
// the new outer polygon bridging the hole to the old outer
npoly =
approx(outer[brdg], hole[ipt], eps)
? [ for(i=[brdg: 1: brdg+l]) outer[i%l] ,
for(i=[ipt+1: 1: ipt+lh-1]) hole[i%lh] ]
: [ for(i=[brdg: 1: brdg+l]) outer[i%l] ,
for(i=[ipt: 1: ipt+lh]) hole[i%lh] ]
)
n==len(holes)-1 ? npoly :
_polyHoles(npoly, holes, extremes, eps, n+1);
// find a point in outer to be connected to pt in the interior of outer
// by a segment that not cross or touch any non adjacente edge of outer.
// return the index of a vertex in the outer path where the bridge should end
// see _polyHoles(outer, holes, extremes, eps)
function _bridge(pt, outer,eps) =
// find the intersection of a ray from pt to the right
// with the boundary of the outer cycle
let(
l = len(outer),
crxs =
let( edges = pair(outer,wrap=true) )
[for( i = idx(edges) )
let( edge = edges[i] )
// consider just descending outer edges at right of pt crossing ordinate pt.y
if( (edge[0].y > pt.y) //+eps)
&& (edge[1].y <= pt.y)
&& _is_at_left(pt, [edge[1], edge[0]], eps) )
[ i,
// the point of edge with ordinate pt.y
abs(pt.y-edge[1].y)<eps ? edge[1] :
let( u = (pt-edge[1]).y / (edge[0]-edge[1]).y )
(1-u)*edge[1] + u*edge[0]
]
]
)
crxs == [] ? undef :
let(
// the intersection point of the nearest edge to pt with minimum slope
minX = min([for(p=crxs) p[1].x]),
crxcand = [for(crx=crxs) if(crx[1].x < minX+eps) crx ], // nearest edges
nearest = min_index([for(crx=crxcand)
(outer[crx[0]].x - pt.x) / (outer[crx[0]].y - pt.y) ]), // minimum slope
proj = crxcand[nearest],
vert0 = outer[proj[0]], // the two vertices of the nearest crossing edge
vert1 = outer[(proj[0]+1)%l],
isect = proj[1] // the intersection point
)
norm(pt-vert1) < eps ? (proj[0]+1)%l : // if pt touches an outer vertex, return its index
// as vert0.y > pt.y then pt!=vert0
norm(pt-isect) < eps ? undef : // if pt touches the middle of an outer edge -> error
let(
// the edge [vert0, vert1] necessarily satisfies vert0.y > vert1.y
// indices of candidates to an outer bridge point
cand =
(vert0.x > pt.x)
? [ proj[0],
// select reflex vertices inside of the triangle [pt, vert0, isect]
for(i=idx(outer))
if( _tri_class(select(outer,i-1,i+1),eps) <= 0
&& _pt_in_tri(outer[i], [pt, vert0, isect], eps)>=0 )
i
]
: [ (proj[0]+1)%l,
// select reflex vertices inside of the triangle [pt, isect, vert1]
for(i=idx(outer))
if( _tri_class(select(outer,i-1,i+1),eps) <= 0
&& _pt_in_tri(outer[i], [pt, isect, vert1], eps)>=0 )
i
],
// choose the candidate outer[i] such that the line [pt, outer[i]] has minimum slope
// among those with minimum slope choose the nearest to pt
slopes = [for(i=cand) 1-abs(outer[i].x-pt.x)/norm(outer[i]-pt) ],
min_slp = min(slopes),
cand2 = [for(i=idx(cand)) if(slopes[i]<=min_slp+eps) cand[i] ],
nearest = min_index([for(i=cand2) norm(pt-outer[i]) ])
)
cand2[nearest];
// Function: vnf_from_region()
// Synopsis: Returns a 3D VNF given a 2D region.
// SynTags: VNF
// Topics: VNF Generators, Lists
// See Also: vnf_vertex_array(), vnf_tri_array(), vnf_join(), vnf_from_polygons()
// Usage:
// vnf = vnf_from_region(region, [transform], [reverse]);
// Description:
// Given a (two-dimensional) region, applies the given transformation matrix to it and makes a (three-dimensional) triangulated VNF of
// faces for that region, reversed if desired.
// Arguments:
// region = The region to convert to a VNF.
// transform = If given, a transformation matrix to apply to the faces generated from the region. Default: No transformation applied.
// reverse = If true, reverse the normals of the faces generated from the region. An untransformed region has face normals pointing `UP`. Default: false
// Example(3D):
// region = [square([20,10],center=true),
// right(5,square(4,center=true)),
// left(5,square(6,center=true))];
// vnf = vnf_from_region(region);
// color("gray")down(.125)