1//////////////////////////////////////////////////////////////////////
2// LibFile: skin.scad
3// This file provides functions and modules that construct shapes from a list of cross sections.
4// In the case of skin() you specify each cross sectional shape yourself, and the number of
5// points can vary. The various forms of sweep use a fixed shape, which may follow a path, or
6// be transformed in other ways to produce the list of cross sections. In all cases it is the
7// user's responsibility to avoid creating a self-intersecting shape, which will produce
8// cryptic CGAL errors. This file was inspired by list-comprehension-demos skin():
9// - https://github.com/openscad/list-comprehension-demos/blob/master/skin.scad
10// Includes:
11// include <BOSL2/std.scad>
12// FileGroup: Advanced Modeling
13// FileSummary: Construct 3D shapes from 2D cross sections of the desired shape.
14// FileFootnotes: STD=Included in std.scad
15//////////////////////////////////////////////////////////////////////
16
17
18// Section: Skin and sweep
19
20// Function&Module: skin()
21// Synopsis: Connect a sequence of arbitrary polygons into a 3D object.
22// SynTags: VNF, Geom
23// Topics: Extrusion, Skin
24// See Also: sweep(), linear_sweep(), rotate_sweep(), spiral_sweep(), path_sweep(), offset_sweep()
25// Usage: As module:
26// skin(profiles, slices, [z=], [refine=], [method=], [sampling=], [caps=], [closed=], [style=], [convexity=], [anchor=],[cp=],[spin=],[orient=],[atype=]) [ATTACHMENTS];
27// Usage: As function:
28// vnf = skin(profiles, slices, [z=], [refine=], [method=], [sampling=], [caps=], [closed=], [style=], [anchor=],[cp=],[spin=],[orient=],[atype=]);
29// Description:
30// Given a list of two or more path `profiles` in 3d space, produces faces to skin a surface between
31// the profiles. Optionally the first and last profiles can have endcaps, or the first and last profiles
32// can be connected together. Each profile should be roughly planar, but some variation is allowed.
33// Each profile must rotate in the same clockwise direction. If called as a function, returns a
34// [VNF structure](vnf.scad) `[VERTICES, FACES]`. If called as a module, creates a polyhedron
35// of the skinned profiles.
36// .
37// The profiles can be specified either as a list of 3d curves or they can be specified as
38// 2d curves with heights given in the `z` parameter. It is your responsibility to ensure
39// that the resulting polyhedron is free from self-intersections, which would make it invalid
40// and can result in cryptic CGAL errors upon rendering with a second object present, even though the polyhedron appears
41// OK during preview or when rendered by itself.
42// .
43// For this operation to be well-defined, the profiles must all have the same vertex count and
44// we must assume that profiles are aligned so that vertex `i` links to vertex `i` on all polygons.
45// Many interesting cases do not comply with this restriction. Two basic methods can handle
46// these cases: either subdivide edges (insert additional points along edges)
47// or duplicate vertcies (insert edges of length 0) so that both polygons have
48// the same number of points.
49// Duplicating vertices allows two distinct points in one polygon to connect to a single point
50// in the other one, creating
51// triangular faces. You can adjust non-matching polygons yourself
52// either by resampling them using {{subdivide_path()}} or by duplicating vertices using
53// `repeat_entries`. It is OK to pass a polygon that has the same vertex repeated, such as
54// a square with 5 points (two of which are identical), so that it can match up to a pentagon.
55// Such a combination would create a triangular face at the location of the duplicated vertex.
56// Alternatively, `skin` provides methods (described below) for inserting additional vertices
57// automatically to make incompatible paths match.
58// .
59// In order for skinned surfaces to look good it is usually necessary to use a fine sampling of
60// points on all of the profiles, and a large number of extra interpolated slices between the
61// profiles that you specify. It is generally best if the triangles forming your polyhedron
62// are approximately equilateral. The `slices` parameter specifies the number of slices to insert
63// between each pair of profiles, either a scalar to insert the same number everywhere, or a vector
64// to insert a different number between each pair.
65// .
66// Resampling may occur, depending on the `method` parameter, to make profiles compatible.
67// To force (possibly additional) resampling of the profiles to increase the point density you can set `refine=N`, which
68// will multiply the number of points on your profile by `N`. You can choose between two resampling
69// schemes using the `sampling` option, which you can set to `"length"` or `"segment"`.
70// The length resampling method resamples proportional to length.
71// The segment method divides each segment of a profile into the same number of points.
72// This means that if you refine a profile with the "segment" method you will get N points
73// on each edge, but if you refine a profile with the "length" method you will get new points
74// distributed around the profile based on length, so small segments will get fewer new points than longer ones.
75// A uniform division may be impossible, in which case the code computes an approximation, which may result
76// in arbitrary distribution of extra points. See {{subdivide_path()}} for more details.
77// Note that when dealing with continuous curves it is always better to adjust the
78// sampling in your code to generate the desired sampling rather than using the `refine` argument.
79// .
80// You can choose from five methods for specifying alignment for incommensurate profiles.
81// The available methods are `"distance"`, `"fast_distance"`, `"tangent"`, `"direct"` and `"reindex"`.
82// It is useful to distinguish between continuous curves like a circle and discrete profiles
83// like a hexagon or star, because the algorithms' suitability depend on this distinction.
84// .
85// The default method for aligning profiles is `method="direct"`.
86// If you simply supply a list of compatible profiles it will link them up
87// exactly as you have provided them. You may find that profiles you want to connect define the
88// right shapes but the point lists don't start from points that you want aligned in your skinned
89// polyhedron. You can correct this yourself using `reindex_polygon`, or you can use the "reindex"
90// method which will look for the index choice that will minimize the length of all of the edges
91// in the polyhedron—it will produce the least twisted possible result. This algorithm has quadratic
92// run time so it can be slow with very large profiles.
93// .
94// When the profiles are incommensurate, the "direct" and "reindex" resample them to match. As noted above,
95// for continuous input curves, it is better to generate your curves directly at the desired sample size,
96// but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled
97// to match the circle. When you use "direct" or "reindex" the default `sampling` value is
98// of `sampling="length"` to approximate a uniform length sampling of the profile. This will generally
99// produce the natural result for connecting two continuously sampled profiles or a continuous
100// profile and a polygonal one. However depending on your particular case,
101// `sampling="segment"` may produce a more pleasing result. These two approaches differ only when
102// the segments of your input profiles have unequal length.
103// .
104// The "distance", "fast_distance" and "tangent" methods work by duplicating vertices to create
105// triangular faces. In the skined object created by two polygons, every vertex of a polygon must
106// have an edge that connects to some vertex on the other one. If you connect two squares this can be
107// accomplished with four edges, but if you want to connect a square to a pentagon you must add a
108// fifth edge for the "extra" vertex on the pentagon. You must now decide which vertex on the square to
109// connect the "extra" edge to. How do you decide where to put that fifth edge? The "distance" method answers this
110// question by using an optimization: it minimizes the total length of all the edges connecting
111// the two polygons. This algorithm generally produces a good result when both profiles are discrete ones with
112// a small number of vertices. It is computationally intensive (O(N^3)) and may be
113// slow on large inputs. The resulting surfaces generally have curved faces, so be
114// sure to select a sufficiently large value for `slices` and `refine`. Note that for
115// this method, `sampling` must be set to `"segment"`, and hence this is the default setting.
116// Using sampling by length would ignore the repeated vertices and ruin the alignment.
117// The "fast_distance" method restricts the optimization by assuming that an edge should connect
118// vertex 0 of the two polygons. This reduces the run time to O(N^2) and makes
119// the method usable on profiles with more points if you take care to index the inputs to match.
120// .
121// The `"tangent"` method generally produces good results when
122// connecting a discrete polygon to a convex, finely sampled curve. Given a polygon and a curve, consider one edge
123// on the polygon. Find a plane passing through the edge that is tangent to the curve. The endpoints of the edge and
124// the point of tangency define a triangular face in the output polyhedron. If you work your way around the polygon
125// edges, you can establish a series of triangular faces in this way, with edges linking the polygon to the curve.
126// You can then complete the edge assignment by connecting all the edges in between the triangular faces together,
127// with many edges meeting at each polygon vertex. The result is an alternation of flat triangular faces with conical
128// curves joining them. Another way to think about it is that it splits the points on the curve up into groups and
129// connects all the points in one group to the same vertex on the polygon.
130// .
131// The "tangent" method may fail if the curved profile is non-convex, or doesn't have enough points to distinguish
132// all of the tangent points from each other. The algorithm treats whichever input profile has fewer points as the polygon
133// and the other one as the curve. Using `refine` with this method will have little effect on the model, so
134// you should do it only for agreement with other profiles, and these models are linear, so extra slices also
135// have no effect. For best efficiency set `refine=1` and `slices=0`. As with the "distance" method, refinement
136// must be done using the "segment" sampling scheme to preserve alignment across duplicated points.
137// Note that the "tangent" method produces similar results to the "distance" method on curved inputs. If this
138// method fails due to concavity, "fast_distance" may be a good option.
139// .
140// It is possible to specify `method` and `refine` as arrays, but it is important to observe
141// matching rules when you do this. If a pair of profiles is connected using "tangent" or "distance"
142// then the `refine` values for those two profiles must be equal. If a profile is connected by
143// a vertex duplicating method on one side and a resampling method on the other side, then
144// `refine` must be set so that the resulting number of vertices matches the number that is
145// used for the resampled profiles. The best way to avoid confusion is to ensure that the
146// profiles connected by "direct" or "reindex" all have the same number of points and at the
147// transition, the refined number of points matches.
148// .
149// Arguments:
150// profiles = list of 2d or 3d profiles to be skinned. (If 2d must also give `z`.)
151// 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.
152// ---
153// 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.
154// 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".
155// closed = set to true to connect first and last profile (to make a torus). Default: false
156// caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false.
157// method = method for connecting profiles, one of "distance", "fast_distance", "tangent", "direct" or "reindex". Default: "direct".
158// z = array of height values for each profile if the profiles are 2d
159// convexity = convexity setting for use with polyhedron. (module only) Default: 10
160// anchor = Translate so anchor point is at the origin. Default: "origin"
161// spin = Rotate this many degrees around Z axis after anchor. Default: 0
162// orient = Vector to rotate top towards after spin
163// atype = Select "hull" or "intersect" anchor types. Default: "hull"
164// 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"
165// style = vnf_vertex_array style. Default: "min_edge"
166// Anchor Types:
167// "hull" = Anchors to the virtual convex hull of the shape.
168// "intersect" = Anchors to the surface of the shape.
169// Example:
170// skin([octagon(4), circle($fn=70,r=2)], z=[0,3], slices=10);
171// Example: Rotating the pentagon place the zero index at different locations, giving a twist
172// skin([rot(90,p=pentagon(4)), circle($fn=80,r=2)], z=[0,3], slices=10);
173// Example: You can untwist it with the "reindex" method
174// skin([rot(90,p=pentagon(4)), circle($fn=80,r=2)], z=[0,3], slices=10, method="reindex");
175// Example: Offsetting the starting edge connects to circles in an interesting way:
176// circ = circle($fn=80, r=3);
177// skin([circ, rot(110,p=circ)], z=[0,5], slices=20);
178// Example(FlatSpin,VPD=20):
179// skin([ yrot(37,p=path3d(circle($fn=128, r=4))), path3d(square(3),3)], method="reindex",slices=10);
180// Example(FlatSpin,VPD=16): Ellipses connected with twist
181// ellipse = xscale(2.5,p=circle($fn=80));
182// skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10);
183// Example(FlatSpin,VPD=16): Ellipses connected without a twist. (Note ellipses stay in the same position: just the connecting edges are different.)
184// ellipse = xscale(2.5,p=circle($fn=80));
185// skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10, method="reindex");
186// Example(FlatSpin,VPD=500):
187// $fn=24;
188// skin([
189// yrot(0, p=yscale(2,p=path3d(circle(d=75)))),
190// [[40,0,100], [35,-15,100], [20,-30,100],[0,-40,100],[-40,0,100],[0,40,100],[20,30,100], [35,15,100]]
191// ],slices=10);
192// Example(FlatSpin,VPD=600):
193// $fn=48;
194// skin([
195// for (b=[0,90]) [
196// for (a=[360:-360/$fn:0.01])
197// point3d(polar_to_xy((100+50*cos((a+b)*2))/2,a),b/90*100)
198// ]
199// ], slices=20);
200// Example: Vaccum connector example from list-comprehension-demos
201// include <BOSL2/rounding.scad>
202// $fn=32;
203// base = round_corners(square([2,4],center=true), radius=0.5);
204// skin([
205// path3d(base,0),
206// path3d(base,2),
207// path3d(circle(r=0.5),3),
208// path3d(circle(r=0.5),4),
209// for(i=[0:2]) each [path3d(circle(r=0.6), i+4),
210// path3d(circle(r=0.5), i+5)]
211// ],slices=0);
212// Example: Vaccum nozzle example from list-comprehension-demos, using "length" sampling (the default)
213// xrot(90)down(1.5)
214// difference() {
215// skin(
216// [square([2,.2],center=true),
217// circle($fn=64,r=0.5)], z=[0,3],
218// slices=40,sampling="length",method="reindex");
219// skin(
220// [square([1.9,.1],center=true),
221// circle($fn=64,r=0.45)], z=[-.01,3.01],
222// slices=40,sampling="length",method="reindex");
223// }
224// Example: Same thing with "segment" sampling
225// xrot(90)down(1.5)
226// difference() {
227// skin(
228// [square([2,.2],center=true),
229// circle($fn=64,r=0.5)], z=[0,3],
230// slices=40,sampling="segment",method="reindex");
231// skin(
232// [square([1.9,.1],center=true),
233// circle($fn=64,r=0.45)], z=[-.01,3.01],
234// slices=40,sampling="segment",method="reindex");
235// }
236// Example: Forma Candle Holder (from list-comprehension-demos)
237// r = 50;
238// height = 140;
239// layers = 10;
240// wallthickness = 5;
241// holeradius = r - wallthickness;
242// difference() {
243// skin([for (i=[0:layers-1]) zrot(-30*i,p=path3d(hexagon(ir=r),i*height/layers))],slices=0);
244// up(height/layers) cylinder(r=holeradius, h=height);
245// }
246// Example(FlatSpin,VPD=300): A box that is octagonal on the outside and circular on the inside
247// height = 45;
248// sub_base = octagon(d=71, rounding=2, $fn=128);
249// base = octagon(d=75, rounding=2, $fn=128);
250// interior = regular_ngon(n=len(base), d=60);
251// right_half()
252// skin([ sub_base, base, base, sub_base, interior], z=[0,2,height, height, 2], slices=0, refine=1, method="reindex");
253// Example: Connecting a pentagon and circle with the "tangent" method produces large triangular faces and cone shaped corners.
254// skin([pentagon(4), circle($fn=80,r=2)], z=[0,3], slices=10, method="tangent");
255// 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.
256// 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))))];
257// rotate(180) skin( shapes, slices=0);
258// Example: Here's a simplified version of the above, with `i=0` included. That first layer doesn't look good.
259// shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)];
260// skin(shapes, slices=0);
261// Example: You can fix it by specifying "tangent" for the first method, but you still need "direct" for the rest.
262// shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)];
263// skin(shapes, slices=0, method=concat(["tangent"],repeat("direct",len(shapes)-2)));
264// Example(FlatSpin,VPD=35): Connecting square to pentagon using "direct" method.
265// skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10);
266// Example(FlatSpin,VPD=35): Connecting square to shifted pentagon using "direct" method.
267// skin([regular_ngon(n=4, r=4), right(4,p=regular_ngon(n=5,r=5))], z=[0,4], refine=10, slices=10);
268// 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
269// ellipse = yscale(3,circle(r=10, $fn=32));
270// tri = move([-50/3,-9],[[0,0], [50,0], [0,27]]);
271// skin([ellipse, reverse(tri)], z=[0,20], slices=20, method="reindex");
272// 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.
273// ellipse = yscale(3,circle(r=10, $fn=32));
274// tri = move([-50/3,-9],
275// subdivide_path([[0,0], [50,0], [0,27]], 32));
276// aligned = align_polygon(ellipse,tri, [0:5:180]);
277// skin([ellipse, aligned], z=[0,20], slices=20);
278// Example(FlatSpin,VPD=35): The "distance" method is a completely different approach.
279// skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10, method="distance");
280// Example(FlatSpin,VPD=35,VPT=[0,0,4]): Connecting pentagon to heptagon inserts two triangular faces on each side
281// small = path3d(circle(r=3, $fn=5));
282// big = up(2,p=yrot( 0,p=path3d(circle(r=3, $fn=7), 6)));
283// skin([small,big],method="distance", slices=10, refine=10);
284// Example(FlatSpin,VPD=35,VPT=[0,0,4]): But just a slight rotation of the top profile moves the two triangles to one end
285// small = path3d(circle(r=3, $fn=5));
286// big = up(2,p=yrot(14,p=path3d(circle(r=3, $fn=7), 6)));
287// skin([small,big],method="distance", slices=10, refine=10);
288// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Another "distance" example:
289// off = [0,2];
290// shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]);
291// rshape = rot(180,cp=centroid(shape)+off, p=shape);
292// skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15);
293// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Slightly shifting the profile changes the optimal linkage
294// off = [0,1];
295// shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]);
296// rshape = rot(180,cp=centroid(shape)+off, p=shape);
297// skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15);
298// Example(FlatSpin,VPD=444,VPT=[0,0,50]): This optimal solution doesn't look terrible:
299// prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]);
300// prof2 = path3d(regular_ngon(n=7, r=50),100);
301// skin([prof1, prof2], method="distance", slices=10, refine=10);
302// 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.
303// prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]);
304// prof2 = path3d(regular_ngon(n=7, r=50),100);
305// skin([repeat_entries(prof1,[2,2,1,1,1,1,1]),
306// prof2],
307// method="distance", slices=10, refine=10);
308// 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:
309// skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))], method="distance", slices=0);
310// Example(FlatSpin,VPD=80,VPT=[0,0,7]): Using the "tangent" method produces:
311// skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))], method="tangent", slices=0);
312// Example(FlatSpin,VPD=74): Torus using hexagons and pentagons, where `closed=true`
313// hex = right(7,p=path3d(hexagon(r=3)));
314// pent = right(7,p=path3d(pentagon(r=3)));
315// N=5;
316// skin(
317// [for(i=[0:2*N-1]) yrot(360*i/2/N, p=(i%2==0 ? hex : pent))],
318// refine=1,slices=0,method="distance",closed=true);
319// Example: A smooth morph is achieved when you can calculate all the slices yourself. Since you provide all the slices, set `slices=0`.
320// skin([for(n=[.1:.02:.5])
321// yrot(n*60-.5*60,p=path3d(supershape(step=360/128,m1=5,n1=n, n2=1.7),5-10*n))],
322// slices=0);
323// Example: Another smooth supershape morph:
324// skin([for(alpha=[-.2:.05:1.5])
325// path3d(supershape(step=360/256,m1=7, n1=lerp(2,3,alpha),
326// n2=lerp(8,4,alpha), n3=lerp(4,17,alpha)),alpha*5)],
327// slices=0);
328// Example: Several polygons connected using "distance"
329// skin([regular_ngon(n=4, r=3),
330// regular_ngon(n=6, r=3),
331// regular_ngon(n=9, r=4),
332// rot(17,p=regular_ngon(n=6, r=3)),
333// rot(37,p=regular_ngon(n=4, r=3))],
334// z=[0,2,4,6,9], method="distance", slices=10, refine=10);
335// Example(FlatSpin,VPD=935,VPT=[75,0,123]): Vertex count of the polygon changes at every profile
336// skin([
337// for (ang = [0:10:90])
338// rot([0,ang,0], cp=[200,0,0], p=path3d(circle(d=100,$fn=12-(ang/10))))
339// ],method="distance",slices=10,refine=10);
340// 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.
341// skin([
342// for (ang = [0:5:360])
343// rot([0,ang,0], cp=[100,0,0], p=rot(ang/2, p=path3d(square([1,30],center=true))))
344// ], caps=false, slices=0, refine=20);
345// Example: This model of two scutoids packed together is based on https://www.thingiverse.com/thing:3024272 by mathgrrl
346// sidelen = 10; // Side length of scutoid
347// height = 25; // Height of scutoid
348// angle = -15; // Angle (twists the entire form)
349// push = -5; // Push (translates the base away from the top)
350// flare = 1; // Flare (the two pieces will be different unless this is 1)
351// midpoint = .5; // Height of the extra vertex (as a fraction of total height); the two pieces will be different unless this is .5)
352// pushvec = rot(angle/2,p=push*RIGHT); // Push direction is the average of the top and bottom mating edges
353// pent = path3d(apply(move(pushvec)*rot(angle),pentagon(side=sidelen,align_side=RIGHT,anchor="side0")));
354// hex = path3d(hexagon(side=flare*sidelen, align_side=RIGHT, anchor="side0"),height);
355// pentmate = path3d(pentagon(side=flare*sidelen,align_side=LEFT,anchor="side0"),height);
356// // Native index would require mapping first and last vertices together, which is not allowed, so shift
357// hexmate = list_rotate(
358// path3d(apply(move(pushvec)*rot(angle),hexagon(side=sidelen,align_side=LEFT,anchor="side0"))),
359// -1);
360// join_vertex = lerp(
361// mean(select(hex,1,2)), // midpoint of "extra" hex edge
362// mean(select(hexmate,0,1)), // midpoint of "extra" hexmate edge
363// midpoint);
364// augpent = repeat_entries(pent, [1,2,1,1,1]); // Vertex 1 will split at the top forming a triangular face with the hexagon
365// augpent_mate = repeat_entries(pentmate,[2,1,1,1,1]); // For mating pentagon it is vertex 0 that splits
366// // Middle is the interpolation between top and bottom except for the join vertex, which is doubled because it splits
367// middle = list_set(lerp(augpent,hex,midpoint),[1,2],[join_vertex,join_vertex]);
368// middle_mate = list_set(lerp(hexmate,augpent_mate,midpoint), [0,1], [join_vertex,join_vertex]);
369// skin([augpent,middle,hex], slices=10, refine=10, sampling="segment");
370// color("green")skin([augpent_mate,middle_mate,hexmate], slices=10,refine=10, sampling="segment");
371// 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.
372// skin([
373// for (a = [0:30:180]) let(
374// pos = [-60*sin(a), 0, a ],
375// pos2 = [-60*sin(a+0.1), 0, a+0.1]
376// ) move(pos,
377// p=rot(from=UP, to=pos2-pos,
378// p=path3d(circle(d=150))
379// )
380// )
381// ],refine=1,slices=0);
382// color("red") {
383// zrot(25) fwd(130) xrot(75) {
384// linear_extrude(height=0.1) {
385// ydistribute(25) {
386// text(text="BAD POLYHEDRONS!", size=20, halign="center", valign="center");
387// text(text="CREASES MAKE", size=20, halign="center", valign="center");
388// }
389// }
390// }
391// up(160) zrot(25) fwd(130) xrot(75) {
392// stroke(zrot(30, p=yscale(0.5, p=circle(d=120))),width=10,closed=true);
393// }
394// }
395module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, style="min_edge", convexity=10,
396 anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull")
397{
398 vnf = skin(profiles, slices, refine, method, sampling, caps, closed, z, style=style);
399 vnf_polyhedron(vnf,convexity=convexity,spin=spin,anchor=anchor,orient=orient,atype=atype,cp=cp)
400 children();
401}
402
403
404function skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, style="min_edge",
405 anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull") =
406 assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"")
407 assert(is_def(slices),"The slices argument must be specified.")
408 assert(is_list(profiles) && len(profiles)>1, "Must provide at least two profiles")
409 let(
410 profiles = [for(p=profiles) if (is_region(p) && len(p)==1) p[0] else p]
411 )
412 let( bad = [for(i=idx(profiles)) if (!(is_path(profiles[i]) && len(profiles[i])>2)) i])
413 assert(len(bad)==0, str("Profiles ",bad," are not a paths or have length less than 3"))
414 let(
415 profcount = len(profiles) - (closed?0:1),
416 legal_methods = ["direct","reindex","distance","fast_distance","tangent"],
417 caps = is_def(caps) ? caps :
418 closed ? false : true,
419 capsOK = is_bool(caps) || is_bool_list(caps,2),
420 fullcaps = is_bool(caps) ? [caps,caps] : caps,
421 refine = is_list(refine) ? refine : repeat(refine, len(profiles)),
422 slices = is_list(slices) ? slices : repeat(slices, profcount),
423 refineOK = [for(i=idx(refine)) if (refine[i]<=0 || !is_integer(refine[i])) i],
424 slicesOK = [for(i=idx(slices)) if (!is_integer(slices[i]) || slices[i]<0) i],
425 maxsize = max_length(profiles),
426 methodok = is_list(method) || in_list(method, legal_methods),
427 methodlistok = is_list(method) ? [for(i=idx(method)) if (!in_list(method[i], legal_methods)) i] : [],
428 method = is_string(method) ? repeat(method, profcount) : method,
429 // Define to be zero where a resampling method is used and 1 where a vertex duplicator is used
430 RESAMPLING = 0,
431 DUPLICATOR = 1,
432 method_type = [for(m = method) m=="direct" || m=="reindex" ? 0 : 1],
433 sampling = is_def(sampling) ? sampling :
434 in_list(DUPLICATOR,method_type) ? "segment" : "length"
435 )
436 assert(len(refine)==len(profiles), "refine list is the wrong length")
437 assert(len(slices)==profcount, str("slices list must have length ",profcount))
438 assert(slicesOK==[],str("slices must be nonnegative integers"))
439 assert(refineOK==[],str("refine must be postive integer"))
440 assert(methodok,str("method must be one of ",legal_methods,". Got ",method))
441 assert(methodlistok==[], str("method list contains invalid method at ",methodlistok))
442 assert(len(method) == profcount,"Method list is the wrong length")
443 assert(in_list(sampling,["length","segment"]), "sampling must be set to \"length\" or \"segment\"")
444 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\"")
445 assert(capsOK, "caps must be boolean or a list of two booleans")
446 assert(!closed || !caps, "Cannot make closed shape with caps")
447 let(
448 profile_dim=list_shape(profiles,2),
449 profiles_zcheck = (profile_dim != 2) || (profile_dim==2 && is_list(z) && len(z)==len(profiles)),
450 profiles_ok = (profile_dim==2 && is_list(z) && len(z)==len(profiles)) || profile_dim==3
451 )
452 assert(profiles_zcheck, "z parameter is invalid or has the wrong length.")
453 assert(profiles_ok,"Profiles must all be 3d or must all be 2d, with matching length z parameter.")
454 assert(is_undef(z) || profile_dim==2, "Do not specify z with 3d profiles")
455 assert(profile_dim==3 || len(z)==len(profiles),"Length of z does not match length of profiles.")
456 let(
457 // Adjoin Z coordinates to 2d profiles
458 profiles = profile_dim==3 ? profiles :
459 [for(i=idx(profiles)) path3d(profiles[i], z[i])],
460 // True length (not counting repeated vertices) of profiles after refinement
461 refined_len = [for(i=idx(profiles)) refine[i]*len(profiles[i])],
462 // Define this to be 1 if a profile is used on either side by a resampling method, zero otherwise.
463 profile_resampled = [for(i=idx(profiles))
464 1-(
465 i==0 ? method_type[0] * (closed? last(method_type) : 1) :
466 i==len(profiles)-1 ? last(method_type) * (closed ? select(method_type,-2) : 1) :
467 method_type[i] * method_type[i-1])],
468 parts = search(1,[1,for(i=[0:1:len(profile_resampled)-2]) profile_resampled[i]!=profile_resampled[i+1] ? 1 : 0],0),
469 plen = [for(i=idx(parts)) (i== len(parts)-1? len(refined_len) : parts[i+1]) - parts[i]],
470 max_list = [for(i=idx(parts)) each repeat(max(select(refined_len, parts[i], parts[i]+plen[i]-1)), plen[i])],
471 transition_profiles = [for(i=[(closed?0:1):1:profcount-1]) if (select(method_type,i-1) != method_type[i]) i],
472 badind = [for(tranprof=transition_profiles) if (refined_len[tranprof] != max_list[tranprof]) tranprof]
473 )
474 assert(badind==[],str("Profile length mismatch at method transition at indices ",badind," in skin()"))
475 let(
476 full_list = // If there are no duplicators then use more efficient where the whole input is treated together
477 !in_list(DUPLICATOR,method_type) ?
478 let(
479 resampled = [for(i=idx(profiles)) subdivide_path(profiles[i], max_list[i], method=sampling)],
480 fixedprof = [for(i=idx(profiles))
481 i==0 || method[i-1]=="direct" ? resampled[i]
482 : reindex_polygon(resampled[i-1],resampled[i])],
483 sliced = slice_profiles(fixedprof, slices, closed)
484 )
485 [!closed ? sliced : concat(sliced,[sliced[0]])]
486 : // There are duplicators, so use approach where each pair is treated separately
487 [for(i=[0:profcount-1])
488 let(
489 pair =
490 method[i]=="distance" ? _skin_distance_match(profiles[i],select(profiles,i+1)) :
491 method[i]=="fast_distance" ? _skin_aligned_distance_match(profiles[i], select(profiles,i+1)) :
492 method[i]=="tangent" ? _skin_tangent_match(profiles[i],select(profiles,i+1)) :
493 /*method[i]=="reindex" || method[i]=="direct" ?*/
494 let( p1 = subdivide_path(profiles[i],max_list[i], method=sampling),
495 p2 = subdivide_path(select(profiles,i+1),max_list[i], method=sampling)
496 ) (method[i]=="direct" ? [p1,p2] : [p1, reindex_polygon(p1, p2)]),
497 nsamples = method_type[i]==RESAMPLING ? len(pair[0]) :
498 assert(refine[i]==select(refine,i+1),str("Refine value mismatch at indices ",[i,(i+1)%len(refine)],
499 ". Method ",method[i]," requires equal values"))
500 refine[i] * len(pair[0])
501 )
502 subdivide_and_slice(pair,slices[i], nsamples, method=sampling)],
503 vnf=vnf_join(
504 [for(i=idx(full_list))
505 vnf_vertex_array(full_list[i], cap1=i==0 && fullcaps[0], cap2=i==len(full_list)-1 && fullcaps[1],
506 col_wrap=true, style=style)])
507 )
508 reorient(anchor,spin,orient,vnf=vnf,p=vnf,extent=atype=="hull",cp=cp);
509
510
511
512// Function&Module: linear_sweep()
513// Synopsis: Create a linear extrusion from a path, with optional texturing.
514// SynTags: VNF, Geom
515// Topics: Extrusion, Textures, Sweep
516// See Also: rotate_sweep(), sweep(), spiral_sweep(), path_sweep(), offset_sweep()
517// Usage: As Module
518// linear_sweep(region, [height], [center=], [slices=], [twist=], [scale=], [style=], [caps=], [convexity=]) [ATTACHMENTS];
519// Usage: With Texturing
520// linear_sweep(region, [height], [center=], texture=, [tex_size=]|[tex_reps=], [tex_depth=], [style=], [tex_samples=], ...) [ATTACHMENTS];
521// Usage: As Function
522// vnf = linear_sweep(region, [height], [center=], [slices=], [twist=], [scale=], [style=], [caps=]);
523// vnf = linear_sweep(region, [height], [center=], texture=, [tex_size=]|[tex_reps=], [tex_depth=], [style=], [tex_samples=], ...);
524// Description:
525// If called as a module, creates a polyhedron that is the linear extrusion of the given 2D region or polygon.
526// If called as a function, returns a VNF that can be used to generate a polyhedron of the linear extrusion
527// of the given 2D region or polygon. The benefit of using this, over using `linear_extrude region(rgn)` is
528// that it supports `anchor`, `spin`, `orient` and attachments. You can also make more refined
529// twisted extrusions by using `maxseg` to subsample flat faces.
530// .
531// 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
532// predictable anchors for LEFT, RIGHT, FWD and BACK in many cases, but the anchors may only
533// be aproximately correct for twisted objects, and corner anchors may point in unexpected directions in some cases.
534// If you need anchors directly computed from the surface you can pass the vnf from linear_sweep
535// to {{vnf_polyhedron()}}, which will compute anchors directly from the full VNF.
536// Arguments:
537// region = The 2D [Region](regions.scad) or polygon that is to be extruded.
538// h / height / l / length = The height to extrude the region. Default: 1
539// center = If true, the created polyhedron will be vertically centered. If false, it will be extruded upwards from the XY plane. Default: `false`
540// ---
541// twist = The number of degrees to rotate the top of the shape, clockwise around the Z axis, relative to the bottom. Default: 0
542// 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
543// 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]
544// 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`
545// 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)
546// 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.
547// 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]`
548// 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.
549// 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`
550// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
551// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
552// tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
553// style = The style to use when triangulating the surface of the object. Valid values are `"default"`, `"alt"`, or `"quincunx"`.
554// caps = If false do not create end caps. Can be a boolean vector. Default: true
555// convexity = Max number of surfaces any single ray could pass through. Module use only.
556// 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"`
557// atype = Set to "hull" or "intersect" to select anchor type. Default: "hull"
558// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
559// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
560// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
561// Anchor Types:
562// "hull" = Anchors to the virtual convex hull of the shape.
563// "intersect" = Anchors to the surface of the shape.
564// "bbox" = Anchors to the bounding box of the extruded shape.
565// Extra Anchors:
566// "origin" = Centers the extruded shape vertically only, but keeps the original path positions in the X and Y. Oriented UP.
567// "original_base" = Keeps the original path positions in the X and Y, but at the bottom of the extrusion. Oriented UP.
568// Example: Extruding a Compound Region.
569// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
570// rgn2 = [square(30,center=false)];
571// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
572// mrgn = union(rgn1,rgn2);
573// orgn = difference(mrgn,rgn3);
574// linear_sweep(orgn,height=20,convexity=16);
575// Example: With Twist, Scale, Shift, Slices and Maxseg.
576// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
577// rgn2 = [square(30,center=false)];
578// rgn3 = [
579// for (size=[10:10:20])
580// apply(
581// move([15,15]),
582// square(size=size, center=true)
583// )
584// ];
585// mrgn = union(rgn1,rgn2);
586// orgn = difference(mrgn,rgn3);
587// linear_sweep(
588// orgn, height=50, maxseg=2, slices=40,
589// twist=90, scale=0.5, shift=[10,5],
590// convexity=16
591// );
592// Example: Anchors on an Extruded Region
593// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
594// rgn2 = [square(30,center=false)];
595// rgn3 = [
596// for (size=[10:10:20])
597// apply(
598// move([15,15]),
599// rect(size=size)
600// )
601// ];
602// mrgn = union(rgn1,rgn2);
603// orgn = difference(mrgn,rgn3);
604// linear_sweep(orgn,height=20,convexity=16)
605// show_anchors();
606// Example: "diamonds" texture.
607// path = glued_circles(r=15, spread=40, tangent=45);
608// linear_sweep(
609// path, texture="diamonds", tex_size=[5,10],
610// h=40, style="concave");
611// Example: "pyramids" texture.
612// linear_sweep(
613// rect(50), texture="pyramids", tex_size=[10,10],
614// h=40, style="convex");
615// Example: "bricks_vnf" texture.
616// path = glued_circles(r=15, spread=40, tangent=45);
617// linear_sweep(
618// path, texture="bricks_vnf", tex_size=[10,10],
619// tex_depth=0.25, h=40);
620// Example: User defined heightfield texture.
621// path = ellipse(r=[20,10]);
622// texture = [for (i=[0:9])
623// [for (j=[0:9])
624// 1/max(0.5,norm([i,j]-[5,5])) ]];
625// linear_sweep(
626// path, texture=texture, tex_size=[5,5],
627// h=40, style="min_edge", anchor=BOT);
628// Example: User defined VNF tile texture.
629// path = ellipse(r=[20,10]);
630// tex = let(n=16,m=0.25) [
631// [
632// each resample_path(path3d(square(1)),n),
633// each move([0.5,0.5],
634// p=path3d(circle(d=0.5,$fn=n),m)),
635// [1/2,1/2,0],
636// ], [
637// for (i=[0:1:n-1]) each [
638// [i,(i+1)%n,(i+3)%n+n],
639// [i,(i+3)%n+n,(i+2)%n+n],
640// [2*n,n+i,n+(i+1)%n],
641// ]
642// ]
643// ];
644// linear_sweep(path, texture=tex, tex_size=[5,5], h=40);
645// Example: Textured with twist and scale.
646// linear_sweep(regular_ngon(n=3, d=50),
647// texture="rough", h=100, tex_depth=2,
648// tex_size=[20,20], style="min_edge",
649// convexity=10, scale=0.2, twist=120);
650// Example: As Function
651// path = glued_circles(r=15, spread=40, tangent=45);
652// vnf = linear_sweep(
653// path, h=40, texture="trunc_pyramids", tex_size=[5,5],
654// tex_depth=1, style="convex");
655// vnf_polyhedron(vnf, convexity=10);
656// Example: VNF tile that has no top/bottom edges and produces a disconnected result
657// shape = skin([rect(2/5),
658// rect(2/3),
659// rect(2/5)],
660// z=[0,1/2,1],
661// slices=0,
662// caps=false);
663// tile = move([0,1/2,2/3],yrot(90,shape));
664// linear_sweep(circle(20), texture=tile,
665// tex_size=[10,10],tex_depth=5,
666// h=40,convexity=4);
667// 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.
668// shape = skin([rect(2/5),
669// rect(2/3),
670// rect(2/5)],
671// z=[0,1/2,1],
672// slices=0,
673// caps=false);
674// tile = move([1/2,1,2/3],xrot(90,shape));
675// linear_sweep(circle(20), texture=tile,
676// tex_size=[30,20],tex_depth=15,
677// h=40,convexity=4);
678// Example: This example shows some endcap polygons missing and a spurious triangle
679// shape = skin([rect(2/5),
680// rect(2/3),
681// rect(2/5)],
682// z=[0,1/2,1],
683// slices=0,
684// caps=false);
685// tile = xscale(.5,move([1/2,1,2/3],xrot(90,shape)));
686// doubletile = vnf_join([tile, right(.5,tile)]);
687// linear_sweep(circle(20), texture=doubletile,
688// tex_size=[45,45],tex_depth=15, h=40);
689// Example: You can fix ends for disconnected cases using {{top_half()}} and {{bottom_half()}}
690// shape = skin([rect(2/5),
691// rect(2/3),
692// rect(2/5)],
693// z=[0,1/2,1],
694// slices=0,
695// caps=false);
696// tile = move([1/2,1,2/3],xrot(90,shape));
697// vnf_polyhedron(
698// top_half(
699// bottom_half(
700// linear_sweep(circle(20), texture=tile,
701// tex_size=[30,20],tex_depth=15,
702// h=40.2,caps=false),
703// z=20),
704// z=-20));
705
706module linear_sweep(
707 region, height, center,
708 twist=0, scale=1, shift=[0,0],
709 slices, maxseg, style="default", convexity, caps=true,
710 texture, tex_size=[5,5], tex_reps, tex_counts,
711 tex_inset=false, tex_rot=0,
712 tex_depth, tex_scale, tex_samples,
713 cp, atype="hull", h,l,length,
714 anchor, spin=0, orient=UP
715) {
716 h = one_defined([h, height,l,length],"h,height,l,length",dflt=1);
717 region = force_region(region);
718 check = assert(is_region(region),"Input is not a region");
719 anchor = center==true? "origin" :
720 center == false? "original_base" :
721 default(anchor, "original_base");
722 vnf = linear_sweep(
723 region, height=h, style=style, caps=caps,
724 twist=twist, scale=scale, shift=shift,
725 texture=texture,
726 tex_size=tex_size,
727 tex_reps=tex_reps,
728 tex_counts=tex_counts,
729 tex_inset=tex_inset,
730 tex_rot=tex_rot,
731 tex_depth=tex_depth,
732 tex_samples=tex_samples,
733 slices=slices,
734 maxseg=maxseg,
735 anchor="origin"
736 );
737 anchors = [
738 named_anchor("original_base", [0,0,-h/2], UP)
739 ];
740 cp = default(cp, "centroid");
741 geom = atype=="hull"? attach_geom(cp=cp, region=region, h=h, extent=true, shift=shift, scale=scale, twist=twist, anchors=anchors) :
742 atype=="intersect"? attach_geom(cp=cp, region=region, h=h, extent=false, shift=shift, scale=scale, twist=twist, anchors=anchors) :
743 atype=="bbox"?
744 let(
745 bounds = pointlist_bounds(flatten(region)),
746 size = bounds[1] - bounds[0],
747 midpt = (bounds[0] + bounds[1])/2
748 )
749 attach_geom(cp=[0,0,0], size=point3d(size,h), offset=point3d(midpt), shift=shift, scale=scale, twist=twist, anchors=anchors) :
750 assert(in_list(atype, ["hull","intersect","bbox"]), "Anchor type must be \"hull\", \"intersect\", or \"bbox\".");
751 attachable(anchor,spin,orient, geom=geom) {
752 vnf_polyhedron(vnf, convexity=convexity);
753 children();
754 }
755}
756
757
758function linear_sweep(
759 region, height, center,
760 twist=0, scale=1, shift=[0,0],
761 slices, maxseg, style="default", caps=true,
762 cp, atype="hull", h,
763 texture, tex_size=[5,5], tex_reps, tex_counts,
764 tex_inset=false, tex_rot=0,
765 tex_scale, tex_depth, tex_samples, h, l, length,
766 anchor, spin=0, orient=UP
767) =
768 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.")
769 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.")
770 let(
771 region = force_region(region),
772 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
773 : tex_reps,
774 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
775 : default(tex_depth,1)
776 )
777 assert(is_region(region), "Input is not a region or polygon.")
778 assert(is_num(scale) || is_vector(scale))
779 assert(is_vector(shift, 2), str(shift))
780 assert(is_bool(caps) || is_bool_list(caps,2), "caps must be boolean or a list of two booleans")
781 let(
782 h = one_defined([h, height,l,length],"h,height,l,length",dflt=1)
783 )
784 !is_undef(texture)? _textured_linear_sweep(
785 region, h=h, caps=caps,
786 texture=texture, tex_size=tex_size,
787 counts=tex_reps, inset=tex_inset,
788 rot=tex_rot, tex_scale=tex_depth,
789 twist=twist, scale=scale, shift=shift,
790 style=style, samples=tex_samples,
791 anchor=anchor, spin=spin, orient=orient
792 ) :
793 let(
794 caps = is_bool(caps) ? [caps,caps] : caps,
795 anchor = center==true? "origin" :
796 center == false? "original_base" :
797 default(anchor, "original_base"),
798 regions = region_parts(region),
799 slices = default(slices, max(1,ceil(abs(twist)/5))),
800 scale = is_num(scale)? [scale,scale] : point2d(scale),
801 topmat = move(shift) * scale(scale) * rot(-twist),
802 trgns = [
803 for (rgn = regions) [
804 for (path = rgn) let(
805 p = list_unwrap(path),
806 path = is_undef(maxseg)? p : [
807 for (seg = pair(p,true)) each
808 let( steps = ceil(norm(seg.y - seg.x) / maxseg) )
809 lerpn(seg.x, seg.y, steps, false)
810 ]
811 ) apply(topmat, path)
812 ]
813 ],
814 vnf = vnf_join([
815 for (rgn = regions)
816 for (pathnum = idx(rgn)) let(
817 p = list_unwrap(rgn[pathnum]),
818 path = is_undef(maxseg)? p : [
819 for (seg=pair(p,true)) each
820 let(steps=ceil(norm(seg.y-seg.x)/maxseg))
821 lerpn(seg.x, seg.y, steps, false)
822 ],
823 verts = [
824 for (i=[0:1:slices]) let(
825 u = i / slices,
826 scl = lerp([1,1], scale, u),
827 ang = lerp(0, -twist, u),
828 off = lerp([0,0,-h/2], point3d(shift,h/2), u),
829 m = move(off) * scale(scl) * rot(ang)
830 ) apply(m, path3d(path))
831 ]
832 ) vnf_vertex_array(verts, caps=false, col_wrap=true, style=style),
833 if (caps[0]) for (rgn = regions) vnf_from_region(rgn, down(h/2), reverse=true),
834 if (caps[1]) for (rgn = trgns) vnf_from_region(rgn, up(h/2), reverse=false)
835 ]),
836 anchors = [
837 named_anchor("original_base", [0,0,-h/2], UP)
838 ],
839 cp = default(cp, "centroid"),
840 geom = atype=="hull"? attach_geom(cp=cp, region=region, h=h, extent=true, shift=shift, scale=scale, twist=twist, anchors=anchors) :
841 atype=="intersect"? attach_geom(cp=cp, region=region, h=h, extent=false, shift=shift, scale=scale, twist=twist, anchors=anchors) :
842 atype=="bbox"?
843 let(
844 bounds = pointlist_bounds(flatten(region)),
845 size = bounds[1] - bounds[0],
846 midpt = (bounds[0] + bounds[1])/2
847 )
848 attach_geom(cp=[0,0,0], size=point3d(size,h), offset=point3d(midpt), shift=shift, scale=scale, twist=twist, anchors=anchors) :
849 assert(in_list(atype, ["hull","intersect","bbox"]), "Anchor type must be \"hull\", \"intersect\", or \"bbox\".")
850 ) reorient(anchor,spin,orient, geom=geom, p=vnf);
851
852
853// Function&Module: rotate_sweep()
854// Synopsis: Create a surface of revolution from a path with optional texturing.
855// SynTags: VNF, Geom
856// Topics: Extrusion, Sweep, Revolution, Textures
857// See Also: linear_sweep(), sweep(), spiral_sweep(), path_sweep(), offset_sweep()
858// Usage: As Function
859// vnf = rotate_sweep(shape, [angle], ...);
860// Usage: As Module
861// rotate_sweep(shape, [angle], ...) [ATTACHMENTS];
862// Usage: With Texturing
863// rotate_sweep(shape, texture=, [tex_size=]|[tex_reps=], [tex_depth=], [tex_samples=], [tex_rot=], [tex_inset=], ...) [ATTACHMENTS];
864// Description:
865// Takes a polygon or [region](regions.scad) and sweeps it in a rotation around the Z axis, with optional texturing.
866// When called as a function, returns a [VNF](vnf.scad).
867// When called as a module, creates the sweep as geometry.
868// Arguments:
869// shape = The polygon or [region](regions.scad) to sweep around the Z axis.
870// 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)
871// ---
872// 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.
873// 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]`
874// 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.
875// 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`
876// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
877// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
878// tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
879// style = {{vnf_vertex_array()}} style. Default: "min_edge"
880// 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`
881// convexity = (Module only) Convexity setting for use with polyhedron. Default: 10
882// 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"
883// atype = Select "hull" or "intersect" anchor types. Default: "hull"
884// anchor = Translate so anchor point is at the origin. Default: "origin"
885// spin = Rotate this many degrees around Z axis after anchor. Default: 0
886// orient = Vector to rotate top towards after spin (module only)
887// Anchor Types:
888// "hull" = Anchors to the virtual convex hull of the shape.
889// "intersect" = Anchors to the surface of the shape.
890// Example:
891// rgn = [
892// for (a = [0, 120, 240]) let(
893// cp = polar_to_xy(15, a) + [30,0]
894// ) each [
895// move(cp, p=circle(r=10)),
896// move(cp, p=hexagon(d=15)),
897// ]
898// ];
899// rotate_sweep(rgn, angle=240);
900// Example:
901// rgn = right(30, p=union([for (a = [0, 90]) rot(a, p=rect([15,5]))]));
902// rotate_sweep(rgn);
903// Example:
904// path = right(50, p=circle(d=40));
905// rotate_sweep(path, texture="bricks_vnf", tex_size=[10,10], tex_depth=0.5, style="concave");
906// Example:
907// tex = [
908// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
909// [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
910// [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
911// [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
912// [0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1],
913// [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
914// [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
915// [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
916// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
917// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
918// [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
919// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
920// ];
921// path = arc(cp=[0,0], r=40, start=60, angle=-120);
922// rotate_sweep(
923// path, closed=false,
924// texture=tex, tex_size=[20,20],
925// tex_depth=1, style="concave");
926// Example:
927// include <BOSL2/beziers.scad>
928// bezpath = [
929// [15, 30], [10,15],
930// [10, 0], [20, 10], [30,12],
931// [30,-12], [20,-10], [10, 0],
932// [10,-15], [15,-30]
933// ];
934// path = bezpath_curve(bezpath, splinesteps=32);
935// rotate_sweep(
936// path, closed=false,
937// texture="diamonds", tex_size=[10,10],
938// tex_depth=1, style="concave");
939// Example:
940// path = [
941// [20, 30], [20, 20],
942// each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
943// [20,-20], [20,-30],
944// ];
945// vnf = rotate_sweep(
946// path, closed=false,
947// texture="trunc_pyramids",
948// tex_size=[5,5], tex_depth=1,
949// style="convex");
950// vnf_polyhedron(vnf, convexity=10);
951// Example:
952// rgn = [
953// right(40, p=circle(d=50)),
954// right(40, p=circle(d=40,$fn=6)),
955// ];
956// rotate_sweep(
957// rgn, texture="diamonds",
958// tex_size=[10,10], tex_depth=1,
959// angle=240, style="concave");
960
961function rotate_sweep(
962 shape, angle=360,
963 texture, tex_size=[5,5], tex_counts, tex_reps,
964 tex_inset=false, tex_rot=0,
965 tex_scale, tex_depth, tex_samples,
966 tex_taper, shift=[0,0], closed=true,
967 style="min_edge", cp="centroid",
968 atype="hull", anchor="origin",
969 spin=0, orient=UP,
970 _tex_inhibit_y_slicing=false
971) =
972 assert(num_defined([tex_reps,tex_counts])<2, "In rotate_sweep() the 'tex_counts' parameters has been replaced by 'tex_reps'. You cannot give both.")
973 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.")
974 let( region = force_region(shape),
975 tex_reps = is_def(tex_counts)? echo("In rotate_sweep() the 'tex_counts' parameter is deprecated and has been replaced by 'tex_reps'")tex_counts
976 : tex_reps,
977 tex_depth = is_def(tex_scale)? echo("In rotate_sweep() the 'tex_scale' parameter is deprecated and has been replaced by 'tex_depth'")tex_scale
978 : default(tex_depth,1)
979 )
980 assert(is_region(region), "Input is not a region or polygon.")
981 let(
982 bounds = pointlist_bounds(flatten(region)),
983 min_x = bounds[0].x,
984 max_x = bounds[1].x,
985 min_y = bounds[0].y,
986 max_y = bounds[1].y,
987 h = max_y - min_y
988 )
989 assert(min_x>=0, "Input region must exist entirely in the X+ half-plane.")
990 !is_undef(texture)? _textured_revolution(
991 shape,
992 texture=texture,
993 tex_size=tex_size,
994 counts=tex_reps,
995 tex_scale=tex_depth,
996 inset=tex_inset,
997 rot=tex_rot,
998 samples=tex_samples,
999 inhibit_y_slicing=_tex_inhibit_y_slicing,
1000 taper=tex_taper,
1001 shift=shift,
1002 closed=closed,
1003 angle=angle,
1004 style=style
1005 ) :
1006 let(
1007 steps = ceil(segs(max_x) * angle / 360) + (angle<360? 1 : 0),
1008 skmat = down(min_y) * skew(sxz=shift.x/h, syz=shift.y/h) * up(min_y),
1009 transforms = [
1010 if (angle==360) for (i=[0:1:steps-1]) skmat * rot([90,0,360-i*360/steps]),
1011 if (angle<360) for (i=[0:1:steps-1]) skmat * rot([90,0,angle-i*angle/(steps-1)]),
1012 ],
1013 vnf = sweep(
1014 region, transforms,
1015 closed=angle==360,
1016 caps=angle!=360,
1017 style=style, cp=cp,
1018 atype=atype, anchor=anchor,
1019 spin=spin, orient=orient
1020 )
1021 ) vnf;
1022
1023
1024module rotate_sweep(
1025 shape, angle=360,
1026 texture, tex_size=[5,5], tex_counts, tex_reps,
1027 tex_inset=false, tex_rot=0,
1028 tex_scale, tex_depth, tex_samples,
1029 tex_taper, shift=[0,0],
1030 style="min_edge",
1031 closed=true,
1032 cp="centroid",
1033 convexity=10,
1034 atype="hull",
1035 anchor="origin",
1036 spin=0,
1037 orient=UP,
1038 _tex_inhibit_y_slicing=false
1039) {
1040 dummy =
1041 assert(num_defined([tex_reps,tex_counts])<2, "In rotate_sweep() the 'tex_counts' parameters has been replaced by 'tex_reps'. You cannot give both.")
1042 assert(num_defined([tex_scale,tex_depth])<2, "In rotate_sweep() the 'tex_scale' parameter has been replaced by 'tex_depth'. You cannot give both.");
1043 tex_reps = is_def(tex_counts)? echo("In rotate_sweep() the 'tex_counts' parameter is deprecated and has been replaced by 'tex_reps'")tex_counts
1044 : tex_reps;
1045 tex_depth = is_def(tex_scale)? echo("In rotate_sweep() the 'tex_scale' parameter is deprecated and has been replaced by 'tex_depth'")tex_scale
1046 : default(tex_depth,1);
1047 region = force_region(shape);
1048 check = assert(is_region(region), "Input is not a region or polygon.");
1049 bounds = pointlist_bounds(flatten(region));
1050 min_x = bounds[0].x;
1051 max_x = bounds[1].x;
1052 min_y = bounds[0].y;
1053 max_y = bounds[1].y;
1054 h = max_y - min_y;
1055 check2 = assert(min_x>=0, "Input region must exist entirely in the X+ half-plane.");
1056 if (!is_undef(texture)) {
1057 _textured_revolution(
1058 shape,
1059 texture=texture,
1060 tex_size=tex_size,
1061 counts=tex_reps,
1062 tex_scale=tex_depth,
1063 inset=tex_inset,
1064 rot=tex_rot,
1065 samples=tex_samples,
1066 taper=tex_taper,
1067 shift=shift,
1068 closed=closed,
1069 inhibit_y_slicing=_tex_inhibit_y_slicing,
1070 angle=angle,
1071 style=style,
1072 atype=atype, anchor=anchor,
1073 spin=spin, orient=orient
1074 ) children();
1075 } else {
1076 steps = ceil(segs(max_x) * angle / 360) + (angle<360? 1 : 0);
1077 skmat = down(min_y) * skew(sxz=shift.x/h, syz=shift.y/h) * up(min_y);
1078 transforms = [
1079 if (angle==360) for (i=[0:1:steps-1]) skmat * rot([90,0,360-i*360/steps]),
1080 if (angle<360) for (i=[0:1:steps-1]) skmat * rot([90,0,angle-i*angle/(steps-1)]),
1081 ];
1082 sweep(
1083 region, transforms,
1084 closed=angle==360,
1085 caps=angle!=360,
1086 style=style, cp=cp,
1087 convexity=convexity,
1088 atype=atype, anchor=anchor,
1089 spin=spin, orient=orient
1090 ) children();
1091 }
1092}
1093
1094
1095// Function&Module: spiral_sweep()
1096// Synopsis: Sweep a path along a helix.
1097// SynTags: VNF, Geom
1098// Topics: Extrusion, Sweep, Spiral
1099// See Also: thread_helix(), linear_sweep(), rotate_sweep(), sweep(), path_sweep(), offset_sweep()
1100// Usage: As Module
1101// spiral_sweep(poly, h, r|d=, turns, [taper=], [center=], [taper1=], [taper2=], [internal=], ...)[ATTACHMENTS];
1102// spiral_sweep(poly, h, r1=|d1=, r2=|d2=, turns, [taper=], [center=], [taper1=], [taper2=], [internal=], ...)[ATTACHMENTS];
1103// Usage: As Function
1104// vnf = spiral_sweep(poly, h, r|d=, turns, ...);
1105// vnf = spiral_sweep(poly, h, r1=|d1=, r1=|d2=, turns, ...);
1106// Description:
1107// Takes a closed 2D polygon path, centered on the XY plane, and sweeps/extrudes it along a 3D spiral path
1108// of a given radius, height and degrees of rotation. The origin in the profile traces out the helix of the specified radius.
1109// If turns is positive the path will be right-handed; if turns is negative the path will be left-handed.
1110// Such an extrusion can be used to make screw threads.
1111// .
1112// The lead_in options specify a lead-in section where the ends of the spiral scale down to avoid a sharp cut face at the ends.
1113// You can specify the length of this scaling directly with the lead_in parameters or as an angle using the lead_in_ang parameters.
1114// If you give a positive value, the extrusion is lengthenend by the specified distance or angle; if you give a negative
1115// value then the scaled end is included in the extrusion length specified by `turns`. If the value is zero then no scaled ends
1116// are produced. The shape of the scaled ends can be controlled with the lead_in_shape parameter. Supported options are "sqrt", "linear"
1117// "smooth" and "cut".
1118// .
1119// The inside argument changes how the extrusion lead-in sections are formed. If it is true then they scale
1120// towards the outside, like would be needed for internal threading. If internal is fale then the lead-in sections scale
1121// towards the inside, like would be appropriate for external threads.
1122// Arguments:
1123// poly = Array of points of a polygon path, to be extruded.
1124// h = height of the spiral extrusion path
1125// r = Radius of the spiral extrusion path
1126// turns = number of revolutions to include in the spiral
1127// ---
1128// d = Diameter of the spiral extrusion path.
1129// d1/r1 = Bottom inside diameter or radius of spiral to extrude along.
1130// d2/r2 = Top inside diameter or radius of spiral to extrude along.
1131// lead_in = Specify linear length of the lead-in scaled section of the spiral. Default: 0
1132// lead_in1 = Specify linear length of the lead-in scaled section of the spiral at the bottom
1133// lead_in2 = Specify linear length of the lead-in scaled section of the spiral at the top
1134// lead_in_ang = Specify angular length of the lead-in scaled section of the spiral
1135// lead_in_ang1 = Specify angular length of the lead-in scaled section of the spiral at the bottom
1136// lead_in_ang2 = Specify angular length of the lead-in scaled section of the spiral at the top
1137// lead_in_shape = Specify the shape of the thread lead in by giving a text string or function. Default: "sqrt"
1138// lead_in_shape1 = Specify the shape of the thread lead-in at the bottom by giving a text string or function.
1139// lead_in_shape2 = Specify the shape of the thread lead-in at the top by giving a text string or function.
1140// lead_in_sample = Factor to increase sample rate in the lead-in section. Default: 10
1141// internal = if true make internal threads. The only effect this has is to change how the extrusion lead-in section are formed. When true, the extrusion scales towards the outside; when false, it scales towards the inside. Default: false
1142// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
1143// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
1144// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
1145// Example:
1146// poly = [[-10,0], [-3,-5], [3,-5], [10,0], [0,-30]];
1147// spiral_sweep(poly, h=200, r=50, turns=3, $fn=36);
1148_leadin_ogive=function (x,L)
1149 let( minscale = .05,
1150 r=(L^2+(1-minscale^2))/2/(1-minscale),
1151 scale = sqrt(r^2-(L*(1-x))^2) -(r-1)
1152 )
1153 x>1 ? [1,1]
1154 : x<0 ? [lerp(minscale,1,.25),0]
1155 : [lerp(scale,1,.25),scale];
1156
1157_leadin_cut = function(x,L) x>0 ? [1,1] : [1,0];
1158
1159_leadin_sqrt = function(x,L)
1160 let(end=0.05) // Smallest scale at the end
1161 x>1 ? [1,1]
1162 : x<0 ? [lerp(end,1,.25),0]
1163 : let(
1164 s = sqrt(x + end^2 * (1-x))
1165 )
1166 [lerp(s,1,.25),s]; // thread width scale, thread height scale
1167
1168_leadin_linear = function(x,L)
1169 let(minscale=.1)
1170 x>1 ? [1,1]
1171 : x<0 ? [lerp(minscale,1,.25),0]
1172 : let(scale = lerp(minscale,1,x))
1173 [lerp(scale,1,.25),scale];
1174
1175_lead_in_table = [
1176 ["default", _leadin_sqrt],
1177 ["sqrt", _leadin_sqrt],
1178 ["cut", _leadin_cut],
1179 ["smooth", _leadin_ogive],
1180 ["linear", _leadin_linear]
1181];
1182
1183
1184function _ss_polygon_r(N,theta) =
1185 let( alpha = 360/N )
1186 cos(alpha/2)/(cos(posmod(theta,alpha)-alpha/2));
1187function spiral_sweep(poly, h, r, turns=1, taper, r1, r2, d, d1, d2, internal=false,
1188 lead_in_shape,lead_in_shape1, lead_in_shape2,
1189 lead_in, lead_in1, lead_in2,
1190 lead_in_ang, lead_in_ang1, lead_in_ang2,
1191 height,l,length,
1192 lead_in_sample = 10,
1193 anchor=CENTER, spin=0, orient=UP) =
1194 assert(is_num(turns) && turns != 0, "turns must be a nonzero number")
1195 assert(all_positive([h]), "Spiral height must be a positive number")
1196 let(
1197 dir = sign(turns),
1198 r1 = get_radius(r1=r1, r=r, d1=d1, d=d),
1199 r2 = get_radius(r1=r2, r=r, d1=d2, d=d),
1200 bounds = pointlist_bounds(poly),
1201 yctr = (bounds[0].y+bounds[1].y)/2,
1202 xmin = bounds[0].x,
1203 xmax = bounds[1].x,
1204 poly = path3d(clockwise_polygon(poly)),
1205 sides = segs(max(r1,r2)),
1206 ang_step = 360/sides,
1207 turns = abs(turns),
1208 lead_in1 = first_defined([lead_in1, lead_in]),
1209 lead_in2 = first_defined([lead_in1, lead_in]),
1210 lead_in_ang1 =
1211 let(
1212 user_ang = first_defined([lead_in_ang1,lead_in_ang])
1213 )
1214 assert(is_undef(user_ang) || is_undef(lead_in1), "Cannot define lead_in/lead_in1 by both length and angle")
1215 is_def(user_ang) ? user_ang : default(lead_in1,0)*360/(2*PI*r1),
1216 lead_in_ang2 =
1217 let(
1218 user_ang = first_defined([lead_in_ang2,lead_in_ang])
1219 )
1220 assert(is_undef(user_ang) || is_undef(lead_in2), "Cannot define lead_in/lead_in2 by both length and angle")
1221 is_def(user_ang) ? user_ang : default(lead_in2,0)*360/(2*PI*r2),
1222 minang = -max(0,lead_in_ang1),
1223 maxang = 360*turns + max(0,lead_in_ang2),
1224 cut_ang1 = minang+abs(lead_in_ang1),
1225 cut_ang2 = maxang-abs(lead_in_ang1),
1226 lead_in_shape1 = first_defined([lead_in_shape1, lead_in_shape, "default"]),
1227 lead_in_shape2 = first_defined([lead_in_shape2, lead_in_shape, "default"]),
1228 lead_in_func1 = is_func(lead_in_shape1) ? lead_in_shape1
1229 : assert(is_string(lead_in_shape1),"lead_in_shape/lead_in_shape1 must be a function or string")
1230 let(ind = search([lead_in_shape1], _lead_in_table,0)[0])
1231 assert(ind!=[],str("Unknown lead_in_shape, \"",lead_in_shape1,"\""))
1232 _lead_in_table[ind[0]][1],
1233 lead_in_func2 = is_func(lead_in_shape2) ? lead_in_shape2
1234 : assert(is_string(lead_in_shape2),"lead_in_shape/lead_in_shape2 must be a function or string")
1235 let(ind = search([lead_in_shape2], _lead_in_table,0)[0])
1236 assert(ind!=[],str("Unknown lead_in_shape, \"",lead_in_shape2,"\""))
1237 _lead_in_table[ind[0]][1]
1238 )
1239 assert( cut_ang1<cut_ang2, "Tapers are too long to fit")
1240 assert( all_positive([r1,r2]), "Diameter/radius must be positive")
1241 let(
1242
1243 // This complicated sampling scheme is designed to ensure that faceting always starts at angle zero
1244 // for alignment with cylinders, and there is always a facet boundary at the $fn specified locations,
1245 // regardless of what kind of subsampling occurs for tapers.
1246 orig_anglist = [
1247 if (minang<0) minang,
1248 each reverse([for(ang = [-ang_step:-ang_step:minang+EPSILON]) ang]),
1249 for(ang = [0:ang_step:maxang-EPSILON]) ang,
1250 maxang
1251 ],
1252 anglist = [
1253 for(a=orig_anglist) if (a<cut_ang1-EPSILON) a,
1254 cut_ang1,
1255 for(a=orig_anglist) if (a>cut_ang1+EPSILON && a<cut_ang2-EPSILON) a,
1256 cut_ang2,
1257 for(a=orig_anglist) if (a>cut_ang2+EPSILON) a
1258 ],
1259 interp_ang = [
1260 for(i=idx(anglist,e=-2))
1261 each lerpn(anglist[i],anglist[i+1],
1262 (lead_in_ang1!=0 && anglist[i+1]<=cut_ang1) || (lead_in_ang2!=0 && anglist[i]>=cut_ang2)
1263 ? ceil((anglist[i+1]-anglist[i])/ang_step*lead_in_sample)
1264 : 1,
1265 endpoint=false),
1266 last(anglist)
1267 ],
1268 skewmat = affine3d_skew_xz(xa=atan2(r2-r1,h)),
1269 points = [
1270 for (a = interp_ang) let (
1271 hsc = a<cut_ang1 ? lead_in_func1((a-minang)/abs(lead_in_ang1),abs(lead_in_ang1)*2*PI*r1/360)
1272 : a>cut_ang2 ? lead_in_func2((maxang-a)/abs(lead_in_ang2),abs(lead_in_ang2)*2*PI*r2/360)
1273 : [1,1],
1274 u = a/(360*turns),
1275 r = lerp(r1,r2,u),
1276 mat = affine3d_zrot(dir*a)
1277 * affine3d_translate([_ss_polygon_r(sides,dir*a)*r, 0, h * (u-0.5)])
1278 * affine3d_xrot(90)
1279 * skewmat
1280 * scale([hsc.y,hsc.x,1], cp=[internal ? xmax : xmin, yctr, 0]),
1281 pts = apply(mat, poly)
1282 ) pts
1283 ],
1284 vnf = vnf_vertex_array(
1285 points, col_wrap=true, caps=true, reverse=dir>0,
1286 // style=higbee1>0 || higbee2>0 ? "quincunx" : "alt"
1287 style="convex"
1288 ),
1289 vnf2 = vnf_triangulate(vnf)
1290 )
1291 reorient(anchor,spin,orient, vnf=vnf2, r1=r1, r2=r2, l=h, p=vnf2);
1292
1293
1294
1295module spiral_sweep(poly, h, r, turns=1, taper, r1, r2, d, d1, d2, internal=false,
1296 lead_in_shape,lead_in_shape1, lead_in_shape2,
1297 lead_in, lead_in1, lead_in2,
1298 lead_in_ang, lead_in_ang1, lead_in_ang2,
1299 height,l,length,
1300 lead_in_sample=10,
1301 anchor=CENTER, spin=0, orient=UP)
1302{
1303 vnf = spiral_sweep(poly=poly, h=h, r=r, turns=turns, r1=r1, r2=r2, d=d, d1=d1, d2=d2, internal=internal,
1304 lead_in_shape=lead_in_shape,lead_in_shape1=lead_in_shape1, lead_in_shape2=lead_in_shape2,
1305 lead_in=lead_in, lead_in1=lead_in1, lead_in2=lead_in2,
1306 lead_in_ang=lead_in_ang, lead_in_ang1=lead_in_ang1, lead_in_ang2=lead_in_ang2,
1307 height=height,l=length,length=length,
1308 lead_in_sample=lead_in_sample);
1309 h = one_defined([h,height,length,l],"h,height,length,l");
1310 r1 = get_radius(r1=r1, r=r, d1=d1, d=d);
1311 r2 = get_radius(r1=r2, r=r, d1=d2, d=d);
1312 lead_in1 = u_mul(first_defined([lead_in1,lead_in]),1/(2*PI*r1));
1313 lead_in2 = u_mul(first_defined([lead_in2,lead_in]),1/(2*PI*r2));
1314 lead_in_ang1 = first_defined([lead_in_ang1,lead_in_ang]);
1315 lead_in_ang2 = first_defined([lead_in_ang2,lead_in_ang]);
1316 extra_turns = max(0,first_defined([lead_in1,lead_in_ang1,0]))+max(0,first_defined([lead_in2,lead_in_ang2,0]));
1317 attachable(anchor,spin,orient, r1=r1, r2=r2, l=h) {
1318 vnf_polyhedron(vnf, convexity=ceil(2*(abs(turns)+extra_turns)));
1319 children();
1320 }
1321}
1322
1323
1324
1325// Function&Module: path_sweep()
1326// Synopsis: Sweep a 2d polygon path along a 2d or 3d path.
1327// SynTags: VNF, Geom
1328// Topics: Extrusion, Sweep, Paths
1329// See Also: linear_sweep(), rotate_sweep(), sweep(), spiral_sweep(), path_sweep2d(), offset_sweep()
1330// Usage: As module
1331// path_sweep(shape, path, [method], [normal=], [closed=], [twist=], [twist_by_length=], [symmetry=], [scale=], [scale_by_length=], [last_normal=], [tangent=], [uniform=], [relaxed=], [caps=], [style=], [convexity=], [anchor=], [cp=], [spin=], [orient=], [atype=]) [ATTACHMENTS];
1332// Usage: As function
1333// vnf = path_sweep(shape, path, [method], [normal=], [closed=], [twist=], [twist_by_length=], [symmetry=], [scale=], [scale_by_length=], [last_normal=], [tangent=], [uniform=], [relaxed=], [caps=], [style=], [transforms=], [anchor=], [cp=], [spin=], [orient=], [atype=]);
1334// Description:
1335// Takes as input `shape`, a 2D polygon path (list of points), and `path`, a 2d or 3d path (also a list of points)
1336// and constructs a polyhedron by sweeping the shape along the path. When run as a module returns the polyhedron geometry.
1337// When run as a function returns a VNF by default or if you set `transforms=true` then it returns a list of transformations suitable as input to `sweep`.
1338// .
1339// The sweeping process places one copy of the shape for each point in the path. The origin in `shape` is translated to
1340// the point in `path`. The normal vector of the shape, which points in the Z direction, is aligned with the tangent
1341// vector for the path, so this process is constructing a shape whose normal cross sections are equal to your specified shape.
1342// If you do not supply a list of tangent vectors then an approximate tangent vector is computed
1343// based on the path points you supply using {{path_tangents()}}.
1344// Figure(3D,Big,VPR=[70,0,345],VPD=20,VPT=[5.5,10.8,-2.7],NoScales): This example shows how the shape, in this case the quadrilateral defined by `[[0, 0], [0, 1], [0.25, 1], [1, 0]]`, appears as the cross section of the swept polyhedron. The blue line shows the path. The normal vector to the shape is shown in black; it is based at the origin and points upwards in the Z direction. The sweep aligns this normal vector with the blue path tangent, which in this case, flips the shape around. Note that for a 2D path like this one, the Y direction in the shape is mapped to the Z direction in the sweep.
1345// tri= [[0, 0], [0, 1], [.25,1], [1, 0]];
1346// path = arc(r=5,n=81,angle=[-20,65]);
1347// % path_sweep(tri,path);
1348// T = path_sweep(tri,path,transforms=true);
1349// color("red")for(i=[0:20:80]) stroke(apply(T[i],path3d(tri)),width=.1,closed=true);
1350// color("blue")stroke(path3d(arc(r=5,n=101,angle=[-20,80])),width=.1,endcap2="arrow2");
1351// color("red")stroke([path3d(tri)],width=.1);
1352// stroke([CENTER,UP], width=.07,endcap2="arrow2",color="black");
1353// Continues:
1354// In the figure you can see that the swept polyhedron, shown in transparent gray, has the quadrilateral as its cross
1355// section. The quadrilateral is positioned perpendicular to the path, which is shown in blue, so that the normal
1356// vector for the quadrilateral is parallel to the tangent vector for the path. The origin for the shape is the point
1357// which follows the path. For a 2D path, the Y axis of the shape is mapped to the Z axis and in this case,
1358// pointing the quadrilateral's normal vector (in black) along the tangent line of
1359// the path, which is going in the direction of the blue arrow, requires that the quadrilateral be "turned around". If we
1360// reverse the order of points in the path we get a different result:
1361// Figure(3D,Big,VPR=[70,0,20],VPD=20,VPT=[1.25,9.25,-2.65],NoScales): The same sweep operation with the path traveling in the opposite direction. Note that in order to line up the normal correctly, the shape is reversed compared to Figure 1, so the resulting sweep looks quite different.
1362// tri= [[0, 0], [0, 1], [.25,1], [1, 0]];
1363// path = reverse(arc(r=5,n=81,angle=[-20,65]));
1364// % path_sweep(tri,path);
1365// T = path_sweep(tri,path,transforms=true);
1366// color("red")for(i=[0:20:80]) stroke(apply(T[i],path3d(tri)),width=.1,closed=true);
1367// color("blue")stroke(reverse(path3d(arc(r=5,n=101,angle=[-20-15,65]))),width=.1,endcap2="arrow2");
1368// color("red")stroke([path3d(tri)],width=.1);
1369// stroke([CENTER,UP], width=.07,endcap2="arrow2",color="black");
1370// Continues:
1371// If your shape is too large for the curves in the path you can create a situation where the shapes cross each
1372// other. This results in an invalid polyhedron, which may appear OK when previewed or rendered alone, but will give rise
1373// to cryptic CGAL errors when rendered with a second object in your model. You may be able to use {{path_sweep2d()}}
1374// to produce a valid model in cases like this. You can debug models like this using the `profiles=true` option which will show all
1375// the cross sections in your polyhedron. If any of them intersect, the polyhedron will be invalid.
1376// Figure(3D,Big,VPR=[47,0,325],VPD=23,VPT=[6.8,4,-3.8],NoScales): We have scaled the path to an ellipse and show a large triangle as the shape. The triangle is sometimes bigger than the local radius of the path, leading to an invalid polyhedron, which you can identify because the red lines cross in the middle.
1377// tri= scale([4.5,2.5],[[0, 0], [0, 1], [1, 0]]);
1378// path = xscale(1.5,arc(r=5,n=81,angle=[-70,70]));
1379// % path_sweep(tri,path);
1380// T = path_sweep(tri,path,transforms=true);
1381// color("red")for(i=[0:20:80]) stroke(apply(T[i],path3d(tri)),width=.1,closed=true);
1382// color("blue")stroke(path3d(xscale(1.5,arc(r=5,n=81,angle=[-70,80]))),width=.1,endcap2="arrow2");
1383// Continues:
1384// During the sweep operation the shape's normal vector aligns with the tangent vector of the path. Note that
1385// this leaves an ambiguity about how the shape is rotated as it sweeps along the path.
1386// For 2D paths, this ambiguity is resolved by aligning the Y axis of the shape to the Z axis of the swept polyhedron.
1387// You can force the shape to twist as it sweeps along the path using the `twist` parameter, which specifies the total
1388// number of degrees to twist along the whole swept polyhedron. This produces a result like the one shown below.
1389// Figure(3D,Big,VPR=[66,0,14],VPD=20,VPT=[3.4,4.5,-0.8]): The shape twists as we sweep. Note that it still aligns the origin in the shape with the path, and still aligns the normal vector with the path tangent vector.
1390// tri= [[0, 0], [0, 1], [.25,1],[1, 0]];
1391// path = arc(r=5,n=81,angle=[-20,65]);
1392// % path_sweep(tri,path,twist=-60);
1393// T = path_sweep(tri,path,transforms=true,twist=-60);
1394// color("red")for(i=[0:20:80]) stroke(apply(T[i],path3d(tri)),width=.1,closed=true);
1395// color("blue")stroke(path3d(arc(r=5,n=101,angle=[-20,80])),width=.1,endcap2="arrow2");
1396// Continues:
1397// The `twist` argument adds the specified number of degrees of twist into the model, and it may be positive or
1398// negative. When `closed=true` the starting shape and ending shape must match to avoid a sudden extreme twist at the
1399// joint. By default `twist` is therefore required to be a multiple of 360. However, if your shape has rotational
1400// symmetry, this requirement is overly strict. You can specify the symmetry using the `symmetry` argument, and then
1401// you can choose smaller twists consistent with the specified symmetry. The symmetry argument gives the number of
1402// rotations that map the shape exactly onto itself, so a pentagon has 5-fold symmetry. This argument is only valid
1403// for closed sweeps. When you specify symmetry, the twist must be a multiple of 360/symmetry.
1404// .
1405// The twist is normally spread uniformly along your shape based on the path length. If you set `twist_by_length` to
1406// false then the twist will be uniform based on the point count of your path. Twisted shapes will produce twisted
1407// faces, so if you want them to look good you should use lots of points on your path and also lots of points on the
1408// shape. If your shape is a simple polygon, use {{subdivide_path()}} to increase
1409// the number of points.
1410// .
1411// As noted above, the sweep process has an ambiguity regarding the twist. For 2D paths it is easy to resolve this
1412// ambiguity by aligning the Y axis in the shape to the Z axis in the swept polyhedron. When the path is
1413// three-dimensional, things become more complex. It is no longer possible to use a simple alignment rule like the
1414// one we use in 2D. You may find that the shape rotates unexpectedly around its axis as it traverses the path. The
1415// `method` parameter allows you to specify how the shapes are aligned, resulting in different twist in the resulting
1416// polyhedron. You can choose from three different methods for selecting the rotation of your shape. None of these
1417// methods will produce good, or even valid, results on all inputs, so it is important to select a suitable method.
1418// .
1419// The three methods you can choose using the `method` parameter are:
1420// .
1421// The "incremental" method (the default) works by adjusting the shape at each step by the minimal rotation that makes the shape normal to the tangent
1422// at the next point. This method is robust in that it always produces a valid result for well-behaved paths with sufficiently high
1423// sampling. Unfortunately, it can produce a large amount of undesirable twist. When constructing a closed shape this algorithm in
1424// its basic form provides no guarantee that the start and end shapes match up. To prevent a sudden twist at the last segment,
1425// the method calculates the required twist for a good match and distributes it over the whole model (as if you had specified a
1426// twist amount). If you specify `symmetry` this may allow the algorithm to choose a smaller twist for this alignment.
1427// To start the algorithm, we need an initial condition. This is supplied by
1428// using the `normal` argument to give a direction to align the Y axis of your shape. By default the normal points UP if the path
1429// makes an angle of 45 deg or less with the xy plane and it points BACK if the path makes a higher angle with the XY plane. You
1430// can also supply `last_normal` which provides an ending orientation constraint. Be aware that the curve may still exhibit
1431// twisting in the middle. This method is the default because it is the most robust, not because it generally produces the best result.
1432// .
1433// The "natural" method works by computing the Frenet frame at each point on the path. This is defined by the tangent to the curve and
1434// the normal which lies in the plane defined by the curve at each point. This normal points in the direction of curvature of the curve.
1435// The result is a very well behaved set of shape positions without any unexpected twisting—as long as the curvature never falls to zero. At a
1436// point of zero curvature (a flat point), the curve does not define a plane and the natural normal is not defined. Furthermore, even if
1437// you skip over this troublesome point so the normal is defined, it can change direction abruptly when the curvature is zero, leading to
1438// a nasty twist and an invalid model. A simple example is a circular arc joined to another arc that curves the other direction. Note
1439// that the X axis of the shape is aligned with the normal from the Frenet frame.
1440// .
1441// The "manual" method allows you to specify your desired normal either globally with a single vector, or locally with
1442// a list of normal vectors for every path point. The normal you supply is projected to be orthogonal to the tangent to the
1443// path and the Y direction of your shape will be aligned with the projected normal. (Note this is different from the "natural" method.)
1444// Careless choice of a normal may result in a twist in the shape, or an error if your normal is parallel to the path tangent.
1445// If you set `relax=true` then the condition that the cross sections are orthogonal to the path is relaxed and the swept object
1446// uses the actual specified normal. In this case, the tangent is projected to be orthogonal to your supplied normal to define
1447// the cross section orientation. Specifying a list of normal vectors gives you complete control over the orientation of your
1448// cross sections and can be useful if you want to position your model to be on the surface of some solid.
1449// .
1450// You can also apply scaling to the profile along the path. You can give a list of scalar scale factors or a list of 2-vector scale.
1451// In the latter scale the x and y scales of the profile are scaled separately before the profile is placed onto the path. For non-closed
1452// paths you can also give a single scale value or a 2-vector which is treated as the final scale. The intermediate sections
1453// are then scaled by linear interpolation either relative to length (if scale_by_length is true) or by point count otherwise.
1454// .
1455// You can use set `transforms` to true to return a list of transformation matrices instead of the swept shape. In this case, you can
1456// often omit shape entirely. The exception is when `closed=true` and you are using the "incremental" method. In this case, `path_sweep`
1457// uses the shape to correct for twist when the shape closes on itself, so you must include a valid shape.
1458// Arguments:
1459// shape = A 2D polygon path or region describing the shape to be swept.
1460// path = 2D or 3D path giving the path to sweep over
1461// method = one of "incremental", "natural" or "manual". Default: "incremental"
1462// ---
1463// normal = normal vector for initializing the incremental method, or for setting normals with method="manual". Default: UP if the path makes an angle lower than 45 degrees to the xy plane, BACK otherwise.
1464// closed = path is a closed loop. Default: false
1465// twist = amount of twist to add in degrees. For closed sweeps must be a multiple of 360/symmetry. Default: 0
1466// twist_by_length = if true then interpolate twist based on the path length of the path. If false interoplate based on point count. Default: true
1467// symmetry = symmetry of the shape when closed=true. Allows the shape to join with a 360/symmetry rotation instead of a full 360 rotation. Default: 1
1468// scale = Amount to scale the profiles. If you give a scalar the scale starts at 1 and ends at your specified value. The same is true for a 2-vector, but x and y are scaled separately. You can also give a vector of values, one for each path point, and you can give a list of 2-vectors that give the x and y scales of your profile for every point on the path (a Nx2 matrix for a path of length N. Default: 1 (no scaling)
1469// scale_by_length = if true then interpolate scale based on the path length of the path. If false interoplate based on point count. Default: true
1470// last_normal = normal to last point in the path for the "incremental" method. Constrains the orientation of the last cross section if you supply it.
1471// uniform = if set to false then compute tangents using the uniform=false argument, which may give better results when your path is non-uniformly sampled. This argument is passed to {{path_tangents()}}. Default: true
1472// tangent = a list of tangent vectors in case you need more accuracy (particularly at the end points of your curve)
1473// relaxed = set to true with the "manual" method to relax the orthogonality requirement of cross sections to the path tangent. Default: false
1474// caps = Can be a boolean or vector of two booleans. Set to false to disable caps at the two ends. Default: true
1475// style = vnf_vertex_array style. Default: "min_edge"
1476// profiles = if true then display all the cross section profiles instead of the solid shape. Can help debug a sweep. (module only) Default: false
1477// width = the width of lines used for profile display. (module only) Default: 1
1478// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). (function only) Default: false.
1479// convexity = convexity parameter for polyhedron(). (module only) Default: 10
1480// anchor = Translate so anchor point is at the origin. Default: "origin"
1481// spin = Rotate this many degrees around Z axis after anchor. Default: 0
1482// orient = Vector to rotate top towards after spin
1483// atype = Select "hull" or "intersect" anchor types. Default: "hull"
1484// 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"
1485// Side Effects:
1486// `$transforms` is set to the array of transformation matrices that define the swept object.
1487// `$scales` is set to the array of scales that were applied at each point to create the swept object.
1488// Anchor Types:
1489// "hull" = Anchors to the virtual convex hull of the shape.
1490// "intersect" = Anchors to the surface of the shape.
1491// Extra Anchors:
1492// start = When `closed==false`, the origin point of the shape, on the starting face of the object
1493// end = When `closed==false`, the origin point of the shape, on the ending face of the object
1494// start-centroid = When `closed==false`, the centroid of the shape, on the starting face of the object
1495// end-centroid = When `closed==false`, the centroid of the shape, on the ending face of the object
1496// Example(NoScales): A simple sweep of a square along a sine wave:
1497// path = [for(theta=[-180:5:180]) [theta/10, 10*sin(theta)]];
1498// sq = square(6,center=true);
1499// path_sweep(sq,path);
1500// Example(NoScales): If the square is not centered, then we get a different result because the shape is in a different place relative to the origin:
1501// path = [for(theta=[-180:5:180]) [theta/10, 10*sin(theta)]];
1502// sq = square(6);
1503// path_sweep(sq,path);
1504// Example(Med,VPR=[34,0,8],NoScales): It may not be obvious, but the polyhedron in the previous example is invalid. It will eventually give CGAL errors when you combine it with other shapes. To see this, set profiles to true and look at the left side. The profiles cross each other and intersect. Any time this happens, your polyhedron is invalid, even if it seems to be working at first. Another observation from the profile display is that we have more profiles than needed over a lot of the shape, so if the model is slow, using fewer profiles in the flat portion of the curve might speed up the calculation.
1505// path = [for(theta=[-180:5:180]) [theta/10, 10*sin(theta)]];
1506// sq = square(6);
1507// path_sweep(sq,path,profiles=true,width=.1,$fn=8);
1508// Example(2D): We'll use this shape in several examples
1509// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1510// polygon(ushape);
1511// Example(NoScales): Sweep along a clockwise elliptical arc, using default "incremental" method.
1512// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1513// elliptic_arc = xscale(2, p=arc($fn=64,angle=[180,00], r=30)); // Clockwise
1514// path_sweep(ushape, path3d(elliptic_arc));
1515// Example(NoScales): Sweep along a counter-clockwise elliptical arc. Note that the orientation of the shape flips.
1516// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1517// elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=30)); // Counter-clockwise
1518// path_sweep(ushape, path3d(elliptic_arc));
1519// Example(NoScales): Sweep along a clockwise elliptical arc, using "natural" method, which lines up the X axis of the shape with the direction of curvature. This means the X axis will point inward, so a counterclockwise arc gives:
1520// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1521// elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=30)); // Counter-clockwise
1522// path_sweep(ushape, elliptic_arc, method="natural");
1523// Example(NoScales): Sweep along a clockwise elliptical arc, using "natural" method. If the curve is clockwise then the shape flips upside-down to align the X axis.
1524// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1525// elliptic_arc = xscale(2, p=arc($fn=64,angle=[180,0], r=30)); // Clockwise
1526// path_sweep(ushape, path3d(elliptic_arc), method="natural");
1527// Example(NoScales): Sweep along a clockwise elliptical arc, using "manual" method. You can orient the shape in a direction you choose (subject to the constraint that the profiles remain normal to the path):
1528// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1529// elliptic_arc = xscale(2, p=arc($fn=64,angle=[180,0], r=30)); // Clockwise
1530// path_sweep(ushape, path3d(elliptic_arc), method="manual", normal=UP+RIGHT);
1531// Example(NoScales): Here we changed the ellipse to be more pointy, and with the same results as above we get a shape with an irregularity in the middle where it maintains the specified direction around the point of the ellipse. If the ellipse were more pointy, this would result in a bad polyhedron:
1532// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1533// elliptic_arc = yscale(2, p=arc($fn=64,angle=[180,0], r=30)); // Clockwise
1534// path_sweep(ushape, path3d(elliptic_arc), method="manual", normal=UP+RIGHT);
1535// Example(NoScales): It is easy to produce an invalid shape when your path has a smaller radius of curvature than the width of your shape. The exact threshold where the shape becomes invalid depends on the density of points on your path. The error may not be immediately obvious, as the swept shape appears fine when alone in your model, but adding a cube to the model reveals the problem. In this case the pentagon is turned so its longest direction points inward to create the singularity.
1536// qpath = [for(x=[-3:.01:3]) [x,x*x/1.8,0]];
1537// // Prints 0.9, but we use pentagon with radius of 1.0 > 0.9
1538// echo(radius_of_curvature = 1/max(path_curvature(qpath)));
1539// path_sweep(apply(rot(90),pentagon(r=1)), qpath, normal=BACK, method="manual");
1540// cube(0.5); // Adding a small cube forces a CGAL computation which reveals
1541// // the error by displaying nothing or giving a cryptic message
1542// Example(NoScales): Using the `relax` option we allow the profiles to deviate from orthogonality to the path. This eliminates the crease that broke the previous example because the sections are all parallel to each other.
1543// qpath = [for(x=[-3:.01:3]) [x,x*x/1.8,0]];
1544// path_sweep(apply(rot(90),pentagon(r=1)), qpath, normal=BACK, method="manual", relaxed=true);
1545// cube(0.5); // Adding a small cube is not a problem with this valid model
1546// Example(Med,VPR=[16,0,100],VPT=[0.05,0.6,0.6],VPD=25,NoScales): Using the `profiles=true` option can help debug bad polyhedra such as this one. If any of the profiles intersect or cross each other, the polyhedron will be invalid. In this case, you can see these intersections in the middle of the shape, which may give insight into how to fix your shape. The profiles may also help you identify cases with a valid polyhedron where you have more profiles than needed to adequately define the shape.
1547// tri= scale([4.5,2.5],[[0, 0], [0, 1], [1, 0]]);
1548// path = left(4,xscale(1.5,arc(r=5,n=25,angle=[-70,70])));
1549// path_sweep(tri,path,profiles=true,width=.1);
1550// Example(NoScales): This 3d arc produces a result that twists to an undefined angle. By default the incremental method sets the starting normal to UP, but the ending normal is unconstrained.
1551// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1552// arc = yrot(37, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1553// path_sweep(ushape, arc, method="incremental");
1554// Example(NoScales): You can constrain the last normal as well. Here we point it right, which produces a nice result.
1555// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1556// arc = yrot(37, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1557// path_sweep(ushape, arc, method="incremental", last_normal=RIGHT);
1558// Example(NoScales): Here we constrain the last normal to UP. Be aware that the behavior in the middle is unconstrained.
1559// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1560// arc = yrot(37, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1561// path_sweep(ushape, arc, method="incremental", last_normal=UP);
1562// Example(NoScales): The "natural" method produces a very different result
1563// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1564// arc = yrot(37, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1565// path_sweep(ushape, arc, method="natural");
1566// Example(NoScales): When the path starts at an angle of more that 45 deg to the xy plane the initial normal for "incremental" is BACK. This produces the effect of the shape rising up out of the xy plane. (Using UP for a vertical path is invalid, hence the need for a split in the defaults.)
1567// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1568// arc = xrot(75, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1569// path_sweep(ushape, arc, method="incremental");
1570// Example(NoScales): Adding twist
1571// // Counter-clockwise
1572// elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=3));
1573// path_sweep(pentagon(r=1), path3d(elliptic_arc), twist=72);
1574// Example(NoScales): Closed shape
1575// ellipse = xscale(2, p=circle($fn=64, r=3));
1576// path_sweep(pentagon(r=1), path3d(ellipse), closed=true);
1577// Example(NoScales): Closed shape with added twist
1578// ellipse = xscale(2, p=circle($fn=64, r=3));
1579// // Looks better with finer sampling
1580// pentagon = subdivide_path(pentagon(r=1), 30);
1581// path_sweep(pentagon, path3d(ellipse),
1582// closed=true, twist=360);
1583// Example(NoScales): The last example was a lot of twist. In order to use less twist you have to tell `path_sweep` that your shape has symmetry, in this case 5-fold. Mobius strip with pentagon cross section:
1584// ellipse = xscale(2, p=circle($fn=64, r=3));
1585// // Looks better with finer sampling
1586// pentagon = subdivide_path(pentagon(r=1), 30);
1587// path_sweep(pentagon, path3d(ellipse), closed=true,
1588// symmetry = 5, twist=2*360/5);
1589// Example(Med,NoScales): A helical path reveals the big problem with the "incremental" method: it can introduce unexpected and extreme twisting. (Note helix example came from list-comprehension-demos)
1590// function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1591// (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1592// 200 * (1 - t)];
1593// helix_steps = 200;
1594// helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1595// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1596// path_sweep(ushape, helix);
1597// Example(Med,NoScales): You can constrain both ends, but still the twist remains:
1598// function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1599// (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1600// 200 * (1 - t)];
1601// helix_steps = 200;
1602// helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1603// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1604// path_sweep(ushape, helix, normal=UP, last_normal=UP);
1605// Example(Med,NoScales): Even if you manually guess the amount of twist and remove it, the result twists one way and then the other:
1606// function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1607// (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1608// 200 * (1 - t)];
1609// helix_steps = 200;
1610// helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1611// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1612// path_sweep(ushape, helix, normal=UP, last_normal=UP, twist=360);
1613// Example(Med,NoScales): To get a good result you must use a different method.
1614// function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1615// (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1616// 200 * (1 - t)];
1617// helix_steps = 200;
1618// helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1619// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1620// path_sweep(ushape, helix, method="natural");
1621// Example(Med,NoScales): Note that it may look like the shape above is flat, but the profiles are very slightly tilted due to the nonzero torsion of the curve. If you want as flat as possible, specify it so with the "manual" method:
1622// function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1623// (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1624// 200 * (1 - t)];
1625// helix_steps = 200;
1626// helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1627// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1628// path_sweep(ushape, helix, method="manual", normal=UP);
1629// Example(Med,NoScales): What if you want to angle the shape inward? This requires a different normal at every point in the path:
1630// function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1631// (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1632// 200 * (1 - t)];
1633// helix_steps = 200;
1634// helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1635// normals = [for(i=[0:helix_steps]) [-cos(6*360*i/helix_steps), -sin(6*360*i/helix_steps), 2.5]];
1636// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1637// path_sweep(ushape, helix, method="manual", normal=normals);
1638// Example(NoScales): When using "manual" it is important to choose a normal that works for the whole path, producing a consistent result. Here we have specified an upward normal, and indeed the shape is pointed up everywhere, but two abrupt transitional twists render the model invalid.
1639// yzcircle = yrot(90,p=path3d(circle($fn=64, r=30)));
1640// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1641// path_sweep(ushape, yzcircle, method="manual", normal=UP, closed=true);
1642// Example(NoScales): The "natural" method will introduce twists when the curvature changes direction. A warning is displayed.
1643// arc1 = path3d(arc(angle=90, r=30));
1644// arc2 = xrot(-90, cp=[0,30],p=path3d(arc(angle=[90,180], r=30)));
1645// two_arcs = path_merge_collinear(concat(arc1,arc2));
1646// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1647// path_sweep(ushape, two_arcs, method="natural");
1648// Example(NoScales): The only simple way to get a good result is the "incremental" method:
1649// arc1 = path3d(arc(angle=90, r=30));
1650// arc2 = xrot(-90, cp=[0,30],p=path3d(arc(angle=[90,180], r=30)));
1651// arc3 = apply( translate([-30,60,30])*yrot(90), path3d(arc(angle=[270,180], r=30)));
1652// three_arcs = path_merge_collinear(concat(arc1,arc2,arc3));
1653// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1654// path_sweep(ushape, three_arcs, method="incremental");
1655// Example(Med,NoScales): knot example from list-comprehension-demos, "incremental" method
1656// function knot(a,b,t) = // rolling knot
1657// [ a * cos (3 * t) / (1 - b* sin (2 *t)),
1658// a * sin( 3 * t) / (1 - b* sin (2 *t)),
1659// 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))];
1660// a = 0.8; b = sqrt (1 - a * a);
1661// ksteps = 400;
1662// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)];
1663// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1664// path_sweep(ushape, knot_path, closed=true, method="incremental");
1665// Example(Med,NoScales): knot example from list-comprehension-demos, "natural" method. Which one do you like better?
1666// function knot(a,b,t) = // rolling knot
1667// [ a * cos (3 * t) / (1 - b* sin (2 *t)),
1668// a * sin( 3 * t) / (1 - b* sin (2 *t)),
1669// 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))];
1670// a = 0.8; b = sqrt (1 - a * a);
1671// ksteps = 400;
1672// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)];
1673// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1674// path_sweep(ushape, knot_path, closed=true, method="natural");
1675// Example(Med,NoScales): knot with twist. Note if you twist it the other direction the center section untwists because of the natural twist there. Also compare to the "incremental" method which has less twist in the center.
1676// function knot(a,b,t) = // rolling knot
1677// [ a * cos (3 * t) / (1 - b* sin (2 *t)),
1678// a * sin( 3 * t) / (1 - b* sin (2 *t)),
1679// 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))];
1680// a = 0.8; b = sqrt (1 - a * a);
1681// ksteps = 400;
1682// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)];
1683// path_sweep(subdivide_path(pentagon(r=12),30), knot_path, closed=true,
1684// twist=-360*8, symmetry=5, method="natural");
1685// Example(Med,NoScales): twisted knot with twist distributed by path sample points instead of by length using `twist_by_length=false`
1686// function knot(a,b,t) = // rolling knot
1687// [ a * cos (3 * t) / (1 - b* sin (2 *t)),
1688// a * sin( 3 * t) / (1 - b* sin (2 *t)),
1689// 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))];
1690// a = 0.8; b = sqrt (1 - a * a);
1691// ksteps = 400;
1692// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)];
1693// path_sweep(subdivide_path(pentagon(r=12),30), knot_path, closed=true,
1694// twist=-360*8, symmetry=5, method="natural", twist_by_length=false);
1695// Example(Big,NoScales): This torus knot example comes from list-comprehension-demos. The knot lies on the surface of a torus. When we use the "natural" method the swept figure is angled compared to the surface of the torus because the curve doesn't follow geodesics of the torus.
1696// function knot(phi,R,r,p,q) =
1697// [ (r * cos(q * phi) + R) * cos(p * phi),
1698// (r * cos(q * phi) + R) * sin(p * phi),
1699// r * sin(q * phi) ];
1700// ushape = 3*[[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1701// points = 50; // points per loop
1702// R = 400; r = 150; // Torus size
1703// p = 2; q = 5; // Knot parameters
1704// %torus(r_maj=R,r_min=r);
1705// k = max(p,q) / gcd(p,q) * points;
1706// knot_path = [ for (i=[0:k-1]) knot(360*i/k/gcd(p,q),R,r,p,q) ];
1707// path_sweep(rot(90,p=ushape),knot_path, method="natural", closed=true);
1708// Example(Big,NoScales): By computing the normal to the torus at the path we can orient the path to lie on the surface of the torus:
1709// function knot(phi,R,r,p,q) =
1710// [ (r * cos(q * phi) + R) * cos(p * phi),
1711// (r * cos(q * phi) + R) * sin(p * phi),
1712// r * sin(q * phi) ];
1713// function knot_normal(phi,R,r,p,q) =
1714// knot(phi,R,r,p,q)
1715// - R*unit(knot(phi,R,r,p,q)
1716// - [0,0, knot(phi,R,r,p,q)[2]]) ;
1717// ushape = 3*[[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]];
1718// points = 50; // points per loop
1719// R = 400; r = 150; // Torus size
1720// p = 2; q = 5; // Knot parameters
1721// %torus(r_maj=R,r_min=r);
1722// k = max(p,q) / gcd(p,q) * points;
1723// knot_path = [ for (i=[0:k-1]) knot(360*i/k/gcd(p,q),R,r,p,q) ];
1724// normals = [ for (i=[0:k-1]) knot_normal(360*i/k/gcd(p,q),R,r,p,q) ];
1725// path_sweep(ushape,knot_path,normal=normals, method="manual", closed=true);
1726// Example(NoScales): You can request the transformations and manipulate them before passing them on to sweep. Here we construct a tube that changes scale by first generating the transforms and then applying the scale factor and connecting the inside and outside. Note that the wall thickness varies because it is produced by scaling.
1727// shape = star(n=5, r=10, ir=5);
1728// rpath = arc(25, points=[[29,6,-4], [3,4,6], [1,1,7]]);
1729// trans = path_sweep(shape, rpath, transforms=true);
1730// outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))];
1731// inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))];
1732// sweep(shape, concat(outside,inside),closed=true);
1733// Example(NoScales): An easier way to scale your model is to use the scale parameter.
1734// elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=3));
1735// path_sweep(pentagon(r=1), path3d(elliptic_arc), scale=2);
1736// Example(NoScales): Scaling only in the y direction of the profile (z direction in the model in this case)
1737// elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=3));
1738// path_sweep(rect(2), path3d(elliptic_arc), scale=[1,2]);
1739// Example(NoScales): Specifying scale at every point for a closed path
1740// N=64;
1741// path = circle(r=5, $fn=64);
1742// theta = lerpn(0,360,N,endpoint=false);
1743// scale = [for(t=theta) sin(6*t)/5+1];
1744// path_sweep(rect(2), path3d(path), closed=true, scale=scale);
1745// Example(Med,NoScales): Using path_sweep on a region
1746// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
1747// rgn2 = [square(30,center=false)];
1748// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
1749// mrgn = union(rgn1,rgn2);
1750// orgn = difference(mrgn,rgn3);
1751// path_sweep(orgn,arc(r=40,angle=180));
1752// Example(Med,NoScales): A region with a twist
1753// region = [for(i=pentagon(5)) move(i,p=circle(r=2,$fn=25))];
1754// path_sweep(region,
1755// circle(r=16,$fn=75),closed=true,
1756// twist=360/5*2,symmetry=5);
1757// Example(Med,NoScales): Cutting a cylinder with a curved path. Note that in this case, the incremental method produces just a slight twist but the natural method produces an extreme twist. But manual specification produces no twist, as desired:
1758// $fn=90;
1759// r=8;
1760// thickness=1;
1761// len=21;
1762// curve = [for(theta=[0:4:359])
1763// [r*cos(theta), r*sin(theta), 10+sin(6*theta)]];
1764// difference(){
1765// cylinder(r=r, h=len);
1766// down(.5)cylinder(r=r-thickness, h=len+1);
1767// path_sweep(left(.05,square([1.1,1])), curve, closed=true,
1768// method="manual", normal=UP);
1769// }
1770// Example(Med,NoScales,VPR=[78.1,0,43.2],VPT=[2.18042,-0.485127,1.90371],VPD=74.4017): The "start" and "end" anchors are located at the origin point of the swept shape.
1771// shape = back_half(right_half(star(n=5,id=5,od=10)),y=-1);
1772// path = arc(angle=[0,180],d=30);
1773// path_sweep(shape,path,method="natural"){
1774// attach(["start","end"]) anchor_arrow(s=5);
1775// }
1776// Example(Med,NoScales,VPR=[78.1,0,43.2],VPT=[2.18042,-0.485127,1.90371],VPD=74.4017): The "start" and "end" anchors are located at the origin point of the swept shape.
1777// shape = back_half(right_half(star(n=5,id=5,od=10)),y=-1);
1778// path = arc(angle=[0,180],d=30);
1779// path_sweep(shape,path,method="natural"){
1780// attach(["start-centroid","end-centroid"]) anchor_arrow(s=5);
1781// }
1782// Example(Med,NoScales,VPR=[78.1,0,43.2],VPT=[2.18042,-0.485127,1.90371],VPD=74.4017): Note that the "start" anchors are backwards compared to the direction of the sweep, so you have to attach the TOP to align the shape with its ends.
1783// shape = back_half(right_half(star(n=5,id=5,od=10)),y=-1)[0];
1784// path = arc(angle=[0,180],d=30);
1785// path_sweep(shape,path,method="natural",scale=[1,1.5])
1786// recolor("red"){
1787// attach("start",TOP) stroke([path3d(shape)],width=.5);
1788// attach("end") stroke([path3d(yscale(1.5,shape))],width=.5);
1789// }
1790
1791module path_sweep(shape, path, method="incremental", normal, closed, twist=0, twist_by_length=true, scale=1, scale_by_length=true,
1792 symmetry=1, last_normal, tangent, uniform=true, relaxed=false, caps, style="min_edge", convexity=10,
1793 anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull",profiles=false,width=1)
1794{
1795 dummy = assert(is_region(shape) || is_path(shape,2), "shape must be a 2D path or region")
1796 assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"");
1797 trans_scale = path_sweep(shape, path, method, normal, closed, twist, twist_by_length, scale, scale_by_length,
1798 symmetry, last_normal, tangent, uniform, relaxed, caps, style, transforms=true,_return_scales=true);
1799 transforms = trans_scale[0];
1800 scales = trans_scale[1];
1801 firstscale = is_num(scales[0]) ? 1/scales[0] : [1/scales[0].x, 1/scales[0].y];
1802 lastscale = is_num(last(scales)) ? 1/last(scales) : [1/last(scales).x, 1/last(scales).y];
1803 vnf = sweep(is_path(shape)?clockwise_polygon(shape):shape, transforms, closed=false, caps=caps,style=style);
1804 shapecent = point3d(centroid(shape));
1805 $transforms = transforms;
1806 $scales = scales;
1807 anchors = closed ? []
1808 :
1809 [
1810 named_anchor("start", rot=transforms[0]*scale(firstscale), flip=true),
1811 named_anchor("end", rot=last(transforms)*scale(lastscale)),
1812 named_anchor("start-centroid", rot=transforms[0]*move(shapecent)*scale(firstscale), flip=true),
1813 named_anchor("end-centroid", rot=last(transforms)*move(shapecent)*scale(lastscale))
1814 ];
1815 if (profiles){
1816 rshape = is_path(shape) ? [path3d(shape)]
1817 : [for(s=shape) path3d(s)];
1818 attachable(anchor,spin,orient, vnf=vnf, extent=atype=="hull", cp=cp, anchors=anchors) {
1819 for(T=transforms) stroke([for(part=rshape)apply(T,part)],width=width);
1820 children();
1821 }
1822 }
1823 else
1824 attachable(anchor,spin,orient,vnf=vnf,extent=atype=="hull", cp=cp,anchors=anchors){
1825 vnf_polyhedron(vnf,convexity=convexity);
1826 children();
1827 }
1828}
1829
1830
1831function path_sweep(shape, path, method="incremental", normal, closed, twist=0, twist_by_length=true, scale=1, scale_by_length=true,
1832 symmetry=1, last_normal, tangent, uniform=true, relaxed=false, caps, style="min_edge", transforms=false,
1833 anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull",_return_scales=false) =
1834 is_1region(path) ? path_sweep(shape=shape,path=path[0], method=method, normal=normal, closed=default(closed,true),
1835 twist=twist, scale=scale, scale_by_length=scale_by_length, twist_by_length=twist_by_length, symmetry=symmetry, last_normal=last_normal,
1836 tangent=tangent, uniform=uniform, relaxed=relaxed, caps=caps, style=style, transforms=transforms,
1837 anchor=anchor, cp=cp, spin=spin, orient=orient, atype=atype, _return_scales=_return_scales) :
1838 let(closed=default(closed,false))
1839 assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"")
1840 assert(!closed || twist % (360/symmetry)==0, str("For a closed sweep, twist must be a multiple of 360/symmetry = ",360/symmetry))
1841 assert(closed || symmetry==1, "symmetry must be 1 when closed is false")
1842 assert(is_integer(symmetry) && symmetry>0, "symmetry must be a positive integer")
1843 let(path = force_path(path))
1844 assert(is_path(path,[2,3]), "input path is not a 2D or 3D path")
1845 assert(!closed || !approx(path[0],last(path)), "Closed path includes start point at the end")
1846 assert((is_region(shape) || is_path(shape,2)) || (transforms && !(closed && method=="incremental")),"shape must be a 2d path or region")
1847 let(
1848 path = path3d(path),
1849 caps = is_def(caps) ? caps :
1850 closed ? false : true,
1851 capsOK = is_bool(caps) || is_bool_list(caps,2),
1852 fullcaps = is_bool(caps) ? [caps,caps] : caps,
1853 normalOK = is_undef(normal) || (method!="natural" && is_vector(normal,3))
1854 || (method=="manual" && same_shape(normal,path)),
1855 scaleOK = scale==1 || ((is_num(scale) || is_vector(scale,2)) && !closed) || is_vector(scale,len(path)) || is_matrix(scale,len(path),2)
1856
1857 )
1858 assert(normalOK, method=="natural" ? "Cannot specify normal with the \"natural\" method"
1859 : method=="incremental" ? "Normal with \"incremental\" method must be a 3-vector"
1860 : str("Incompatible normal given. Must be a 3-vector or a list of ",len(path)," 3-vectors"))
1861 assert(capsOK, "caps must be boolean or a list of two booleans")
1862 assert(!closed || !caps, "Cannot make closed shape with caps")
1863 assert(is_undef(normal) || (is_vector(normal) && len(normal)==3) || (is_path(normal) && len(normal)==len(path) && len(normal[0])==3), "Invalid normal specified")
1864 assert(is_undef(tangent) || (is_path(tangent) && len(tangent)==len(path) && len(tangent[0])==3), "Invalid tangent specified")
1865 assert(scaleOK,str("Incompatible or invalid scale",closed?" for closed path":"",": must be ", closed?"":"a scalar, a 2-vector, ",
1866 "a vector of length ",len(path)," or a ",len(path),"x2 matrix of scales"))
1867 let(
1868 scale = !(is_num(scale) || is_vector(scale,2)) ? scale
1869 : let(s=is_num(scale) ? [scale,scale] : scale)
1870 !scale_by_length ? lerpn([1,1],s,len(path))
1871 : lerp([1,1],s, path_length_fractions(path,false)),
1872 scale_list = [for(s=scale) scale(s),if (closed) scale(scale[0])],
1873 tangents = is_undef(tangent) ? path_tangents(path,uniform=uniform,closed=closed) : [for(t=tangent) unit(t)],
1874 normal = is_path(normal) ? [for(n=normal) unit(n)] :
1875 is_def(normal) ? unit(normal) :
1876 method =="incremental" && abs(tangents[0].z) > 1/sqrt(2) ? BACK : UP,
1877 normals = is_path(normal) ? normal : repeat(normal,len(path)),
1878 tpathfrac = twist_by_length ? path_length_fractions(path, closed) : [for(i=[0:1:len(path)]) i / (len(path)-(closed?0:1))],
1879 spathfrac = scale_by_length ? path_length_fractions(path, closed) : [for(i=[0:1:len(path)]) i / (len(path)-(closed?0:1))],
1880 L = len(path),
1881 unscaled_transform_list =
1882 method=="old_incremental" ?
1883 let(rotations =
1884 [for( i = 0,
1885 ynormal = normal - (normal * tangents[0])*tangents[0],
1886 rotation = frame_map(y=ynormal, z=tangents[0])
1887 ;
1888 i < len(tangents) + (closed?1:0)
1889 ;
1890 rotation = i<len(tangents)-1+(closed?1:0)? rot(from=tangents[i],to=tangents[(i+1)%L])*rotation : undef,
1891 i=i+1
1892 )
1893 rotation],
1894 // The mismatch is the inverse of the last transform times the first one for the closed case, or the inverse of the
1895 // desired final transform times the realized final transform in the open case. Note that when closed==true the last transform
1896 // is a actually looped around and applies to the first point position, so if we got back exactly where we started
1897 // then it will be the identity, but we might have accumulated some twist which will show up as a rotation around the
1898 // X axis. Similarly, in the closed==false case the desired and actual transformations can only differ in the twist,
1899 // so we can need to calculate the twist angle so we can apply a correction, which we distribute uniformly over the whole path.
1900 reference_rot = closed ? rotations[0] :
1901 is_undef(last_normal) ? last(rotations) :
1902 let(
1903 last_tangent = last(tangents),
1904 lastynormal = last_normal - (last_normal * last_tangent) * last_tangent
1905 )
1906 frame_map(y=lastynormal, z=last_tangent),
1907 mismatch = transpose(last(rotations)) * reference_rot,
1908 correction_twist = atan2(mismatch[1][0], mismatch[0][0]),
1909 // Spread out this extra twist over the whole sweep so that it doesn't occur
1910 // abruptly as an artifact at the last step.
1911 twistfix = correction_twist%(360/symmetry),
1912 adjusted_final = !closed ? undef :
1913 translate(path[0]) * rotations[0] * zrot(-correction_twist+correction_twist%(360/symmetry)-twist)
1914 ) [for(i=idx(path)) translate(path[i]) * rotations[i] * zrot((twistfix-twist)*tpathfrac[i]), if(closed) adjusted_final]
1915 : method=="incremental" ? // Implements Rotation Minimizing Frame from "Computation of Rotation Minimizing Frames"
1916 // by Wenping Yang, Bert Büttler, Dayue Zheng, Yang Liu, 2008
1917 // http://doi.acm.org/10.1145/1330511.1330513
1918 let(rotations = // https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf
1919 [for( i = 0,
1920 ynormal = normal - (normal * tangents[0])*tangents[0],
1921 rotation = frame_map(y=ynormal, z=tangents[0]),
1922 r=ynormal
1923 ;
1924 i < len(tangents) + (closed?1:0)
1925 ;
1926 v1 = path[(i+1)%L]-path[i%L],
1927 c1 = v1*v1,
1928 rL = r - 2*(v1*r)/c1 * v1,
1929 tL = tangents[i%L] - 2*(v1*tangents[i%L])/c1 * v1,
1930 v2 = tangents[(i+1)%L]-tL,
1931 c2 = v2*v2,
1932 r = rL - (2/c2)*(v2*rL)*v2,
1933 rotation = i<len(tangents)-1+(closed?1:0)? frame_map(y=r,z=tangents[(i+1)%L]) : undef,
1934 i=i+1
1935 )
1936 rotation],
1937 // The mismatch is the inverse of the last transform times the first one for the closed case, or the inverse of the
1938 // desired final transform times the realized final transform in the open case. Note that when closed==true the last transform
1939 // is a actually looped around and applies to the first point position, so if we got back exactly where we started
1940 // then it will be the identity, but we might have accumulated some twist which will show up as a rotation around the
1941 // X axis. Similarly, in the closed==false case the desired and actual transformations can only differ in the twist,
1942 // so we can need to calculate the twist angle so we can apply a correction, which we distribute uniformly over the whole path.
1943 reference_rot = closed ? rotations[0] :
1944 is_undef(last_normal) ? last(rotations) :
1945 let(
1946 last_tangent = last(tangents),
1947 lastynormal = last_normal - (last_normal * last_tangent) * last_tangent
1948 )
1949 frame_map(y=lastynormal, z=last_tangent),
1950 mismatch = transpose(last(rotations)) * reference_rot,
1951 correction_twist = atan2(mismatch[1][0], mismatch[0][0]),
1952 // Spread out this extra twist over the whole sweep so that it doesn't occur
1953 // abruptly as an artifact at the last step.
1954 twistfix = correction_twist%(360/symmetry),
1955 adjusted_final = !closed ? undef :
1956 translate(path[0]) * rotations[0] * zrot(-correction_twist+correction_twist%(360/symmetry)-twist)
1957 ) [for(i=idx(path)) translate(path[i]) * rotations[i] * zrot((twistfix-twist)*tpathfrac[i]), if(closed) adjusted_final]
1958 : method=="manual" ?
1959 [for(i=[0:L-(closed?0:1)]) let(
1960 ynormal = relaxed ? normals[i%L] : normals[i%L] - (normals[i%L] * tangents[i%L])*tangents[i%L],
1961 znormal = relaxed ? tangents[i%L] - (normals[i%L] * tangents[i%L])*normals[i%L] : tangents[i%L],
1962 rotation = frame_map(y=ynormal, z=znormal)
1963 )
1964 assert(approx(ynormal*znormal,0),str("Supplied normal is parallel to the path tangent at point ",i))
1965 translate(path[i%L])*rotation*zrot(-twist*tpathfrac[i])
1966 ]
1967 : method=="natural" ? // map x axis of shape to the path normal, which points in direction of curvature
1968 let (pathnormal = path_normals(path, tangents, closed))
1969 assert(all_defined(pathnormal),"Natural normal vanishes on your curve, select a different method")
1970 let( testnormals = [for(i=[0:len(pathnormal)-1-(closed?1:2)]) pathnormal[i]*select(pathnormal,i+2)],
1971 a=[for(i=idx(testnormals)) testnormals[i]<.5 ? echo(str("Big change at index ",i," pn=",pathnormal[i]," pn2= ",select(pathnormal,i+2))):0],
1972 dummy = min(testnormals) < .5 ? echo("WARNING: ***** Abrupt change in normal direction. Consider a different method in path_sweep() *****") :0
1973 )
1974 [for(i=[0:L-(closed?0:1)]) let(
1975 rotation = frame_map(x=pathnormal[i%L], z=tangents[i%L])
1976 )
1977 translate(path[i%L])*rotation*zrot(-twist*tpathfrac[i])
1978 ]
1979 : assert(false,"Unknown method or no method given"), // unknown method
1980 transform_list = v_mul(unscaled_transform_list, scale_list),
1981 ends_match = !closed ? true
1982 : let( rshape = is_path(shape) ? [path3d(shape)]
1983 : [for(s=shape) path3d(s)]
1984 )
1985 are_regions_equal(apply(transform_list[0], rshape),
1986 apply(transform_list[L], rshape)),
1987 dummy = ends_match ? 0 : echo("WARNING: ***** The points do not match when closing the model in path_sweep() *****")
1988 )
1989 transforms && _return_scales
1990 ? [transform_list,scale]
1991: transforms ? transform_list
1992 : sweep(is_path(shape)?clockwise_polygon(shape):shape, transform_list, closed=false, caps=fullcaps,style=style,
1993 anchor=anchor,cp=cp,spin=spin,orient=orient,atype=atype);
1994
1995
1996// Function&Module: path_sweep2d()
1997// Synopsis: Sweep a 2d polygon path along a 2d path allowing self-intersection.
1998// SynTags: VNF, Geom
1999// Topics: Extrusion, Sweep, Paths
2000// See Also: linear_sweep(), rotate_sweep(), sweep(), spiral_sweep(), path_sweep(), offset_sweep()
2001// Usage: as module
2002// path_sweep2d(shape, path, [closed], [caps], [quality], [style], [convexity=], [anchor=], [spin=], [orient=], [atype=], [cp=]) [ATTACHMENTS];
2003// Usage: as function
2004// vnf = path_sweep2d(shape, path, [closed], [caps], [quality], [style], [anchor=], [spin=], [orient=], [atype=], [cp=]);
2005// Description:
2006// Takes an input 2D polygon (the shape) and a 2d path, and constructs a polyhedron by sweeping the shape along the path.
2007// When run as a module returns the polyhedron geometry. When run as a function returns a VNF.
2008// .
2009// See {{path_sweep()}} for more details on how the sweep operation works and for introductory examples.
2010// This 2d version is different because local self-intersections (creases in the output) are allowed and do not produce CGAL errors.
2011// This is accomplished by using offset() calculations, which are more expensive than simply copying the shape along
2012// the path, so if you do not have local self-intersections, use {{path_sweep()}} instead. If xmax is the largest x value (in absolute value)
2013// of the shape, then path_sweep2d() will work as long as the offset of `path` exists at `delta=xmax`. If the offset vanishes, as in the
2014// case of a circle offset by more than its radius, then you will get an error about a degenerate offset.
2015// Note that global self-intersections will still give rise to CGAL errors. You should be able to handle these by partitioning your model. The y axis of the
2016// shape is mapped to the z axis in the swept polyhedron, and no twisting can occur.
2017// The quality parameter is passed to offset to determine the offset quality.
2018// Arguments:
2019// shape = a 2D polygon describing the shape to be swept
2020// path = a 2D path giving the path to sweep over
2021// closed = path is a closed loop. Default: false
2022// caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false.
2023// quality = quality of offset used in calculation. Default: 1
2024// style = vnf_vertex_array style. Default: "min_edge"
2025// ---
2026// convexity = convexity parameter for polyhedron (module only) Default: 10
2027// anchor = Translate so anchor point is at the origin. Default: "origin"
2028// spin = Rotate this many degrees around Z axis after anchor. Default: 0
2029// orient = Vector to rotate top towards after spin
2030// atype = Select "hull" or "intersect" anchor types. Default: "hull"
2031// 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"
2032// Anchor Types:
2033// "hull" = Anchors to the virtual convex hull of the shape.
2034// "intersect" = Anchors to the surface of the shape.
2035// Example: Sine wave example with self-intersections at each peak. This would fail with path_sweep().
2036// sinewave = [for(i=[-30:10:360*2+30]) [i/40,3*sin(i)]];
2037// path_sweep2d(circle(r=3,$fn=15), sinewave);
2038// Example: The ends can look weird if they are in a place where self intersection occurs. This is a natural result of how offset behaves at ends of a path.
2039// coswave = [for(i=[0:10:360*1.5]) [i/40,3*cos(i)]];
2040// zrot(-20)
2041// path_sweep2d( circle(r=3,$fn=15), coswave);
2042// Example: This closed path example works ok as long as the hole in the center remains open.
2043// ellipse = yscale(3,p=circle(r=3,$fn=120));
2044// path_sweep2d(circle(r=2.5,$fn=32), reverse(ellipse), closed=true);
2045// Example: When the hole is closed a global intersection renders the model invalid. You can fix this by taking the union of the two (valid) halves.
2046// ellipse = yscale(3,p=circle(r=3,$fn=120));
2047// L = len(ellipse);
2048// path_sweep2d(circle(r=3.25, $fn=32), select(ellipse,floor(L*.2),ceil(L*.8)),closed=false);
2049// path_sweep2d(circle(r=3.25, $fn=32), select(ellipse,floor(L*.7),ceil(L*.3)),closed=false);
2050
2051function path_sweep2d(shape, path, closed=false, caps, quality=1, style="min_edge",
2052 anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull") =
2053 let(
2054 caps = is_def(caps) ? caps
2055 : closed ? false : true,
2056 capsOK = is_bool(caps) || is_bool_list(caps,2),
2057 fullcaps = is_bool(caps) ? [caps,caps] : caps,
2058 shape = force_path(shape,"shape"),
2059 path = force_path(path)
2060 )
2061 assert(is_path(shape,2), "shape must be a 2D path")
2062 assert(is_path(path,2), "path must be a 2D path")
2063 assert(capsOK, "caps must be boolean or a list of two booleans")
2064 assert(!closed || !caps, "Cannot make closed shape with caps")
2065 let(
2066 profile = ccw_polygon(shape),
2067 flip = closed && is_polygon_clockwise(path) ? -1 : 1,
2068 path = flip ? reverse(path) : path,
2069 proflist= transpose(
2070 [for(pt = profile)
2071 let(
2072 ofs = offset(path, delta=-flip*pt.x, return_faces=true,closed=closed, quality=quality),
2073 map = column(_ofs_vmap(ofs,closed=closed),1)
2074 )
2075 select(path3d(ofs[0],pt.y),map)
2076 ]
2077 ),
2078 vnf = vnf_vertex_array([
2079 each proflist,
2080 if (closed) proflist[0]
2081 ],cap1=fullcaps[0],cap2=fullcaps[1],col_wrap=true,style=style)
2082 )
2083 reorient(anchor,spin,orient,vnf=vnf,p=vnf,extent=atype=="hull",cp=cp);
2084
2085
2086module path_sweep2d(profile, path, closed=false, caps, quality=1, style="min_edge", convexity=10,
2087 anchor="origin", cp="centroid", spin=0, orient=UP, atype="hull")
2088{
2089 vnf = path_sweep2d(profile, path, closed, caps, quality, style);
2090 vnf_polyhedron(vnf,convexity=convexity,anchor=anchor, spin=spin, orient=orient, atype=atype, cp=cp)
2091 children();
2092}
2093
2094// Extract vertex mapping from offset face list. The output of this function
2095// is a list of pairs [i,j] where i is an index into the parent curve and j is
2096// an index into the offset curve. It would probably make sense to rewrite
2097// offset() to return this instead of the face list and have offset_sweep
2098// use this input to assemble the faces it needs.
2099
2100function _ofs_vmap(ofs,closed=false) =
2101 let( // Caclulate length of the first (parent) curve
2102 firstlen = max(flatten(ofs[1]))+1-len(ofs[0])
2103 )
2104 [
2105 for(entry=ofs[1]) _ofs_face_edge(entry,firstlen),
2106 if (!closed) _ofs_face_edge(last(ofs[1]),firstlen,second=true)
2107 ];
2108
2109
2110// Extract first (default) or second edge that connects the parent curve to its offset. The first input
2111// face is a list of 3 or 4 vertices as indices into the two curves where the parent curve vertices are
2112// numbered from 0 to firstlen-1 and the offset from firstlen and up. The firstlen pararameter is used
2113// to determine which curve the vertices belong to and to remove the offset so that the return gives
2114// the index into each curve with a 0 base.
2115function _ofs_face_edge(face,firstlen,second=false) =
2116 let(
2117 itry = min_index(face),
2118 i = select(face,itry-1)<firstlen ? itry-1:itry,
2119 edge1 = select(face,[i,i-1]),
2120 edge2 = select(face,i+1)<firstlen ? select(face,[i+1,i+2])
2121 : select(face,[i,i+1])
2122 )
2123 (second ? edge2 : edge1)-[0,firstlen];
2124
2125
2126
2127// Function&Module: sweep()
2128// Synopsis: Construct a 3d object from arbitrary transformations of a 2d polygon path.
2129// SynTags: VNF, Geom
2130// Topics: Extrusion, Sweep, Paths
2131// See Also: linear_sweep(), rotate_sweep(), spiral_sweep(), path_sweep(), path_sweep2d(), offset_sweep()
2132// Usage: As Module
2133// sweep(shape, transforms, [closed], [caps], [style], [convexity=], [anchor=], [spin=], [orient=], [atype=]) [ATTACHMENTS];
2134// Usage: As Function
2135// vnf = sweep(shape, transforms, [closed], [caps], [style], [anchor=], [spin=], [orient=], [atype=]);
2136// Description:
2137// The input `shape` must be a non-self-intersecting 2D polygon or region, and `transforms`
2138// is a list of 4x4 transformation matrices. The sweep algorithm applies each transformation in sequence
2139// to the shape input and links the resulting polygons together to form a polyhedron.
2140// If `closed=true` then the first and last transformation are linked together.
2141// The `caps` parameter controls whether the ends of the shape are closed.
2142// As a function, returns the VNF for the polyhedron. As a module, computes the polyhedron.
2143// .
2144// Note that this is a very powerful, general framework for producing polyhedra. It is important
2145// to ensure that your resulting polyhedron does not include any self-intersections, or it will
2146// be invalid and will generate CGAL errors. If you get such errors, most likely you have an
2147// overlooked self-intersection. Note also that the errors will not occur when your shape is alone
2148// in your model, but will arise if you add a second object to the model. This may mislead you into
2149// thinking the second object caused a problem. Even adding a simple cube to the model will reveal the problem.
2150// Arguments:
2151// shape = 2d path or region, describing the shape to be swept.
2152// transforms = list of 4x4 matrices to apply
2153// closed = set to true to form a closed (torus) model. Default: false
2154// caps = true to create endcap faces when closed is false. Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array. Default is true if closed is false.
2155// style = vnf_vertex_array style. Default: "min_edge"
2156// ---
2157// convexity = convexity setting for use with polyhedron. (module only) Default: 10
2158// 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"
2159// atype = Select "hull" or "intersect" anchor types. Default: "hull"
2160// anchor = Translate so anchor point is at the origin. Default: "origin"
2161// spin = Rotate this many degrees around Z axis after anchor. Default: 0
2162// orient = Vector to rotate top towards after spin (module only)
2163// Anchor Types:
2164// "hull" = Anchors to the virtual convex hull of the shape.
2165// "intersect" = Anchors to the surface of the shape.
2166// Example(VPR=[45,0,74],VPD=175,VPT=[-3.8,12.4,19]): A bent object that also changes shape along its length.
2167// radius = 75;
2168// angle = 40;
2169// shape = circle(r=5,$fn=32);
2170// T = [for(i=[0:25]) xrot(-angle*i/25,cp=[0,radius,0])*scale([1+i/25, 2-i/25,1])];
2171// sweep(shape,T);
2172// Example: This is the "sweep-drop" example from list-comprehension-demos.
2173// function drop(t) = 100 * 0.5 * (1 - cos(180 * t)) * sin(180 * t) + 1;
2174// function path(t) = [0, 0, 80 + 80 * cos(180 * t)];
2175// function rotate(t) = 180 * pow((1 - t), 3);
2176// step = 0.01;
2177// path_transforms = [for (t=[0:step:1-step]) translate(path(t)) * zrot(rotate(t)) * scale([drop(t), drop(t), 1])];
2178// sweep(circle(1, $fn=12), path_transforms);
2179// Example: Another example from list-comprehension-demos
2180// function f(x) = 3 - 2.5 * x;
2181// function r(x) = 2 * 180 * x * x * x;
2182// pathstep = 1;
2183// height = 100;
2184// shape_points = subdivide_path(square(10),40,closed=true);
2185// path_transforms = [for (i=[0:pathstep:height]) let(t=i/height) up(i) * scale([f(t),f(t),i]) * zrot(r(t))];
2186// sweep(shape_points, path_transforms);
2187// Example: Twisted container. Note that this technique doesn't create a fixed container wall thickness.
2188// shape = subdivide_path(square(30,center=true), 40, closed=true);
2189// outside = [for(i=[0:24]) up(i)*rot(i)*scale(1.25*i/24+1)];
2190// inside = [for(i=[24:-1:2]) up(i)*rot(i)*scale(1.2*i/24+1)];
2191// sweep(shape, concat(outside,inside));
2192
2193function sweep(shape, transforms, closed=false, caps, style="min_edge",
2194 anchor="origin", cp="centroid", spin=0, orient=UP, atype="hull") =
2195 assert(is_consistent(transforms, ident(4)), "Input transforms must be a list of numeric 4x4 matrices in sweep")
2196 assert(is_path(shape,2) || is_region(shape), "Input shape must be a 2d path or a region.")
2197 let(
2198 caps = is_def(caps) ? caps :
2199 closed ? false : true,
2200 capsOK = is_bool(caps) || is_bool_list(caps,2),
2201 fullcaps = is_bool(caps) ? [caps,caps] : caps
2202 )
2203 assert(len(transforms)>=2, "transformation must be length 2 or more")
2204 assert(capsOK, "caps must be boolean or a list of two booleans")
2205 assert(!closed || !caps, "Cannot make closed shape with caps")
2206 is_region(shape)? let(
2207 regions = region_parts(shape),
2208 rtrans = reverse(transforms),
2209 vnfs = [
2210 for (rgn=regions) each [
2211 for (path=rgn)
2212 sweep(path, transforms, closed=closed, caps=false, style=style),
2213 if (fullcaps[0]) vnf_from_region(rgn, transform=transforms[0], reverse=true),
2214 if (fullcaps[1]) vnf_from_region(rgn, transform=last(transforms)),
2215 ],
2216 ],
2217 vnf = vnf_join(vnfs)
2218 ) vnf :
2219 assert(len(shape)>=3, "shape must be a path of at least 3 non-colinear points")
2220 vnf_vertex_array([for(i=[0:len(transforms)-(closed?0:1)]) apply(transforms[i%len(transforms)],path3d(shape))],
2221 cap1=fullcaps[0],cap2=fullcaps[1],col_wrap=true,style=style);
2222
2223
2224module sweep(shape, transforms, closed=false, caps, style="min_edge", convexity=10,
2225 anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull")
2226{
2227 vnf = sweep(shape, transforms, closed, caps, style);
2228 vnf_polyhedron(vnf, convexity=convexity, anchor=anchor, spin=spin, orient=orient, atype=atype, cp=cp)
2229 children();
2230}
2231
2232
2233
2234// Section: Functions for resampling and slicing profile lists
2235
2236// Function: subdivide_and_slice()
2237// Synopsis: Resample list of paths to have the same point count and interpolate additional paths.
2238// SynTags: PathList
2239// Topics: Paths, Path Subdivision
2240// See Also: slice_profiles()
2241// Usage:
2242// newprof = subdivide_and_slice(profiles, slices, [numpoints], [method], [closed]);
2243// Description:
2244// Subdivides the input profiles to have length `numpoints` where `numpoints` must be at least as
2245// big as the largest input profile. By default `numpoints` is set equal to the length of the
2246// largest profile. You can set `numpoints="lcm"` to sample to the least common multiple of all
2247// curves, which will avoid sampling artifacts but may produce a huge output. After subdivision,
2248// profiles are sliced.
2249// Arguments:
2250// profiles = profiles to operate on
2251// slices = number of slices to insert between each pair of profiles. May be a vector
2252// numpoints = number of points after sampling.
2253// method = method used for calling {{subdivide_path()}}, either `"length"` or `"segment"`. Default: `"length"`
2254// closed = the first and last profile are connected. Default: false
2255function subdivide_and_slice(profiles, slices, numpoints, method="length", closed=false) =
2256 let(
2257 maxsize = max_length(profiles),
2258 numpoints = is_undef(numpoints) ? maxsize :
2259 numpoints == "lcm" ? lcmlist([for(p=profiles) len(p)]) :
2260 is_num(numpoints) ? round(numpoints) : undef
2261 )
2262 assert(is_def(numpoints), "Parameter numpoints must be \"max\", \"lcm\" or a positive number")
2263 assert(numpoints>=maxsize, "Number of points requested is smaller than largest profile")
2264 let(fixpoly = [for(poly=profiles) subdivide_path(poly, numpoints,method=method)])
2265 slice_profiles(fixpoly, slices, closed);
2266
2267
2268
2269// Function: slice_profiles()
2270// Synopsis: Linearly interpolates between path profiles.
2271// SynTags: PathList
2272// Topics: Paths, Path Subdivision
2273// See Also: subdivide_and_slice()
2274// Usage:
2275// profs = slice_profiles(profiles, slices, [closed]);
2276// Description:
2277// Given an input list of profiles, linearly interpolate between each pair to produce a
2278// more finely sampled list. The parameters `slices` specifies the number of slices to
2279// be inserted between each pair of profiles and can be a number or a list.
2280// Arguments:
2281// profiles = list of paths to operate on. They must be lists of the same shape and length.
2282// slices = number of slices to insert between each pair, or a list to vary the number inserted.
2283// closed = set to true if last profile connects to first one. Default: false
2284function slice_profiles(profiles,slices,closed=false) =
2285 assert(is_num(slices) || is_list(slices))
2286 let(listok = !is_list(slices) || len(slices)==len(profiles)-(closed?0:1))
2287 assert(listok, "Input slices to slice_profiles is a list with the wrong length")
2288 let(
2289 count = is_num(slices) ? repeat(slices,len(profiles)-(closed?0:1)) : slices,
2290 slicelist = [for (i=[0:len(profiles)-(closed?1:2)])
2291 each lerpn(profiles[i], select(profiles,i+1), count[i]+1, false)
2292 ]
2293 )
2294 concat(slicelist, closed?[]:[profiles[len(profiles)-1]]);
2295
2296
2297
2298function _closest_angle(alpha,beta) =
2299 is_vector(beta) ? [for(entry=beta) _closest_angle(alpha,entry)]
2300 : beta-alpha > 180 ? beta - ceil((beta-alpha-180)/360) * 360
2301 : beta-alpha < -180 ? beta + ceil((alpha-beta-180)/360) * 360
2302 : beta;
2303
2304
2305// Smooth data with N point moving average. If angle=true handles data as angles.
2306// If closed=true assumes last point is adjacent to the first one.
2307// If closed=false pads data with left/right value (probably wrong behavior...should do linear interp)
2308function _smooth(data,len,closed=false,angle=false) =
2309 let( halfwidth = floor(len/2),
2310 result = closed ? [for(i=idx(data))
2311 let(
2312 window = angle ? _closest_angle(data[i],select(data,i-halfwidth,i+halfwidth))
2313 : select(data,i-halfwidth,i+halfwidth)
2314 )
2315 mean(window)]
2316 : [for(i=idx(data))
2317 let(
2318 window = select(data,max(i-halfwidth,0),min(i+halfwidth,len(data)-1)),
2319 left = i-halfwidth<0,
2320 pad = left ? data[0] : last(data)
2321 )
2322 sum(window)+pad*(len-len(window))] / len
2323 )
2324 result;
2325
2326
2327// Function: rot_resample()
2328// Synopsis: Resample a list of rotation operators.
2329// SynTags: MatList
2330// Topics: Matrices, Interpolation, Rotation
2331// See Also: subdivide_and_slice(), slice_profiles()
2332// Usage:
2333// rlist = rot_resample(rotlist, n, [method=], [twist=], [scale=], [smoothlen=], [long=], [turns=], [closed=])
2334// Description:
2335// Takes as input a list of rotation matrices in 3d. Produces as output a resampled
2336// list of rotation operators (4x4 matrixes) suitable for use with sweep(). You can optionally apply twist to
2337// the output with the twist parameter, which is either a scalar to apply a uniform
2338// overall twist, or a vector to apply twist non-uniformly. Similarly you can apply
2339// scaling either overall or with a vector. The smoothlen parameter applies smoothing
2340// to the twist and scaling to prevent abrupt changes. This is done by a moving average
2341// of the smoothing or scaling values. The default of 1 means no smoothing. The long parameter causes
2342// the interpolation to be done the "long" way around the rotation instead of the short way.
2343// Note that the rotation matrix cannot distinguish which way you rotate, only the place you
2344// end after rotation. Another ambiguity arises if your rotation is more than 360 degrees.
2345// You can add turns with the turns parameter, so giving turns=1 will add 360 degrees to the
2346// rotation so it completes one full turn plus the additional rotation given my the transform.
2347// You can give long as a scalar or as a vector. Finally if closed is true then the
2348// resampling will connect back to the beginning.
2349// .
2350// The default is to resample based on the length of the arc defined by each rotation operator. This produces
2351// uniform sampling over all of the transformations. It requires that each rotation has nonzero length.
2352// In this case n specifies the total number of samples. If you set method to "count" then you get
2353// n samples for each transform. You can set n to a vector to vary the samples at each step.
2354// Arguments:
2355// rotlist = list of rotation operators in 3d to resample
2356// n = Number of rotations to produce as output when method is "length" or number for each transformation if method is "count". Can be a vector when method is "count"
2357// ---
2358// method = sampling method, either "length" or "count"
2359// twist = scalar or vector giving twist to add overall or at each rotation. Default: none
2360// scale = scalar or vector giving scale factor to add overall or at each rotation. Default: none
2361// smoothlen = amount of smoothing to apply to scaling and twist. Should be an odd integer. Default: 1
2362// long = resample the "long way" around the rotation, a boolean or list of booleans. Default: false
2363// turns = add extra turns. If a scalar adds the turns to every rotation, or give a vector. Default: 0
2364// closed = if true then the rotation list is treated as closed. Default: false
2365// Example(3D): Resampling the arc from a compound rotation with translations thrown in.
2366// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], n=25);
2367// sweep(circle(r=1,$fn=3), tran);
2368// Example(3D): Applying a scale factor
2369// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], n=25, scale=2);
2370// sweep(circle(r=1,$fn=3), tran);
2371// Example(3D): Applying twist
2372// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], n=25, twist=60);
2373// sweep(circle(r=1,$fn=3), tran);
2374// Example(3D): Going the long way
2375// tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], n=25, long=true);
2376// sweep(circle(r=1,$fn=3), tran);
2377// Example(3D): Getting transformations from turtle3d
2378// include<BOSL2/turtle3d.scad>
2379// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,170],transforms=true);
2380// sweep(circle(r=1,$fn=3),rot_resample(tran, n=40));
2381// Example(3D): If you specify a larger angle in turtle you need to use the long argument
2382// include<BOSL2/turtle3d.scad>
2383// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,270],transforms=true);
2384// sweep(circle(r=1,$fn=3),rot_resample(tran, n=40,long=true));
2385// Example(3D): And if the angle is over 360 you need to add turns to get the right result. Note long is false when the remaining angle after subtracting full turns is below 180:
2386// include<BOSL2/turtle3d.scad>
2387// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,90+360],transforms=true);
2388// sweep(circle(r=1,$fn=3),rot_resample(tran, n=40,long=false,turns=1));
2389// Example(3D): Here the remaining angle is 270, so long must be set to true
2390// include<BOSL2/turtle3d.scad>
2391// tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,270+360],transforms=true);
2392// sweep(circle(r=1,$fn=3),rot_resample(tran, n=40,long=true,turns=1));
2393// Example(3D): Note the visible line at the scale transition
2394// include<BOSL2/turtle3d.scad>
2395// tran = turtle3d(["arcsteps",1,"arcup", 10, 90, "arcdown", 10, 90], transforms=true);
2396// rtran = rot_resample(tran,200,scale=[1,6]);
2397// sweep(circle(1,$fn=32),rtran);
2398// Example(3D): Observe how using a large smoothlen value eases that transition
2399// include<BOSL2/turtle3d.scad>
2400// tran = turtle3d(["arcsteps",1,"arcup", 10, 90, "arcdown", 10, 90], transforms=true);
2401// rtran = rot_resample(tran,200,scale=[1,6],smoothlen=17);
2402// sweep(circle(1,$fn=32),rtran);
2403// Example(3D): A similar issues can arise with twist, where a "line" is visible at the transition
2404// include<BOSL2/turtle3d.scad>
2405// tran = turtle3d(["arcsteps", 1, "arcup", 10, 90, "move", 10], transforms=true,state=[1,-.5,0]);
2406// rtran = rot_resample(tran,100,twist=[0,60],smoothlen=1);
2407// sweep(subdivide_path(rect([3,3]),40),rtran);
2408// Example(3D): Here's the smoothed twist transition
2409// include<BOSL2/turtle3d.scad>
2410// tran = turtle3d(["arcsteps", 1, "arcup", 10, 90, "move", 10], transforms=true,state=[1,-.5,0]);
2411// rtran = rot_resample(tran,100,twist=[0,60],smoothlen=17);
2412// sweep(subdivide_path(rect([3,3]),40),rtran);
2413// Example(3D): Toothed belt based on a list-comprehension-demos example. This version has a smoothed twist transition. Try changing smoothlen to 1 to see the more abrupt transition that occurs without smoothing.
2414// include<BOSL2/turtle3d.scad>
2415// r_small = 19; // radius of small curve
2416// r_large = 46; // radius of large curve
2417// flat_length = 100; // length of flat belt section
2418// teeth=42; // number of teeth
2419// belt_width = 12;
2420// tooth_height = 9;
2421// belt_thickness = 3;
2422// angle = 180 - 2*atan((r_large-r_small)/flat_length);
2423// beltprofile = path3d(subdivide_path(
2424// square([belt_width, belt_thickness],anchor=FWD),
2425// 20));
2426// beltrots =
2427// turtle3d(["arcsteps",1,
2428// "move", flat_length,
2429// "arcleft", r_small, angle,
2430// "move", flat_length,
2431// // Closing path will be interpolated
2432// // "arcleft", r_large, 360-angle
2433// ],transforms=true);
2434// beltpath = rot_resample(beltrots,teeth*4,
2435// twist=[180,0,-180,0],
2436// long=[false,false,false,true],
2437// smoothlen=15,closed=true);
2438// belt = [for(i=idx(beltpath))
2439// let(tooth = floor((i+$t*4)/2)%2)
2440// apply(beltpath[i]*
2441// yscale(tooth
2442// ? tooth_height/belt_thickness
2443// : 1),
2444// beltprofile)
2445// ];
2446// skin(belt,slices=0,closed=true);
2447function rot_resample(rotlist,n,twist,scale,smoothlen=1,long=false,turns=0,closed=false,method="length") =
2448 assert(is_int(smoothlen) && smoothlen>0 && smoothlen%2==1, "smoothlen must be a positive odd integer")
2449 assert(method=="length" || method=="count")
2450 let(tcount = len(rotlist) + (closed?0:-1))
2451 assert(method=="count" || is_int(n), "n must be an integer when method is \"length\"")
2452 assert(is_int(n) || is_vector(n,tcount), str("n must be scalar or vector with length ",tcount))
2453 let(
2454 count = method=="length" ? (closed ? n+1 : n)
2455 : (is_vector(n) ? sum(n) : tcount*n)+1 //(closed?0:1)
2456 )
2457 assert(is_bool(long) || len(long)==tcount,str("Input long must be a scalar or have length ",tcount))
2458 let(
2459 long = force_list(long,tcount),
2460 turns = force_list(turns,tcount),
2461 T = [for(i=[0:1:tcount-1]) rot_inverse(rotlist[i])*select(rotlist,i+1)],
2462 parms = [for(i=idx(T))
2463 let(tparm = rot_decode(T[i],long[i]))
2464 [tparm[0]+turns[i]*360,tparm[1],tparm[2],tparm[3]]
2465 ],
2466 radius = [for(i=idx(parms)) norm(parms[i][2])],
2467 length = [for(i=idx(parms)) norm([norm(parms[i][3]), parms[i][0]/360*2*PI*radius[i]])]
2468 )
2469 assert(method=="count" || all_positive(length),
2470 "Rotation list includes a repeated entry or a rotation around the origin, not allowed when method=\"length\"")
2471 let(
2472 cumlen = [0, each cumsum(length)],
2473 totlen = last(cumlen),
2474 stepsize = totlen/(count-1),
2475 samples = method=="count"
2476 ? let( n = force_list(n,tcount))
2477 [for(N=n) lerpn(0,1,N,endpoint=false)]
2478 :[for(i=idx(parms))
2479 let(
2480 remainder = cumlen[i] % stepsize,
2481 offset = remainder==0 ? 0
2482 : stepsize-remainder,
2483 num = ceil((length[i]-offset)/stepsize)
2484 )
2485 count(num,offset,stepsize)/length[i]],
2486 twist = first_defined([twist,0]),
2487 scale = first_defined([scale,1]),
2488 needlast = !approx(last(last(samples)),1),
2489 sampletwist = is_num(twist) ? lerpn(0,twist,count)
2490 : let(
2491 cumtwist = [0,each cumsum(twist)]
2492 )
2493 [for(i=idx(parms)) each lerp(cumtwist[i],cumtwist[i+1],samples[i]),
2494 if (needlast) last(cumtwist)
2495 ],
2496 samplescale = is_num(scale) ? lerp(1,scale,lerpn(0,1,count))
2497 : let(
2498 cumscale = [1,each cumprod(scale)]
2499 )
2500 [for(i=idx(parms)) each lerp(cumscale[i],cumscale[i+1],samples[i]),
2501 if (needlast) last(cumscale)],
2502 smoothtwist = _smooth(closed?select(sampletwist,0,-2):sampletwist,smoothlen,closed=closed,angle=true),
2503 smoothscale = _smooth(samplescale,smoothlen,closed=closed),
2504 interpolated = [
2505 for(i=idx(parms))
2506 each [for(u=samples[i]) rotlist[i] * move(u*parms[i][3]) * rot(a=u*parms[i][0],v=parms[i][1],cp=parms[i][2])],
2507 if (needlast) last(rotlist)
2508 ]
2509 )
2510 [for(i=idx(interpolated,e=closed?-2:-1)) interpolated[i]*zrot(smoothtwist[i])*scale(smoothscale[i])];
2511
2512
2513
2514
2515
2516//////////////////////////////////////////////////////////////////
2517//
2518// Minimum Distance Mapping using Dynamic Programming
2519//
2520// Given inputs of a two polygons, computes a mapping between their vertices that minimizes the sum the sum of
2521// the distances between every matched pair of vertices. The algorithm uses dynamic programming to calculate
2522// the optimal mapping under the assumption that poly1[0] <-> poly2[0]. We then rotate through all the
2523// possible indexings of the longer polygon. The theoretical run time is quadratic in the longer polygon and
2524// linear in the shorter one.
2525//
2526// The top level function, _skin_distance_match(), cycles through all the of the indexings of the larger
2527// polygon, computes the optimal value for each indexing, and chooses the overall best result. It uses
2528// _dp_extract_map() to thread back through the dynamic programming array to determine the actual mapping, and
2529// then converts the result to an index repetition count list, which is passed to repeat_entries().
2530//
2531// The function _dp_distance_array builds up the rows of the dynamic programming matrix with reference
2532// to the previous rows, where `tdist` holds the total distance for a given mapping, and `map`
2533// holds the information about which path was optimal for each position.
2534//
2535// The function _dp_distance_row constructs each row of the dynamic programming matrix in the usual
2536// way where entries fill in based on the three entries above and to the left. Note that we duplicate
2537// entry zero so account for wrap-around at the ends, and we initialize the distance to zero to avoid
2538// double counting the length of the 0-0 pair.
2539//
2540// This function builds up the dynamic programming distance array where each entry in the
2541// array gives the optimal distance for aligning the corresponding subparts of the two inputs.
2542// When the array is fully populated, the bottom right corner gives the minimum distance
2543// for matching the full input lists. The `map` array contains a the three key values for the three
2544// directions, where _MAP_DIAG means you map the next vertex of `big` to the next vertex of `small`,
2545// _MAP_LEFT means you map the next vertex of `big` to the current vertex of `small`, and _MAP_UP
2546// means you map the next vertex of `small` to the current vertex of `big`.
2547//
2548// Return value is [min_distance, map], where map is the array that is used to extract the actual
2549// vertex map.
2550
2551_MAP_DIAG = 0;
2552_MAP_LEFT = 1;
2553_MAP_UP = 2;
2554
2555/*
2556function _dp_distance_array(small, big, abort_thresh=1/0, small_ind=0, tdist=[], map=[]) =
2557 small_ind == len(small)+1 ? [tdist[len(tdist)-1][len(big)-1], map] :
2558 let( newrow = _dp_distance_row(small, big, small_ind, tdist) )
2559 min(newrow[0]) > abort_thresh ? [tdist[len(tdist)-1][len(big)-1],map] :
2560 _dp_distance_array(small, big, abort_thresh, small_ind+1, concat(tdist, [newrow[0]]), concat(map, [newrow[1]]));
2561*/
2562
2563
2564function _dp_distance_array(small, big, abort_thresh=1/0) =
2565 [for(
2566 small_ind = 0,
2567 tdist = [],
2568 map = []
2569 ;
2570 small_ind<=len(small)+1
2571 ;
2572 newrow =small_ind==len(small)+1 ? [0,0,0] : // dummy end case
2573 _dp_distance_row(small,big,small_ind,tdist),
2574 tdist = concat(tdist, [newrow[0]]),
2575 map = concat(map, [newrow[1]]),
2576 small_ind = min(newrow[0])>abort_thresh ? len(small)+1 : small_ind+1
2577 )
2578 if (small_ind==len(small)+1) each [tdist[len(tdist)-1][len(big)], map]];
2579 //[tdist,map]];
2580
2581
2582function _dp_distance_row(small, big, small_ind, tdist) =
2583 // Top left corner is zero because it gets counted at the end in bottom right corner
2584 small_ind == 0 ? [cumsum([0,for(i=[1:len(big)]) norm(big[i%len(big)]-small[0])]), repeat(_MAP_LEFT,len(big)+1)] :
2585 [for(big_ind=1,
2586 newrow=[ norm(big[0] - small[small_ind%len(small)]) + tdist[small_ind-1][0] ],
2587 newmap = [_MAP_UP]
2588 ;
2589 big_ind<=len(big)+1
2590 ;
2591 costs = big_ind == len(big)+1 ? [0] : // handle extra iteration
2592 [tdist[small_ind-1][big_ind-1], // diag
2593 newrow[big_ind-1], // left
2594 tdist[small_ind-1][big_ind]], // up
2595 newrow = concat(newrow, [min(costs)+norm(big[big_ind%len(big)]-small[small_ind%len(small)])]),
2596 newmap = concat(newmap, [min_index(costs)]),
2597 big_ind = big_ind+1
2598 ) if (big_ind==len(big)+1) each [newrow,newmap]];
2599
2600
2601function _dp_extract_map(map) =
2602 [for(
2603 i=len(map)-1,
2604 j=len(map[0])-1,
2605 smallmap=[],
2606 bigmap = []
2607 ;
2608 j >= 0
2609 ;
2610 advance_i = map[i][j]==_MAP_UP || map[i][j]==_MAP_DIAG,
2611 advance_j = map[i][j]==_MAP_LEFT || map[i][j]==_MAP_DIAG,
2612 i = i - (advance_i ? 1 : 0),
2613 j = j - (advance_j ? 1 : 0),
2614 bigmap = concat( [j%(len(map[0])-1)] , bigmap),
2615 smallmap = concat( [i%(len(map)-1)] , smallmap)
2616 )
2617 if (i==0 && j==0) each [smallmap,bigmap]];
2618
2619
2620/// Internal Function: _skin_distance_match(poly1,poly2)
2621/// Usage:
2622/// polys = _skin_distance_match(poly1,poly2);
2623/// Description:
2624/// Find a way of associating the vertices of poly1 and vertices of poly2
2625/// that minimizes the sum of the length of the edges that connect the two polygons.
2626/// Polygons can be in 2d or 3d. The algorithm has cubic run time, so it can be
2627/// slow if you pass large polygons. The output is a pair of polygons with vertices
2628/// duplicated as appropriate to be used as input to `skin()`.
2629/// Arguments:
2630/// poly1 = first polygon to match
2631/// poly2 = second polygon to match
2632function _skin_distance_match(poly1,poly2) =
2633 let(
2634 swap = len(poly1)>len(poly2),
2635 big = swap ? poly1 : poly2,
2636 small = swap ? poly2 : poly1,
2637 map_poly = [ for(
2638 i=0,
2639 bestcost = 1/0,
2640 bestmap = -1,
2641 bestpoly = -1
2642 ;
2643 i<=len(big)
2644 ;
2645 shifted = list_rotate(big,i),
2646 result =_dp_distance_array(small, shifted, abort_thresh = bestcost),
2647 bestmap = result[0]<bestcost ? result[1] : bestmap,
2648 bestpoly = result[0]<bestcost ? shifted : bestpoly,
2649 best_i = result[0]<bestcost ? i : best_i,
2650 bestcost = min(result[0], bestcost),
2651 i=i+1
2652 )
2653 if (i==len(big)) each [bestmap,bestpoly,best_i]],
2654 map = _dp_extract_map(map_poly[0]),
2655 smallmap = map[0],
2656 bigmap = map[1],
2657 // These shifts are needed to handle the case when points from both ends of one curve map to a single point on the other
2658 bigshift = len(bigmap) - max(max_index(bigmap,all=true))-1,
2659 smallshift = len(smallmap) - max(max_index(smallmap,all=true))-1,
2660 newsmall = list_rotate(repeat_entries(small,unique_count(smallmap)[1]),smallshift),
2661 newbig = list_rotate(repeat_entries(map_poly[1],unique_count(bigmap)[1]),bigshift)
2662 )
2663 swap ? [newbig, newsmall] : [newsmall,newbig];
2664
2665
2666// This function associates vertices but with the assumption that index 0 is associated between the
2667// two inputs. This gives only quadratic run time. As above, output is pair of polygons with
2668// vertices duplicated as suited to use as input to skin().
2669
2670function _skin_aligned_distance_match(poly1, poly2) =
2671 let(
2672 result = _dp_distance_array(poly1, poly2, abort_thresh=1/0),
2673 map = _dp_extract_map(result[1]),
2674 shift0 = len(map[0]) - max(max_index(map[0],all=true))-1,
2675 shift1 = len(map[1]) - max(max_index(map[1],all=true))-1,
2676 new0 = list_rotate(repeat_entries(poly1,unique_count(map[0])[1]),shift0),
2677 new1 = list_rotate(repeat_entries(poly2,unique_count(map[1])[1]),shift1)
2678 )
2679 [new0,new1];
2680
2681
2682//////////////////////////////////////////////////////////////////////////////////////////////////////////////
2683/// Internal Function: _skin_tangent_match()
2684/// Usage:
2685/// x = _skin_tangent_match(poly1, poly2)
2686/// Description:
2687/// Finds a mapping of the vertices of the larger polygon onto the smaller one. Whichever input is the
2688/// shorter path is the polygon, and the longer input is the curve. For every edge of the polygon, the algorithm seeks a plane that contains that
2689/// edge and is tangent to the curve. There will be more than one such point. To choose one, the algorithm centers the polygon and curve on their centroids
2690/// and chooses the closer tangent point. The algorithm works its way around the polygon, computing a series of tangent points and then maps all of the
2691/// points on the curve between two tangent points into one vertex of the polygon. This algorithm can fail if the curve has too few points or if it is concave.
2692/// Arguments:
2693/// poly1 = input polygon
2694/// poly2 = input polygon
2695function _skin_tangent_match(poly1, poly2) =
2696 let(
2697 swap = len(poly1)>len(poly2),
2698 big = swap ? poly1 : poly2,
2699 small = swap ? poly2 : poly1,
2700 curve_offset = centroid(small)-centroid(big),
2701 cutpts = [for(i=[0:len(small)-1]) _find_one_tangent(big, select(small,i,i+1),curve_offset=curve_offset)],
2702 shift = last(cutpts)+1,
2703 newbig = list_rotate(big, shift),
2704 repeat_counts = [for(i=[0:len(small)-1]) posmod(cutpts[i]-select(cutpts,i-1),len(big))],
2705 newsmall = repeat_entries(small,repeat_counts)
2706 )
2707 assert(len(newsmall)==len(newbig), "Tangent alignment failed, probably because of insufficient points or a concave curve")
2708 swap ? [newbig, newsmall] : [newsmall, newbig];
2709
2710
2711function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) =
2712 let(
2713 angles = [
2714 for (i = [0:len(curve)-(closed?1:2)])
2715 let(
2716 plane = plane3pt( edge[0], edge[1], curve[i]),
2717 tangent = [curve[i], select(curve,i+1)]
2718 ) plane_line_angle(plane,tangent)
2719 ],
2720 zero_cross = [
2721 for (i = [0:len(curve)-(closed?1:2)])
2722 if (sign(angles[i]) != sign(select(angles,i+1)))
2723 i
2724 ],
2725 d = [
2726 for (i = zero_cross)
2727 point_line_distance(curve[i]+curve_offset, edge)
2728 ]
2729 ) zero_cross[min_index(d)];
2730
2731
2732// Function: associate_vertices()
2733// Synopsis: Create vertex association to control how {{skin()}} links vertices.
2734// SynTags: PathList
2735// Topics: Extrusion, Skinning, Paths
2736// See Also: skin()
2737// Usage:
2738// newpoly = associate_vertices(polygons, split);
2739// Description:
2740// Takes as input a list of polygons and duplicates specified vertices in each polygon in the list through the series so
2741// that the input can be passed to `skin()`. This allows you to decide how the vertices are linked up rather than accepting
2742// the automatically computed minimal distance linkage. However, the number of vertices in the polygons must not decrease in the list.
2743// The output is a list of polygons that all have the same number of vertices with some duplicates. You specify the vertex splitting
2744// using the `split` which is a list where each entry corresponds to a polygon: split[i] is a value or list specifying which vertices in polygon i to split.
2745// Give the empty list if you don't want a split for a particular polygon. If you list a vertex once then it will be split and mapped to
2746// two vertices in the next polygon. If you list it N times then N copies will be created to map to N+1 vertices in the next polygon.
2747// You must ensure that each mapping produces the correct number of vertices to exactly map onto every vertex of the next polygon.
2748// Note that if you split (only) vertex i of a polygon that means it will map to vertices i and i+1 of the next polygon. Vertex 0 will always
2749// map to vertex 0 and the last vertices will always map to each other, so if you want something different than that you'll need to reindex
2750// your polygons.
2751// Arguments:
2752// polygons = list of polygons to split
2753// split = list of lists of split vertices
2754// Example(FlatSpin,VPD=17,VPT=[0,0,2]): If you skin together a square and hexagon using the optimal distance method you get two triangular faces on opposite sides:
2755// sq = regular_ngon(4,side=2);
2756// hex = apply(rot(15),hexagon(side=2));
2757// skin([sq,hex], slices=10, refine=10, method="distance", z=[0,4]);
2758// Example(FlatSpin,VPD=17,VPT=[0,0,2]): Using associate_vertices you can change the location of the triangular faces. Here they are connect to two adjacent vertices of the square:
2759// sq = regular_ngon(4,side=2);
2760// hex = apply(rot(15),hexagon(side=2));
2761// skin(associate_vertices([sq,hex],[[1,2]]), slices=10, refine=10, sampling="segment", z=[0,4]);
2762// Example(FlatSpin,VPD=17,VPT=[0,0,2]): Here the two triangular faces connect to a single vertex on the square. Note that we had to rotate the hexagon to line them up because the vertices match counting forward, so in this case vertex 0 of the square matches to vertices 0, 1, and 2 of the hexagon.
2763// sq = regular_ngon(4,side=2);
2764// hex = apply(rot(60),hexagon(side=2));
2765// skin(associate_vertices([sq,hex],[[0,0]]), slices=10, refine=10, sampling="segment", z=[0,4]);
2766// Example(3D): This example shows several polygons, with only a single vertex split at each step:
2767// sq = regular_ngon(4,side=2);
2768// pent = pentagon(side=2);
2769// hex = hexagon(side=2);
2770// sep = regular_ngon(7,side=2);
2771// profiles = associate_vertices([sq,pent,hex,sep], [1,3,4]);
2772// skin(profiles ,slices=10, refine=10, method="distance", z=[0,2,4,6]);
2773// Example(3D): The polygons cannot shrink, so if you want to have decreasing polygons you'll need to concatenate multiple results. Note that it is perfectly ok to duplicate a profile as shown here, where the pentagon is duplicated:
2774// sq = regular_ngon(4,side=2);
2775// pent = pentagon(side=2);
2776// grow = associate_vertices([sq,pent], [1]);
2777// shrink = associate_vertices([sq,pent], [2]);
2778// skin(concat(grow, reverse(shrink)), slices=10, refine=10, method="distance", z=[0,2,2,4]);
2779function associate_vertices(polygons, split, curpoly=0) =
2780 curpoly==len(polygons)-1 ? polygons :
2781 let(
2782 polylen = len(polygons[curpoly]),
2783 cursplit = force_list(split[curpoly])
2784 )
2785 assert(len(split)==len(polygons)-1,str(split,"Split list length mismatch: it has length ", len(split)," but must have length ",len(polygons)-1))
2786 assert(polylen<=len(polygons[curpoly+1]),str("Polygon ",curpoly," has more vertices than the next one."))
2787 assert(len(cursplit)+polylen == len(polygons[curpoly+1]),
2788 str("Polygon ", curpoly, " has ", polylen, " vertices. Next polygon has ", len(polygons[curpoly+1]),
2789 " vertices. Split list has length ", len(cursplit), " but must have length ", len(polygons[curpoly+1])-polylen))
2790 assert(len(cursplit) == 0 || max(cursplit)<polylen && min(curpoly)>=0,
2791 str("Split ",cursplit," at polygon ",curpoly," has invalid vertices. Must be in [0:",polylen-1,"]"))
2792 len(cursplit)==0 ? associate_vertices(polygons,split,curpoly+1) :
2793 let(
2794 splitindex = sort(concat(count(polylen), cursplit)),
2795 newpoly = [for(i=[0:len(polygons)-1]) i<=curpoly ? select(polygons[i],splitindex) : polygons[i]]
2796 )
2797 associate_vertices(newpoly, split, curpoly+1);
2798
2799
2800
2801// DefineHeader(Table;Headers=Texture Name|Type|Description): Texture Values
2802
2803// Section: Texturing
2804// Some operations are able to add texture to the objects they create. A texture can be any regularly repeated variation in the height of the surface.
2805// To define a texture you need to specify how the height should vary over a rectangular block that will be repeated to tile the object. Because textures
2806// are based on rectangular tiling, this means adding textures to curved shapes may result in distortion of the basic texture unit. For example, if you
2807// texture a cone, the scale of the texture will be larger at the wide end of the cone and smaller at the narrower end of the cone.
2808// .
2809// You can specify a texture using two methods: a height field or a VNF. For each method you also must specify the scale of the texture, which
2810// gives the size of the rectangular unit in your object that will correspond to one texture tile. Note that this scale does not preserve
2811// aspect ratio: you can stretch the texture as desired.
2812// Subsection: Height Field Texture Maps
2813// The simplest way to specify a texture map is to give a 2d array of
2814// height values which specify the height of the texture on a grid.
2815// Values in the height field should range from 0 to 1. A zero height
2816// in the height field corresponds to the height of the surface and 1
2817// the highest point in the texture above the surface being textured.
2818// Figure(2D,Big,NoScales,VPT=[6.21418,0.242814,0],VPD=28.8248,VPR=[0,0,0]): Here is a 2d texture described by a "grid" that just contains a single row. Such a texture can be used to create ribbing. The texture is `[[0, 1, 1, 0]]`, and the fixture shows three repetitions of the basic texture unit.
2819// ftex1 = [0,1,1,0,0];
2820// stroke( transpose([count(5),ftex1]), dots=true, dots_width=3,width=.05);
2821// right(4)stroke( transpose([count(5),ftex1]), dots=true, width=.05,dots_color="red",color="blue",dots_width=3);
2822// right(8)stroke( transpose([count(5),ftex1]), dots=true, dots_width=3,width=.05);
2823// stroke([[4,-.3],[8,-.3]],width=.05,endcaps="arrow2",color="black");
2824// move([6,-.4])color("black")text("Texture Size", size=0.3,anchor=BACK);
2825// Continues:
2826// Line segments connect the dots within the texture and also the dots between adjacent texture tiles.
2827// The size of the texture (specified with `tex_size`) includes the segment that connects the tile to the next one.
2828// Note that the grid is always uniformly spaced.
2829// By default textures are created with unit depth, meaning that the top surface
2830// of the texture is 1 unit above the surface being textured, assuming that the texture
2831// is correctly designed to span the range from 0 to 1. The `tex_depth` parameter can adjust
2832// this dimension of a texture without changing anything else, and setting `tex_depth` negative
2833// will invert a texture.
2834// Figure(2D,Big,NoScales,VPR=[0,0,0],VPT=[6.86022,-1.91238,0],VPD=28.8248):
2835// ftex1 = [0,1,1,0,0];
2836// left(0)color(.6*[1,1,1])rect([12,1],anchor=BACK+LEFT);
2837// stroke( transpose([count(5),ftex1]), dots=true, dots_width=3,width=.05);
2838// polygon( transpose([count(5),ftex1]));
2839// right(4){stroke( transpose([count(5),ftex1]), dots=true, width=.05,dots_width=3);
2840// polygon( transpose([count(5),ftex1]));
2841// }
2842// right(8){stroke( transpose([count(5),ftex1]), dots=true, dots_width=3,width=.05);
2843// polygon( transpose([count(5),ftex1]));
2844// }
2845// stroke([[12.25,0],[12.25,1]],width=.05,endcaps="arrow2",color="black");
2846// move([12.35,.5])color("black")text("Depth=1", size=0.3,anchor=LEFT);
2847// fwd(4){
2848// left(0)color(.6*[1,1,1])rect([12,1],anchor=BACK+LEFT);
2849// stroke( transpose([count(5),2*ftex1]), dots=true, dots_width=3,width=.05);
2850// polygon( transpose([count(5),2*ftex1]));
2851// right(4){stroke( transpose([count(5),2*ftex1]), dots=true, width=.05,dots_width=3);
2852// polygon( transpose([count(5),2*ftex1]));
2853// }
2854// right(8){stroke( transpose([count(5),2*ftex1]), dots=true, dots_width=3,width=.05);
2855// polygon( transpose([count(5),2*ftex1]));
2856// }
2857// stroke([[12.25,0],[12.25,2]],width=.05,endcaps="arrow2",color="black");
2858// move([12.35,1])color("black")text("Depth=2", size=0.3,anchor=LEFT);
2859// }
2860// Continues:
2861// If you want to keep the texture the same size but make the slope
2862// steeper you need to add more points to make the uniform grid fine enough
2863// to represent the slope you want. This means that creating sharp edges
2864// can require a large number of points, resulting in longer run times.
2865// When using the built-in textures you can control the number of points
2866// using the `n=` argument to {{texture()}}.
2867// Figure(2D,Big,NoScales,VPT=[6.21418,0.242814,0],VPD=28.8248,VPR=[0,0,0]):
2868// ftex2 = xscale(4/11,transpose([count(12),[0,1,1,1,1,1,1,1,1,1,0,0]]));
2869// stroke( ftex2, dots=true, dots_width=3,width=.05);
2870// right(4)stroke( ftex2, dots=true, width=.05,dots_color="red",color="blue",dots_width=3);
2871// right(8)stroke( ftex2, dots=true, dots_width=3,width=.05);
2872// stroke([[4,-.3],[8,-.3]],width=.05,endcaps="arrow2",color="black");
2873// move([6,-.4])color("black")text("Texture Size", size=0.3,anchor=BACK);
2874// Continues:
2875// A more serious limitation of height field textures is that some shapes, such as hexagons or circles, cannot be accurately represented because
2876// their points don't fall on any grid. Trying to create such shapes is difficult and will require many points to approximate the
2877// true point positions for the desired shape. This will make the texture slow to compute.
2878// Another serious limitation is more subtle. In the 2D examples above, it is obvious how to connect the
2879// dots together. But in 3D example we need to triangulate the points on a grid, and this triangulation is not unique.
2880// The `style` argument lets you specify how the points are triangulated using the styles supported by {{vnf_vertex_array()}}.
2881// In the example below we have expanded the 2D example into 3D:
2882// ```openscad
2883// [[0,0,0,0],
2884// [0,1,1,0],
2885// [0,1,1,0],
2886// [0,0,0,0]]
2887// ```
2888// and we show the 3D triangulations produced by the different styles:
2889// Figure(3D,Big,NoAxes,VPR=[45.5,0,18.2],VPT=[2.3442,-6.25815,3.91529],VPD=35.5861):
2890// tex = [
2891// [0,0,0,0,0],
2892// [0,1,1,0,0],
2893// [0,1,1,0,0],
2894// [0,0,0,0,0],
2895// [0,0,0,0,0]
2896// ];
2897// hm = [for(i=[0:4]) [for(j=[0:4]) [i,-j,tex[i][j]]]];
2898// types = ["quincunx", "convex", "concave","min_area", "default","alt","min_edge"];
2899// grid_copies(spacing=5, n=[4,2]){
2900// let(s = types[$row*4+$col]){
2901// if (is_def(s)){
2902// vnf_polyhedron(vnf_vertex_array(hm,style=s));
2903// if ($row==1)
2904// back(.8)right(2)rotate($vpr)color("black")text(s,size=.5,anchor=CENTER);
2905// else
2906// fwd(4.7)right(2)rotate($vpr)color("black")text(s,size=.5,anchor=CENTER);
2907// }
2908// }
2909// }
2910// Continues:
2911// Note that of the seven available styles, five produce a different result. There may exist some concave shape where none of the styles
2912// produce the right result everywhere on the shape. If this happens it would be another limitation of height field textures. (If you have an
2913// example of such a texture and shape please let us know!)
2914// Subsection: VNF Textures
2915// VNF textures overcome all of the limitations of height field textures, but with two costs. They can be more difficult to construct than
2916// a simple array of height values, and they are significantly slower to compute for a tile with the same number of points. Note, however, for
2917// textures that don't neatly lie on a grid, a VNF tile will be more efficient than a finely sampled height field. With VNF textures you can create
2918// textures that have disconnected components, or concavities that cannot be expressed with a single valued height map. However, you can also
2919// create invalid textures that fail to close at the ends, so care is required to ensure that your resulting shape is valid.
2920// .
2921// A VNF texture is defined by defining the texture tile with a VNF whose projection onto the XY plane is contained in the unit square [0,1] x [0,1] so
2922// that the VNF can be tiled. The VNF is tiled without a gap, matching the edges, so the vertices along corresponding edges must match to make a
2923// consistent triangulation possible. The VNF cannot have any X or Y values outside the interval [0,1]. If you want a valid polyhedron
2924// that OpenSCAD will render then you need to take care with edges of the tiles that correspond to endcap faces in the textured object.
2925// So for example, in a linear sweep, the top and bottom edges of tiles end abruptly to form the end cap of the object. You can make a valid object
2926// in two ways. One way is to create a tile with a single, complete edge along Y=0, and of course a corresponding edges along Y=1. The second way
2927// to make a valid object is to have no points at all on the Y=0 line, and of course none on Y=1. In this case, the resulting texture produces
2928// a collection of disconnected objects. Note that the Z coordinates of your tile can be anything, but for the dimensional settings on textures
2929// to work intuitively, you should construct your tile so that Z ranges from 0 to 1.
2930// Figure(3D): This is the "hexgrid" VNF tile, which creates a hexagonal grid texture, something which doesn't work well with a height field because the edges of the hexagon don't align with the grid. Note how the tile ranges between 0 and 1 in both X, Y and Z. In fact, to get a proper aspect ratio in your final texture you need to use the `tex_size` parameter to introduct a sqrt(3) scale factor.
2931// tex = texture("hex_grid");
2932// vnf_polyhedron(tex);
2933// Figure(3D): This is an example of a tile that has no edges at the top or bottom, so it creates disconnected rings. See {{linear_sweep()}} for examples showing this tile in use.
2934// shape = skin([
2935// rect(2/5),
2936// rect(2/3),
2937// rect(2/5)
2938// ],
2939// z=[0,1/2,1],
2940// slices=0,
2941// caps=false);
2942// tile = move([0,1/2,2/3],yrot(90,shape));
2943// vnf_polyhedron(tile);
2944// Continues:
2945// A VNF texture provides a flat structure. In order to apply this structure to a cylinder or other curved object, the VNF must be sliced
2946// and "folded" so it can follow the curve. This folding is controlled by the `tex_samples` parameter to {{cyl()}}, {{linear_sweep()}},
2947// and {{rotate_sweep()}}. Note that you specify it when you **use** the texture, not when you create it. This differs from height
2948// fields, where the analogous parameter is the `n=` parameter of the {{texture()}} function. When `tex_samples` is too small, only the
2949// points given in the VNF will follow the surface, resulting in a blocky look and geometrical artifacts.
2950// Figure(3D,Med,NoAxes): On the left the `tex_samples` value is small and the texture is blocky. On the right, the default value of 8 allows a reasonable fit to the cylinder.
2951// xdistribute(spacing=5){
2952// cyl(d=10/PI, h=5, chamfer=0,
2953// texture=texture("bricks_vnf"), tex_samples=1, tex_reps=[6,3], tex_depth=.2);
2954// cyl(d=10/PI, h=5, chamfer=0,
2955// texture=texture("bricks_vnf"), tex_samples=8, tex_reps=[6,3], tex_depth=.2);
2956// }
2957// Continues:
2958// Note that when the VNF is sliced,
2959// extra points can be introduced in the interior of faces leading to unexpected irregularities in the textures, which appear
2960// as extra triangles. These artifacts can be minimized by making the VNF texture's faces as large as possible rather than using
2961// a triangulated VNF, but depending on the specific VNF texture, it may be impossible to entirely eliminate them.
2962// Figure(3D,Big,NoAxes,VPR=[140.9,0,345.7],VPT=[9.48289,-0.88709,5.7837],VPD=39.5401): The left shows a normal bricks_vnf texture. The right shows a texture that was first passed through {{vnf_triangulate()}}. Note the extra triangle artifacts visible at the ends on the brick faces.
2963// tex = texture("bricks_vnf");
2964// cyl(d=10,h=15,texture=tex, tex_reps=[4,2],tex_samples=5,rounding=2);
2965// up(7)fwd(-3)right(15)cyl(d=10,h=15,texture=vnf_triangulate(tex), tex_reps=[4,2],tex_samples=5,rounding=2);
2966
2967
2968// Function: texture()
2969// Topics: Textures, Knurling
2970// Synopsis: Produce a standard texture.
2971// Topics: Extrusion, Textures
2972// See Also: linear_sweep(), rotate_sweep(), heightfield(), cylindrical_heightfield()
2973// Usage:
2974// tx = texture(tex, [n=], [inset=], [gap=], [roughness=]);
2975// Description:
2976// Given a texture name, returns a texture. Textures can come in two varieties:
2977// - Heightfield textures which are 2D arrays of scalars. These are usually faster to render, but can be less precise and prone to triangulation errors. The table below gives the recommended style for the best triangulation. If results are still incorrect, switch to the similar VNF tile by adding the "_vnf" suffix.
2978// - VNF Tile textures, which are VNFs that cover the unit square [0,0] x [1,1]. These tend to be slower to render, but allow greater flexibility and precision for shapes that don't align with a grid.
2979// .
2980// In the descriptions below, imagine the textures positioned on the XY plane, so "horizontal" refers to the "sideways" dimensions of the texture and
2981// "up" and "down" refer to the depth dimension, perpendicular to the surface being textured. If a texture is placed on a cylinder the "depth" will become the radial direction and the "horizontal"
2982// direction will be the vertical and tangential directions on the cylindrical surface. All horizontal dimensions for VNF textures are relative to the unit square
2983// on which the textures are defined, so a value of 0.25 for a gap or border will refer to 1/4 of the texture's full length and/or width. All supported textures appear below in the examples.
2984// Arguments:
2985// tex = The name of the texture to get.
2986// ---
2987// n = The number of samples to use for defining a heightfield texture. Depending on the texture, result will be either n x n or 1 x n. Not allowed for VNF textures. See the `tex_samples` argument to {{cyl()}}, {{linear_sweep()}} and {{rotate_sweep()}} for controlling the sampling of VNF textures.
2988// border = The size of a border region on some VNF tile textures. Generally between 0 and 0.5.
2989// gap = The gap between logically distinct parts of some VNF tiles. (ie: gap between bricks, gap between truncated ribs, etc.)
2990// roughness = The amount of roughness used on the surface of some heightfield textures. Generally between 0 and 0.5.
2991// Example(3D): **"bricks"** (Heightfield) = A brick-wall pattern. Giving `n=` sets the number of heightfield samples to `n x n`. Default: 24. Giving `roughness=` adds a level of height randomization to add roughness to the texture. Default: 0.05. Use `style="convex"`.
2992// tex = texture("bricks");
2993// linear_sweep(
2994// rect(30), texture=tex, h=30,
2995// tex_size=[10,10]
2996// );
2997// Example(3D): **"bricks_vnf"** (VNF) = VNF version of "bricks". Giving `gap=` sets the "mortar" gap between adjacent bricks, default 0.05. Giving `border=` specifies that the top face of the brick is smaller than the bottom of the brick by `border` on each of the four sides. If `gap` is zero then a `border` value close to 0.5 will cause bricks to come to a sharp pointed edge, with just a tiny flat top surface. Note that `gap+border` must be strictly smaller than 0.5. Default is `border=0.05`.
2998// tex = texture("bricks_vnf");
2999// linear_sweep(
3000// rect(30), texture=tex, h=30,
3001// tex_size=[10,10]
3002// );
3003// Example(3D): "bricks_vnf" texture with large border.
3004// tex = texture("bricks_vnf",border=0.25);
3005// linear_sweep(
3006// rect(30), texture=tex, h=30,
3007// tex_size=[10,10]
3008// );
3009// Example(3D,VPR=[84.4,0,4.7],VPT=[2.44496,6.53317,14.6135],VPD = 126): **"checkers"** (VNF) = A pattern of alternating checkerboard squares. Giving `border=` specifies that the top face of the checker surface is smaller than the bottom by `border` on each of the four sides. As `border` approaches 0.5 the tops come to sharp corners. You must set `border` strictly between 0 and 0.5. Default: 0.05.
3010// tex = texture("checkers");
3011// linear_sweep(
3012// rect(30), texture=tex, h=30,
3013// tex_size=[10,10]
3014// );
3015// Example(3D,VPR=[84.4,0,4.7],VPT=[2.44496,6.53317,14.6135],VPD = 126): "checkers" texture with large border.
3016// tex = texture("checkers",border=0.25);
3017// linear_sweep(
3018// rect(30), texture=tex, h=30,
3019// tex_size=[10,10]
3020// );
3021// Example(3D): **"cones"** (VNF) = Raised conical spikes. Specify `$fn` to set the number of segments on the cone (will be rounded to a multiple of 4). The default is `$fn=16`. Note that `$fa` and `$fs` are ignored, since the scale of the texture is unknown at the time of definition. Giving `border=` specifies the horizontal border width between the edge of the tile and the base of the cone. The `border` value must be nonnegative and smaller than 0.5. Default: 0.
3022// tex = texture("cones", $fn=16);
3023// linear_sweep(
3024// rect(30), texture=tex, h=30, tex_depth=3,
3025// tex_size=[10,10]
3026// );
3027// Example(3D): **"cubes"** (VNF) = Corner-cubes texture. This texture needs to be scaled in vertically by sqrt(3) to have its correct aspect
3028// tex = texture("cubes");
3029// linear_sweep(
3030// rect(30), texture=tex, h=30,
3031// tex_size=[10,10]
3032// );
3033// Example(3D): "cubes" texture at the correct scale.
3034// tex = texture("cubes");
3035// linear_sweep(
3036// rect(30), texture=tex, h=20*sqrt(3), tex_depth=3,
3037// tex_size=[10,10*sqrt(3)]
3038// );
3039// Example(3D): **"diamonds"** (Heightfield) = Four-sided pyramid with the corners of the base aligned with the axes. Compare to "pyramids". Useful for knurling. Giving `n=` sets the number of heightfield samples to `n x n`. Default: 2. Use `style="concave"` for pointed bumps, or `style="default"` or `style="alt"` for a diagonal ribs.
3040// tex = texture("diamonds");
3041// linear_sweep(
3042// rect(30), texture=tex, h=30,
3043// tex_size=[10,10], style="concave"
3044// );
3045// Example(3D): "diamonds" texture can give diagonal ribbing with "default" style.
3046// tex = texture("diamonds");
3047// linear_sweep(
3048// rect(30), texture=tex, h=30,
3049// tex_size=[10,10], style="default"
3050// );
3051// Example(3D): "diamonds" texture gives diagonal ribbing the other direction with "alt" style.
3052// tex = texture("diamonds");
3053// linear_sweep(
3054// rect(30), texture=tex, h=30,
3055// tex_size=[10,10], style="alt"
3056// );
3057// Example(3D): **"diamonds_vnf"** (VNF) = VNF version of "diamonds".
3058// tex = texture("diamonds_vnf");
3059// linear_sweep(
3060// rect(30), texture=tex, h=30,
3061// tex_size=[10,10]
3062// );
3063// Example(3D): **"dimples"** (VNF) = Round divots. Specify `$fn` to set the number of segments on the dimples (will be rounded to a multiple of 4). The default is `$fn=16`. Note that `$fa` and `$fs` are ignored, since the scale of the texture is unknown at the time of definition. Giving `border=` specifies the horizontal width of the flat border region between the tile edges and the edge of the dimple. Must be nonnegative and strictly less than 0.5. Default: 0.05.
3064// tex = texture("dimples", $fn=16);
3065// linear_sweep(
3066// rect(30), texture=tex, h=30,
3067// tex_size=[10,10]
3068// );
3069// Example(3D): **"dots"** (VNF) = Raised round bumps. Specify `$fn` to set the number of segments on the dots (will be rounded to a multiple of 4). The default is `$fn=16`. Note that `$fa` and `$fs` are ignored, since the scale of the texture is unknown at the time of definition. Giving `border=` specifies the horizontal width of the flat border region between the tile edge and the edge of the dots. Must be nonnegative and strictly less than 0.5. Default: 0.05.
3070// tex = texture("dots", $fn=16);
3071// linear_sweep(
3072// rect(30), texture=tex, h=30,
3073// tex_size=[10,10]
3074// );
3075// Example(3D): **"hex_grid"** (VNF) = A hexagonal grid defined by V-grove borders. Giving `border=` specifies that the top face of the hexagon is smaller than the bottom by `border` on the left and right sides. This means the V-groove top width for grooves running parallel to the Y axis will be double the border value. If the texture is scaled in the Y direction by sqrt(3) then the groove will be uniform on all six sides of the hexagon. Border must be strictly between 0 and 0.5, default: 0.1.
3076// tex = texture("hex_grid");
3077// linear_sweep(
3078// rect(30), texture=tex, h=30,
3079// tex_size=[10,10]
3080// );
3081// Example(3D): "hex_grid" texture with large border
3082// tex = texture("hex_grid", border=0.4);
3083// linear_sweep(
3084// rect(30), texture=tex, h=30,
3085// tex_size=[10,10]
3086// );
3087// Example(3D): "hex_grid" scaled in Y by sqrt(3) so hexagons are regular and grooves are all the same width. Note height of cube is also scaled so tile fits without being automatically adjusted to fit, ruining our choice of scale.
3088// tex = texture("hex_grid",border=.07);
3089// linear_sweep(
3090// rect(30), texture=tex, h=quantup(30,10*sqrt(3)),
3091// tex_size=[10,10*sqrt(3)], tex_depth=3
3092// );
3093// Example(3D): "hex_grid" texture, with approximate scaling because 17 is close to sqrt(3) times 10.
3094// tex = texture("hex_grid");
3095// linear_sweep(
3096// rect(30), texture=tex, h=34,
3097// tex_size=[10,17]
3098// );
3099// Example(3D): **"hills"** (Heightfield) = Wavy sine-wave hills and valleys, Giving `n=` sets the number of heightfield samples to `n` x `n`. Default: 12. Set `style="quincunx"`.
3100// tex = texture("hills");
3101// linear_sweep(
3102// rect(30), texture=tex, h=30,
3103// tex_size=[10,10], style="quincunx"
3104// );
3105// Example(3D): **"pyramids"** (Heightfield) = Four-sided pyramid with the edges of the base aligned with the axess. Compare to "diamonds". Useful for knurling. Giving `n=` sets the number of heightfield samples to `n` by `n`. Default: 2. Set style to "convex". Note that style="concave" or style="min_edge" produce mini-diamonds with flat squares in between.
3106// tex = texture("pyramids");
3107// linear_sweep(
3108// rect(30), texture=tex, h=30,
3109// tex_size=[10,10], style="convex"
3110// );
3111// Example(3D): "pyramids" texture, with "concave" produces a mini-diamond texture. Note that "min_edge" also gives this result.
3112// tex = texture("pyramids");
3113// linear_sweep(
3114// rect(30), texture=tex, h=30,
3115// tex_size=[10,10], style="concave"
3116// );
3117// Example(3D): **"pyramids_vnf"** (VNF) = VNF version of "pyramids".
3118// tex = texture("pyramids_vnf");
3119// linear_sweep(
3120// rect(30), texture=tex, h=30,
3121// tex_size=[10,10]
3122// );
3123// Example(3D): **"ribs"** (Heightfield) = Vertically aligned triangular ribs. Giving `n=` sets the number of heightfield samples to `n` by 1. Default: 2. The choice of style does not matter.
3124// tex = texture("ribs");
3125// linear_sweep(
3126// rect(30), texture=tex, h=30, tex_depth=3,
3127// tex_size=[10,10], style="concave"
3128// );
3129// Example(3D): **"rough"** (Heightfield) = A pseudo-randomized rough texture. Giving `n=` sets the number of heightfield samples to `n` by `n`. Default: 32. The `roughness=` parameter specifies the height of the random texture. Default: 0.2.
3130// tex = texture("rough");
3131// linear_sweep(
3132// rect(30), texture=tex, h=30,
3133// tex_size=[10,10], style="min_edge"
3134// );
3135// Example(3D): **"tri_grid"** (VNF) = A triangular grid defined by V-groove borders Giving `border=` specifies that the top face of the triangular surface is smaller than the bottom by `border` along the horizontal edges (parallel to the X axis). This means the V-groove top width of the grooves parallel to the X axis will be double the border value. (The other grooves are wider.) If the tile is scaled in the Y direction by sqrt(3) then the groove will be uniform on the three sides of the triangle. The border must be strictly between 0 and 1/6, default: 0.05.
3136// tex = texture("tri_grid");
3137// linear_sweep(
3138// rect(30), texture=tex, h=30,
3139// tex_size=[10,10]
3140// );
3141// Example(3D): "tri_grid" texture with large border. (Max border for tri_grid is 1/6.)
3142// tex = texture("tri_grid",border=.12);
3143// linear_sweep(
3144// rect(30), texture=tex, h=30,
3145// tex_size=[10,10]
3146// );
3147// Example(3D): "tri_grid" texture scaled in Y by sqrt(3) so triangles are equilateral and grooves are all the same width. Note we have to ensure the height evenly fits the scaled texture tiles.
3148// tex = texture("tri_grid",border=.04);
3149// linear_sweep(
3150// rect(30), texture=tex, h=quantup(30,10*sqrt(3)),
3151// tex_size=[10,10*sqrt(3)], tex_depth=3
3152// );
3153// Example(3D): "tri_grid" texture. Here scale makes Y approximately sqrt(3) larger than X so triangles are close to equilateral.
3154// tex = texture("tri_grid");
3155// linear_sweep(
3156// rect(30), texture=tex, h=34,
3157// tex_size=[10,17]
3158// );
3159// Example(3D): **"trunc_diamonds"** (VNF) = Truncated diamonds, four-sided pyramids with the base corners aligned with the axes and the top cut off. Or you can interpret it as V-groove lines at 45º angles. Giving `border=` specifies that the width and height of the top surface of the diamond are smaller by `border` at the left, right, top and bottom. The border is measured in the **horizontal** direction. This means the V-groove width will be sqrt(2) times the border value. The border must be strictly between 0 and sqrt(2)/4, which is about 0.35. Default: 0.1.
3160// tex = texture("trunc_diamonds");
3161// linear_sweep(
3162// rect(30), texture=tex, h=30,
3163// tex_size=[10,10]
3164// );
3165// Example(3D): "trunc_diamonds" texture with large border.
3166// tex = texture("trunc_diamonds",border=.25);
3167// linear_sweep(
3168// rect(30), texture=tex, h=30,
3169// tex_size=[10,10]
3170// );
3171// Example(3D): **"trunc_pyramids"** (Heightfield) = Truncated pyramids, four sided pyramids with the base edges aligned to the axes and the top cut off. Giving `n=` sets the number of heightfield samples to `n` by `n`. Default: 6. Set `style="convex"`.
3172// tex = texture("trunc_pyramids");
3173// linear_sweep(
3174// rect(30), texture=tex, h=30,
3175// tex_size=[10,10], style="convex"
3176// );
3177// Example(3D): **"trunc_pyramids_vnf"** (VNF) = Truncated pyramids, four sided pyramids with the base edges aligned to the axes and the top cut off. You can also regard this as a grid of V-grooves. Giving `border=` specifies that the top face is smaller than the top by `border` on all four sides. This means the V-groove top width will be double the border value. The border must be strictly between 0 and 0.5. Default: 0.1.
3178// tex = texture("trunc_pyramids_vnf");
3179// linear_sweep(
3180// rect(30), texture=tex, h=30,
3181// tex_size=[10,10]
3182// );
3183// Example(3D): "trunc_pyramids_vnf" texture with large border
3184// tex = texture("trunc_pyramids_vnf", border=.4);
3185// linear_sweep(
3186// rect(30), texture=tex, h=30,
3187// tex_size=[10,10]
3188// );
3189// Example(3D): **"trunc_ribs"** (Heightfield) = Truncated ribs. Vertically aligned triangular ribs with the tops cut off, and with rib separation equal to the width of the flat tops. Giving `n=` sets the number of heightfield samples to `n` by `1`. Default: 4. The style does not matter.
3190// tex = texture("trunc_ribs");
3191// linear_sweep(
3192// rect(30), h=30, texture=tex,
3193// tex_depth=3, tex_size=[10,10],
3194// style="concave"
3195// );
3196// Example(3D): **"trunc_ribs_vnf"** (VNF) = Vertically aligned triangular ribs with the tops cut off. Giving `gap=` sets the bottom gap between ribs. Giving `border=` specifies that the top rib face is smaller than its base by `border` on both the left and right sides. The gap measures the flat part between ribs and the border the width of the sloping portion. In order to fit, gap+2*border must be less than 1. (This is because the gap is counted once but the border counts on both sides.) Defaults: gap=1/4, border=1/4.
3197// tex = texture("trunc_ribs_vnf", gap=0.25, border=1/6);
3198// linear_sweep(
3199// rect(30), h=30, texture=tex,
3200// tex_depth=3, tex_size=[10,10]
3201// );
3202// Example(3D): **"wave_ribs"** (Heightfield) = Vertically aligned wavy ribs. Giving `n=` sets the number of heightfield samples to `n` by `1`. Default: 8. The style does not matter.
3203// tex = texture("wave_ribs");
3204// linear_sweep(
3205// rect(30), h=30, texture=tex,
3206// tex_size=[10,10], tex_depth=3, style="concave"
3207// );
3208
3209
3210function _tex_fn_default() = 16;
3211
3212__vnf_no_n_mesg=" texture is a VNF so it does not accept n. Set sample rate for VNF textures using the tex_samples parameter to cyl(), linear_sweep() or rotate_sweep().";
3213
3214function texture(tex, n, border, gap, roughness, inset) =
3215 assert(num_defined([border,inset])<2, "In texture() the 'inset' parameter has been replaced by 'border'. You cannot give both parameters.")
3216 let(
3217 border = is_def(inset)?echo("In texture() the argument 'inset' has been deprecated and will be removed. Use 'border' instead")
3218 inset
3219 :border
3220 )
3221 assert(is_undef(n) || all_positive([n]), "n must be a positive value if given")
3222 assert(is_undef(border) || is_finite(border), "border must be a number if given")
3223 assert(is_undef(gap) || is_finite(gap), "gap must be a number if given")
3224 assert(is_undef(roughness) || all_nonnegative([roughness]), "roughness must be a nonnegative value if given")
3225 tex=="ribs"?
3226 assert(num_defined([gap, border, roughness])==0, "ribs texture does not accept gap, border or roughness")
3227
3228 let(
3229 n = quantup(default(n,2),2)
3230 ) [[
3231 each lerpn(1,0,n/2,endpoint=false),
3232 each lerpn(0,1,n/2,endpoint=false),
3233 ]] :
3234 tex=="trunc_ribs"?
3235 assert(num_defined([gap, border, roughness])==0, "trunc_ribs texture does not accept gap, border or roughness")
3236 let(
3237 n = quantup(default(n,4),4)
3238 ) [[
3239 each repeat(0,n/4),
3240 each lerpn(0,1,n/4,endpoint=false),
3241 each repeat(1,n/4),
3242 each lerpn(1,0,n/4,endpoint=false),
3243 ]] :
3244 tex=="trunc_ribs_vnf"?
3245 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3246 let(
3247 border = default(border,1/4)*2,
3248 gap = default(gap,1/4),
3249 f=echo(gap, border, gap+border, gap+2*border)
3250 )
3251 assert(all_nonnegative([border,gap]), "trunc_ribs_vnf texture requires gap>=0 and border>=0")
3252 assert(gap+border <= 1, "trunc_ribs_vnf texture requires that gap+2*border<=1")
3253 [
3254 [
3255 each move([0.5,0.5], p=path3d(rect([1-gap,1]),0)),
3256 each move([0.5,0.5], p=path3d(rect([1-gap-border,1]),1)),
3257 each path3d(square(1)),
3258 ], [
3259 [4,7,3,0], [1,2,6,5],
3260 if (gap+border < 1-EPSILON) [4,5,6,7],
3261 if (gap > EPSILON) each [[1,9,10,2], [0,3,11,8]],
3262 ]
3263 ] :
3264 tex=="wave_ribs"?
3265 assert(num_defined([gap, border, roughness])==0, "wave_ribs texture does not accept gap, border or roughness")
3266 let(
3267 n = max(6,default(n,8))
3268 ) [[
3269 for(a=[0:360/n:360-EPSILON])
3270 (cos(a)+1)/2
3271 ]] :
3272 tex=="diamonds"?
3273 assert(num_defined([gap, border, roughness])==0, "diamonds texture does not accept gap, border or roughness")
3274 let(
3275 n = quantup(default(n,2),2)
3276 ) [
3277 let(
3278 path = [
3279 each lerpn(0,1,n/2,endpoint=false),
3280 each lerpn(1,0,n/2,endpoint=false),
3281 ]
3282 )
3283 for (i=[0:1:n-1]) [
3284 for (j=[0:1:n-1]) min(
3285 select(path,i+j),
3286 select(path,i-j)
3287 )
3288 ],
3289 ] :
3290 tex=="diamonds_vnf"?
3291 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3292 assert(num_defined([gap, border, roughness])==0, "diamonds_vnf texture does not accept gap, border or roughness")
3293 [
3294 [
3295 [0, 1, 1], [1/2, 1, 0], [1, 1, 1],
3296 [0, 1/2, 0], [1/2, 1/2, 1], [1, 1/2, 0],
3297 [0, 0, 1], [1/2, 0, 0], [1, 0, 1],
3298 ], [
3299 [0,1,3], [2,5,1], [8,7,5], [6,3,7],
3300 [1,5,4], [5,7,4], [7,3,4], [4,3,1],
3301 ]
3302 ] :
3303 tex=="pyramids"?
3304 assert(num_defined([gap, border, roughness])==0, "pyramids texture does not accept gap, border or roughness")
3305 let(
3306 n = quantup(default(n,2),2)
3307 ) [
3308 for (i = [0:1:n-1]) [
3309 for (j = [0:1:n-1])
3310 1 - (max(abs(i-n/2), abs(j-n/2)) / (n/2))
3311 ]
3312 ] :
3313 tex=="pyramids_vnf"?
3314 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3315 assert(num_defined([gap, border, roughness])==0, "pyramids_vnf texture does not accept gap, border or roughness")
3316 [
3317 [ [0,1,0], [1,1,0], [1/2,1/2,1], [0,0,0], [1,0,0] ],
3318 [ [2,0,1], [2,1,4], [2,4,3], [2,3,0] ]
3319 ] :
3320 tex=="trunc_pyramids"?
3321 assert(num_defined([gap, border, roughness])==0, "trunc_pyramids texture does not accept gap, border or roughness")
3322 let(
3323 n = quantup(default(n,6),3)
3324 ) [
3325 for (i = [0:1:n-1]) [
3326 for (j = [0:1:n-1])
3327 (1 - (max(n/6, abs(i-n/2), abs(j-n/2)) / (n/2))) * 1.5
3328 ]
3329 ] :
3330 tex=="trunc_pyramids_vnf"?
3331 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3332 assert(num_defined([gap, roughness])==0, "trunc_pyramids_vnf texture does not accept gap, or roughness")
3333 let(
3334 border = default(border,0.1)
3335 )
3336 assert(border>0 && border<.5, "trunc_pyramids_vnf texture requires border in (0,0.5)")
3337 [
3338 [
3339 each path3d(square(1)),
3340 each move([1/2,1/2,1], p=path3d(rect(1-2*border))),
3341 ], [
3342 for (i=[0:3])
3343 [i, (i+1)%4, (i+1)%4+4,i+4],
3344 [4,5,6,7]
3345 ]
3346 ] :
3347 tex=="hills"?
3348 assert(num_defined([gap, border, roughness])==0, "hills texture does not accept gap, border or roughness")
3349 let(
3350 n = default(n,12)
3351 ) [
3352 for (a=[0:360/n:359.999]) [
3353 for (b=[0:360/n:359.999])
3354 (cos(a)*cos(b)+1)/2
3355 ]
3356 ] :
3357 tex=="bricks"?
3358 assert(num_defined([gap,border])==0, "bricks texture does not accept gap or border")
3359 let(
3360 n = quantup(default(n,24),2),
3361 rough = default(roughness,0.05)
3362 ) [
3363 for (y = [0:1:n-1])
3364 rands(-rough/2, rough/2, n, seed=12345+y*678) + [
3365 for (x = [0:1:n-1])
3366 (y%(n/2) <= max(1,n/16))? 0 :
3367 let( even = floor(y/(n/2))%2? n/2 : 0 )
3368 (x+even) % n <= max(1,n/16)? 0 : 0.5
3369 ]
3370 ] :
3371 tex=="bricks_vnf"?
3372 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3373 assert(num_defined([roughness])==0, "bricks_vnf texture does not accept roughness")
3374 let(
3375 border = default(border,0.05),
3376 gap = default(gap,0.05)
3377 )
3378 assert(border>=0,"bricks_vnf texture requires nonnegative border")
3379 assert(gap>0, "bricks_vnf requires gap greater than 0")
3380 assert(gap+border<0.5, "bricks_vnf requires gap+border<0.5")
3381 [
3382 [
3383 each path3d(square(1)),
3384 each move([gap/2, gap/2, 0], p=path3d(square([1-gap, 0.5-gap]))),
3385 each move([gap/2+border/2, gap/2+border/2, 1], p=path3d(square([1-gap-border, 0.5-gap-border]))),
3386 each move([0, 0.5+gap/2, 0], p=path3d(square([0.5-gap/2, 0.5-gap]))),
3387 each move([0, 0.5+gap/2+border/2, 1], p=path3d(square([0.5-gap/2-border/2, 0.5-gap-border]))),
3388 each move([0.5+gap/2, 0.5+gap/2, 0], p=path3d(square([0.5-gap/2, 0.5-gap]))),
3389 each move([0.5+gap/2+border/2, 0.5+gap/2+border/2, 1], p=path3d(square([0.5-gap/2-border/2, 0.5-gap-border]))),
3390 ], [
3391 [0,4,7,20], [4,8,11,7], [9,8,4,5], [4,0,1,5], [10,9,5,6],
3392 [20,7,6,13,12,21] ,[2,3,23,22,15,14], [15,19,18,14], [22,23,27,26], [16,19,15,12],[13,6,5,1],
3393 [26,25,21,22], [8,9,10,11],[7,11,10,6],[17,16,12,13],[22,21,12,15],[16,17,18,19],[24,25,26,27],[25,24,20,21]
3394 ]
3395 ] :
3396 tex=="checkers"?
3397 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3398 assert(num_defined([gap, roughness])==0, "checkers texture does not accept gap, or roughness")
3399 let(
3400 border = default(border,0.05)
3401 )
3402 assert(border>0 && border<.5, "checkers texture requires border in (0,0.5)")
3403 [
3404 [
3405 each move([0,0], p=path3d(square(0.5-border),1)),
3406 each move([0,0.5], p=path3d(square(0.5-border))),
3407 each move([0.5,0], p=path3d(square(0.5-border))),
3408 each move([0.5,0.5], p=path3d(square(0.5-border),1)),
3409 [1/2-border/2,1/2-border/2,1/2], [0,1,1], [1/2-border,1,1],
3410 [1/2,1,0], [1-border,1,0], [1,0,1], [1,1/2-border,1],
3411 [1,1/2,0], [1,1-border,0], [1,1,1], [1/2-border/2,1-border/2,1/2],
3412 [1-border/2,1-border/2,1/2], [1-border/2,1/2-border/2,1/2],
3413 ], [
3414 for (i=[0:4:12]) each [[i,i+1,i+2,i+3]],
3415 [10,16,13,12,28,11],[9,0,3,16,10], [11,28,22,21,8],
3416 [4,7,26,14,13,16], [7,6,17,18,26], [5,4,16,3,2],
3417 [19,20,27,15,14,26], [20,25,27], [19,26,18],
3418 [23,28,12,15,27,24], [23,22,28], [24,27,25]
3419 ]
3420 ] :
3421 tex=="cones"?
3422 assert(is_undef(n),str("To set number of segments on cones use $fn. ", tex,__vnf_no_n_mesg))
3423 assert(num_defined([gap,roughness])==0, "cones texture does not accept gap or roughness")
3424 let(
3425 border = default(border,0),
3426 n = $fn > 0 ? quantup($fn,4) : _tex_fn_default()
3427 )
3428 assert(border>=0 && border<0.5)
3429 [
3430 [
3431 each move([1/2,1/2], p=path3d(circle(d=1-2*border,$fn=n))),
3432 [1/2,1/2,1],
3433 each border>0 ? path3d(subdivide_path(square(1),refine=2,closed=true))
3434 : path3d(square(1))
3435 ], [
3436 for (i=[0:1:n-1]) [i, (i+1)%n, n],
3437 if (border>0) for (i=[0:3]) [for(j=[(i+1)*n/4:-1:i*n/4]) j%n,
3438 (2*i+7)%8+n+1,(2*i)%8+n+1, (2*i+1)%8+n+1],
3439 if (border==0) for (i=[0:3]) [for(j=[(i+1)*n/4:-1:i*n/4]) j%n, i+n+1]
3440 ]
3441 ] :
3442 tex=="cubes"?
3443 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3444 assert(num_defined([gap, border, roughness])==0, "cubes texture does not accept gap, border or roughness")
3445 [
3446 [
3447 [0,1,1/2], [1,1,1/2], [1/2,5/6,1], [0,4/6,0], [1,4/6,0],
3448 [1/2,3/6,1/2], [0,2/6,1], [1,2/6,1], [1/2,1/6,0], [0,0,1/2],
3449 [1,0,1/2],
3450 ], [
3451 [0,1,2], [0,2,3], [1,4,2], [2,5,3], [2,4,5],
3452 [6,3,5], [4,7,5], [7,8,5], [6,5,8], [10,8,7],
3453 [9,6,8], [10,9,8],
3454 ]
3455 ] :
3456 tex=="trunc_diamonds"?
3457 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3458 assert(num_defined([gap, roughness])==0, "trunc_diamonds texture does not accept gap or roughness")
3459 let(
3460 border = default(border,0.1)/sqrt(2)*2
3461 )
3462 assert(border>0 && border<0.5)
3463 [
3464 [
3465 each move([1/2,1/2,0], p=path3d(circle(d=1,$fn=4))),
3466 each move([1/2,1/2,1], p=path3d(circle(d=1-border*2,$fn=4))),
3467 for (a=[0:90:359]) each move([1/2,1/2], p=zrot(-a, p=[[1/2,border,1], [border,1/2,1], [1/2,1/2,1]]))
3468 ], [
3469 for (i=[0:3]) each let(j=i*3+8) [
3470 [i,(i+1)%4,(i+1)%4+4,i+4],
3471 [j,j+1,j+2], [i, (i+3)%4,j+1, j],
3472 ],
3473 [4,5,6,7],
3474 ]
3475 ] :
3476 tex=="dimples" || tex=="dots" ?
3477 assert(is_undef(n),str("To set number of segments on ",tex," use $fn. ", tex,__vnf_no_n_mesg))
3478 assert(num_defined([gap,roughness])==0, str(tex," texture does not accept gap or roughness"))
3479 let(
3480 border = default(border,0.05),
3481 n = $fn > 0 ? quantup($fn,4) : _tex_fn_default()
3482 )
3483 assert(border>=0 && border < 0.5)
3484 let(
3485 rows=ceil(n/4),
3486 r=adj_ang_to_hyp(1/2-border,45),
3487 dots = tex=="dots",
3488 cp = [1/2, 1/2, r*sin(45)*(dots?-1:1)],
3489 sc = 1 / (r - abs(cp.z)),
3490 uverts = [
3491 for (p=[0:1:rows-1], t=[0:360/n:359.999])
3492 cp + (
3493 dots? spherical_to_xyz(r, -t, 45-45*p/rows) :
3494 spherical_to_xyz(r, -t, 135+45*p/rows)
3495 ),
3496 cp + r * (dots?UP:DOWN),
3497 each border>0 ? path3d(subdivide_path(square(1),refine=2,closed=true))
3498 : path3d(square(1)),
3499
3500 ],
3501 verts = zscale(sc, p=uverts),
3502 faces = [
3503 for (i=[0:1:rows-2], j=[0:1:n-1]) each [
3504 [i*n+j, i*n+(j+1)%n, (i+1)*n+(j+1)%n,(i+1)*n+j],
3505 ],
3506 for (i=[0:1:n-1]) [(rows-1)*n+i, (rows-1)*n+(i+1)%n, rows*n],
3507 if (border>0) for (i=[0:3]) [for(j=[(i+1)*n/4:-1:i*n/4]) j%n,
3508 (2*i+7)%8+rows*n+1,(2*i)%8+rows*n+1, (2*i+1)%8+rows*n+1],
3509 if (border==0) for (i=[0:3]) [for(j=[(i+1)*n/4:-1:i*n/4]) j%n, i+rows*n+1]
3510 ]
3511 ) [verts, faces] :
3512 tex=="tri_grid"?
3513 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3514 assert(num_defined([gap, roughness])==0, str(tex," texture does not accept gap or roughness"))
3515 let(
3516 border = default(border,0.05)*sqrt(3)
3517 )
3518 assert(border>0 && border<sqrt(3)/6, "tri_grid texture requires border in (0,1/6)")
3519 let(
3520 adj = opp_ang_to_adj(border, 30),
3521 y1 = border / adj_ang_to_opp(1,60), // i/sqrt(3)
3522 y2 = 2*y1, // 2*i/sqrt(3)
3523 y3 = 0.5 - y1,
3524 y4 = 0.5 + y1,
3525 y5 = 1 - y2,
3526 y6 = 1 - y1
3527 )
3528 [
3529 [
3530 [0,0,0], [1,0,0],
3531 [adj,y1,1], [1-adj,y1,1],
3532 [0,y2,1], [1,y2,1],
3533 [0.5,0.5-y2,1],
3534 [0,y3,1], [0.5-adj,y3,1], [0.5+adj,y3,1], [1,y3,1],
3535 [0,0.5,0], [0.5,0.5,0], [1,0.5,0],
3536 [0,y4,1], [0.5-adj,y4,1], [0.5+adj,y4,1], [1,y4,1],
3537 [0.5,0.5+y2,1],
3538 [0,y5,1], [1,y5,1],
3539 [adj,y6,1], [1-adj,y6,1],
3540 [0,1,0], [1,1,0],
3541 ], [
3542 [0,2,3,1],
3543 [21,23,24,22],
3544 [2,6,3], [0,12,6,2], [1,3,6,12],
3545 [0,4,8,12], [4,7,8], [8,7,11,12],
3546 [1,12,9,5], [5,9,10], [10,9,12,13],
3547 [11,14,15,12], [19,15,14], [19,23,12,15],
3548 [16,17,13,12], [16,20,17], [12,24,20,16],
3549 [21,22,18], [12,23,21,18],
3550 [12,18,22,24],
3551 ]
3552 ] :
3553 tex=="hex_grid"?
3554 assert(is_undef(n), str(tex,__vnf_no_n_mesg))
3555 assert(num_defined([gap, roughness])==0, str(tex," texture does not accept gap or roughness"))
3556 let(
3557 border=default(border,0.1)
3558 )
3559 assert(border>0 && border<0.5)
3560 let(
3561 diag=opp_ang_to_hyp(border,60),
3562 side=adj_ang_to_opp(1,30),
3563 hyp=adj_ang_to_hyp(0.5,30),
3564 sc = 1/3/hyp,
3565 hex=[ [1,2/6,0], [1/2,1/6,0], [0,2/6,0], [0,4/6,0], [1/2,5/6,0], [1,4/6,0] ]
3566 ) [
3567 [
3568 each hex,
3569 each move([0.5,0.5], p=yscale(sc, p=path3d(ellipse(d=1-2*border, circum=true, spin=-30,$fn=6),1))),
3570 hex[0]-[0,diag*sc,-1],
3571 for (ang=[270+60,270-60]) hex[1]+yscale(sc, p=cylindrical_to_xyz(diag,ang,1)),
3572 hex[2]-[0,diag*sc,-1],
3573 [0,0,1], [0.5-border,0,1], [0.5,0,0], [0.5+border,0,1], [1,0,1],
3574 hex[3]+[0,diag*sc,1],
3575 for (ang=[90+60,90-60]) hex[4]+yscale(sc, p=cylindrical_to_xyz(diag,ang,1)),
3576 hex[5]+[0,diag*sc,1],
3577 [0,1,1], [0.5-border,1,1], [0.5,1,0], [0.5+border,1,1], [1,1,1],
3578 ], [
3579 count(6,s=6),
3580 for (i=[0:1:5]) [i,(i+1)%6, (i+1)%6+6, i+6],
3581 [20,19,13,12], [17,16,15,14],
3582 [21,25,26,22], [23,28,29,24],
3583 [0,12,13,1], [1,14,15,2],
3584 [3,21,22,4], [4,23,24,5],
3585 [1,13,19,18], [1,18,17,14],
3586 [4,22,26,27], [4,27,28,23],
3587 ]
3588 ] :
3589 tex=="rough"?
3590 assert(num_defined([gap,border])==0, str(tex," texture does not accept gap or border"))
3591 let(
3592 n = default(n,32),
3593 rough = default(roughness, 0.2)
3594 ) [
3595 for (y = [0:1:n-1])
3596 rands(0, rough, n, seed=123456+29*y)
3597 ] :
3598 assert(false, str("Unrecognized texture name: ", tex));
3599
3600
3601/// Function&Module: _textured_linear_sweep()
3602/// Usage: As Function
3603/// vnf = _textured_linear_sweep(region, texture, tex_size, h, ...);
3604/// vnf = _textured_linear_sweep(region, texture, counts=, h=, ...);
3605/// Usage: As Module
3606/// _textured_linear_sweep(region, texture, tex_size, h, ...) [ATTACHMENTS];
3607/// _textured_linear_sweep(region, texture, counts=, h=, ...) [ATTACHMENTS];
3608/// Topics: Sweep, Extrusion, Textures, Knurling
3609/// See Also: heightfield(), cylindrical_heightfield(), texture()
3610/// Description:
3611/// Given a [[Region|regions.scad]], creates a linear extrusion of it vertically, optionally twisted, scaled, and/or shifted,
3612/// with a given texture tiled evenly over the side surfaces. The texture can be given in one of three ways:
3613/// - As a texture name string. (See {{texture()}} for supported named textures.)
3614/// - As a 2D array of evenly spread height values. (AKA a heightfield.)
3615/// - As a VNF texture tile. A VNF tile exactly defines a surface from `[0,0]` to `[1,1]`, with the Z coordinates
3616/// being the height of the texture point from the surface. VNF tiles MUST be able to tile in both X and Y
3617/// directions with no gaps, with the front and back edges aligned exactly, and the left and right edges as well.
3618/// .
3619/// One script to convert a grayscale image to a texture heightfield array in a .scad file can be found at:
3620/// https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/scripts/img2scad.py
3621/// Arguments:
3622/// region = The [[Region|regions.scad]] to sweep/extrude.
3623/// 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.
3624/// 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]`
3625/// h / l = The height to extrude/sweep the path.
3626/// ---
3627/// counts = If given instead of tex_size, gives the tile repetition counts for textures over the surface length and height.
3628/// inset = If numeric, lowers the texture into the surface by that amount, before the tex_scale multiplier is applied. If `true`, insets by exactly `1`. Default: `false`
3629/// rot = If true, rotates the texture 90º.
3630/// tex_scale = Scaling multiplier for the texture depth.
3631/// twist = Degrees of twist for the top of the extrustion/sweep, compared to the bottom. Default: 0
3632/// scale = Scaling multiplier for the top of the extrustion/sweep, compared to the bottom. Default: 1
3633/// shift = [X,Y] amount to translate the top, relative to the bottom. Default: [0,0]
3634/// style = The triangulation style used. See {{vnf_vertex_array()}} for valid styles. Used only with heightfield type textures. Default: `"min_edge"`
3635/// samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
3636/// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
3637/// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
3638/// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
3639/// Extra Anchors:
3640/// centroid_top = The centroid of the top of the shape, oriented UP.
3641/// centroid = The centroid of the center of the shape, oriented UP.
3642/// centroid_bot = The centroid of the bottom of the shape, oriented DOWN.
3643
3644function _get_vnf_tile_edges(texture) =
3645 let(
3646 verts = texture[0],
3647 faces = texture[1],
3648 everts = [for (v = verts) (v.x==0 || v.y==0 || v.x==1 || v.y==1)],
3649 uc = unique_count([
3650 for (face = faces, i = idx(face))
3651 let(edge = select(face,i,i+1), i1 = min(edge), i2 = max(edge))
3652 if (everts[i1] && everts[i2])
3653 [i1, i2]
3654 ]),
3655 edges = uc[0], counts = uc[1],
3656 uedges = [for (i = idx(edges)) if (counts[i] == 1) edges[i] ]
3657 ) uedges;
3658
3659
3660function _validate_texture(texture) =
3661 is_vnf(texture)
3662 ? let( // Validate VNF tile texture
3663 bounds = pointlist_bounds(texture[0]),
3664 min_xy = point2d(bounds[0]),
3665 max_xy = point2d(bounds[1])
3666 )
3667 //assert(min_xy==[0,0] && max_xy==[1,1],"VNF tiles must span exactly from [0,0] to [1,1] in the X and Y components."))
3668 assert(all_nonnegative(concat(min_xy,[1,1]-max_xy)), "VNF tile X and Y components must be between 0 and 1.")
3669 let(
3670 verts = texture[0],
3671 uedges = _get_vnf_tile_edges(texture),
3672 edge_verts = [for (i = unique(flatten(uedges))) verts[i] ],
3673 hverts = [for(v = edge_verts) if(v.x==0 || v.x==1) v],
3674 vverts = [for(v = edge_verts) if(v.y==0 || v.y==1) v],
3675 allgoodx = all(hverts, function(v) any(hverts, function(w) approx(w,[1-v.x, v.y, v.z]))),
3676 allgoody = all(vverts, function(v) any(vverts, function(w) approx(w,[v.x, 1-v.y, v.z])))
3677 )
3678 assert(allgoodx && allgoody, "All VNF tile edge vertices must line up with a vertex on the opposite side of the tile.")
3679 true
3680 : // Validate heightfield texture.
3681 assert(is_matrix(texture), "Malformed texture.")
3682 let( tex_dim = list_shape(texture) )
3683 assert(len(tex_dim) == 2, "Heightfield texture must be a 2D square array of scalar heights.")
3684 assert(all_defined(tex_dim), "Heightfield texture must be a 2D square array of scalar heights.")
3685 true;
3686
3687
3688function _textured_linear_sweep(
3689 region, texture, tex_size=[5,5],
3690 h, counts, inset=false, rot=0,
3691 tex_scale=1, twist, scale, shift,
3692 style="min_edge", l, caps=true,
3693 height, length, samples,
3694 anchor=CENTER, spin=0, orient=UP
3695) =
3696 assert(is_path(region,[2]) || is_region(region))
3697 assert(is_undef(samples) || is_int(samples))
3698 assert(counts==undef || is_vector(counts,2))
3699 assert(tex_size==undef || is_vector(tex_size,2))
3700 assert(is_bool(rot) || in_list(rot,[0,90,180,270]))
3701 assert(is_bool(caps) || is_bool_list(caps,2))
3702 let(
3703 caps = is_bool(caps) ? [caps,caps] : caps,
3704 regions = is_path(region,2)? [[region]] : region_parts(region),
3705 tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
3706 dummy = assert(is_undef(samples) || is_vnf(tex), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
3707 dummy2=is_bool(rot)?echo("boolean value for tex_rot is deprecated. Use a numerical angle, one of 0, 90, 180, or 270.")0:0,
3708 texture = !rot? tex :
3709 is_vnf(tex)? zrot(is_num(rot)?rot:90, cp=[1/2,1/2], p=tex) :
3710 rot==180? reverse([for (row=tex) reverse(row)]) :
3711 rot==270? [for (row=transpose(tex)) reverse(row)] :
3712 reverse(transpose(tex)),
3713 h = first_defined([h, l, height, length, 1]),
3714 inset = is_num(inset)? inset : inset? 1 : 0,
3715 twist = default(twist, 0),
3716 shift = default(shift, [0,0]),
3717 scale = scale==undef? [1,1,1] :
3718 is_num(scale)? [scale,scale,1] : scale,
3719 samples = !is_vnf(texture)? len(texture[0]) :
3720 is_num(samples)? samples : 8,
3721 check_tex = _validate_texture(texture),
3722 sorted_tile =
3723 !is_vnf(texture)? texture :
3724 let(
3725 s = 1 / max(1, samples),
3726 vnf = samples<=1? texture :
3727 let(
3728 slice_us = list([s:s:1-s/2]),
3729 vnft1 = vnf_slice(texture, "X", slice_us),
3730 vnft = twist? vnf_slice(vnft1, "Y", slice_us) : vnft1,
3731 zvnf = [
3732 [
3733 for (p=vnft[0]) [
3734 approx(p.x,0)? 0 : approx(p.x,1)? 1 : p.x,
3735 approx(p.y,0)? 0 : approx(p.y,1)? 1 : p.y,
3736 p.z
3737 ]
3738 ],
3739 vnft[1]
3740 ]
3741 ) zvnf
3742 ) _vnf_sort_vertices(vnf, idx=[1,0]),
3743 vertzs = !is_vnf(sorted_tile)? undef :
3744 group_sort(sorted_tile[0], idx=1),
3745 tpath = is_vnf(sorted_tile)
3746 ? _find_vnf_tile_edge_path(sorted_tile,0)
3747 : let(
3748 row = sorted_tile[0],
3749 rlen = len(row)
3750 ) [for (i = [0:1:rlen]) [i/rlen, row[i%rlen]]],
3751 tmat = scale(scale) * zrot(twist) * up(h/2),
3752 pre_skew_vnf = vnf_join([
3753 for (rgn = regions) let(
3754 walls_vnf = vnf_join([
3755 for (path = rgn) let(
3756 path = reverse(path),
3757 plen = path_length(path, closed=true),
3758 counts = is_vector(counts,2)? counts :
3759 is_vector(tex_size,2)
3760 ? [round(plen/tex_size.x), max(1,round(h/tex_size.y)), ]
3761 : [ceil(6*plen/h), 6],
3762 obases = resample_path(path, n=counts.x * samples, closed=true),
3763 onorms = path_normals(obases, closed=true),
3764 bases = list_wrap(obases),
3765 norms = list_wrap(onorms),
3766 vnf = is_vnf(texture)
3767 ? vnf_join( // VNF tile texture
3768 let(
3769 row_vnf = vnf_join([
3770 for (i = [0:1:(scale==1?0:counts.y-1)], j = [0:1:counts.x-1]) [
3771 [
3772 for (group = vertzs)
3773 each [
3774 for (vert = group) let(
3775 u = floor((j + vert.x) * samples),
3776 uu = ((j + vert.x) * samples) - u,
3777 texh = tex_scale<0 ? -(1-vert.z - inset) * tex_scale
3778 : (vert.z - inset) * tex_scale,
3779 base = lerp(bases[u], select(bases,u+1), uu),
3780 norm = unit(lerp(norms[u], select(norms,u+1), uu)),
3781 xy = base + norm * texh,
3782 pt = point3d(xy,vert.y),
3783 v = vert.y / counts.y,
3784 vv = i / counts.y,
3785 sc = lerp([1,1,1], scale, vv+v),
3786 mat =
3787 up((vv-0.5)*h) *
3788 scale(sc) *
3789 zrot(twist*(v+vv)) *
3790 zscale(h/counts.y)
3791 ) apply(mat, pt)
3792 ]
3793 ],
3794 sorted_tile[1]
3795 ]
3796 ])
3797 ) [
3798 for (i = [0:1:0*(scale!=1?0:counts.y-1)])
3799 let(
3800 v = i / (scale==1?counts.y:1),
3801 sc = lerp([1,1,1], scale, v),
3802 mat =
3803 up((v)*h) *
3804 scale(sc) *
3805 zrot(twist*v)
3806 )
3807 apply(mat, row_vnf)
3808 ]
3809 )
3810 : let( // Heightfield texture
3811 texcnt = [len(texture[0]), len(texture)],
3812 tile_rows = [
3813 for (ti = [0:1:texcnt.y-1])
3814 path3d([
3815 for (j = [0:1:counts.x])
3816 for (tj = [0:1:texcnt.x-1])
3817 if (j != counts.x || tj == 0)
3818 let(
3819 part = (j + (tj/texcnt.x)) * samples,
3820 u = floor(part),
3821 uu = part - u,
3822 texh = tex_scale<0 ? -(1-texture[ti][tj] - inset) * tex_scale
3823 : (texture[ti][tj] - inset) * tex_scale,
3824 base = lerp(bases[u], select(bases,u+1), uu),
3825 norm = unit(lerp(norms[u], select(norms,u+1), uu)),
3826 xy = base + norm * texh
3827 ) xy
3828 ])
3829 ],
3830 tiles = [
3831 for (i = [0:1:counts.y], ti = [0:1:texcnt.y-1])
3832 if (i != counts.y || ti == 0)
3833 let(
3834 v = (i + (ti/texcnt.y)) / counts.y,
3835 sc = lerp([1, 1, 1], scale, v),
3836 mat = up((v-0.5)*h) *
3837 scale(sc) *
3838 zrot(twist*v)
3839 ) apply(mat, tile_rows[(texcnt.y-ti)%texcnt.y])
3840 ]
3841 ) vnf_vertex_array(
3842 tiles, caps=false, style=style,
3843 col_wrap=true, row_wrap=false,
3844 reverse=true
3845 )
3846 ) vnf
3847 ]),
3848 brgn = [
3849 for (path = rgn) let(
3850 path = reverse(path),
3851 plen = path_length(path, closed=true),
3852 counts = is_vector(counts,2)? counts :
3853 is_vector(tex_size,2)
3854 ? [round(plen/tex_size.x), max(1,round(h/tex_size.y)), ]
3855 : [ceil(6*plen/h), 6],
3856 obases = resample_path(path, n=counts.x * samples, closed=true),
3857 onorms = path_normals(obases, closed=true),
3858 bases = list_wrap(obases),
3859 norms = list_wrap(onorms),
3860 nupath = [
3861 for (j = [0:1:counts.x-1], vert = tpath) let(
3862 part = (j + vert.x) * samples,
3863 u = floor(part),
3864 uu = part - u,
3865 texh = tex_scale<0 ? -(1-vert.y - inset) * tex_scale
3866 : (vert.y - inset) * tex_scale,
3867 base = lerp(bases[u], select(bases,u+1), uu),
3868 norm = unit(lerp(norms[u], select(norms,u+1), uu)),
3869 xy = base + norm * texh
3870 ) xy
3871 ]
3872 ) nupath
3873 ],
3874 bot_vnf = !caps[0] || brgn==[[]] ? EMPTY_VNF
3875 : vnf_from_region(brgn, down(h/2), reverse=true),
3876 top_vnf = !caps[1] || brgn==[[]] ? EMPTY_VNF
3877 : vnf_from_region(brgn, tmat, reverse=false)
3878 ) vnf_join([walls_vnf, bot_vnf, top_vnf])
3879 ]),
3880 skmat = down(h/2) * skew(sxz=shift.x/h, syz=shift.y/h) * up(h/2),
3881 final_vnf = apply(skmat, pre_skew_vnf),
3882 cent = centroid(region),
3883 anchors = [
3884 named_anchor("centroid_top", point3d(cent, h/2), UP),
3885 named_anchor("centroid", point3d(cent), UP),
3886 named_anchor("centroid_bot", point3d(cent,-h/2), DOWN)
3887 ]
3888 ) reorient(anchor,spin,orient, vnf=final_vnf, extent=true, anchors=anchors, p=final_vnf);
3889
3890
3891
3892function _find_vnf_tile_edge_path(vnf, val) =
3893 let(
3894 verts = vnf[0],
3895 fragments = [
3896 for(edge = _get_vnf_tile_edges(vnf))
3897 let(v0 = verts[edge[0]], v1 = verts[edge[1]])
3898 if (approx(v0.y, val) && approx(v1.y, val))
3899 v0.x <= v1.x? [[v0.x,v0.z], [v1.x,v1.z]] :
3900 [[v1.x,v1.z], [v0.x,v0.z]]
3901 ],
3902 sfrags = sort(fragments, idx=[0,1]),
3903 rpath = _assemble_a_path_from_fragments(sfrags)[0],
3904 opath = rpath==[]? []
3905 : rpath[0].x > last(rpath).x ? reverse(rpath)
3906 : rpath
3907 ) opath;
3908
3909
3910/// Function&Module: _textured_revolution()
3911/// Usage: As Function
3912/// vnf = _textured_revolution(shape, texture, tex_size, [tex_scale=], ...);
3913/// vnf = _textured_revolution(shape, texture, counts=, [tex_scale=], ...);
3914/// Usage: As Module
3915/// _textured_revolution(shape, texture, tex_size, [tex_scale=], ...) [ATTACHMENTS];
3916/// _textured_revolution(shape, texture, counts=, [tex_scale=], ...) [ATTACHMENTS];
3917/// Topics: Sweep, Extrusion, Textures, Knurling
3918/// See Also: heightfield(), cylindrical_heightfield(), texture()
3919/// Description:
3920/// Given a 2D region or path, fully in the X+ half-plane, revolves that shape around the Z axis (after rotating its Y+ to Z+).
3921/// This creates a solid from that surface of revolution, possibly capped top and bottom, with the sides covered in a given tiled texture.
3922/// The texture can be given in one of three ways:
3923/// - As a texture name string. (See {{texture()}} for supported named textures.)
3924/// - As a 2D array of evenly spread height values. (AKA a heightfield.)
3925/// - As a VNF texture tile. A VNF tile exactly defines a surface from `[0,0]` to `[1,1]`, with the Z coordinates
3926/// being the height of the texture point from the surface. VNF tiles MUST be able to tile in both X and Y
3927/// directions with no gaps, with the front and back edges aligned exactly, and the left and right edges as well.
3928/// .
3929/// One script to convert a grayscale image to a texture heightfield array in a .scad file can be found at:
3930/// https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/scripts/img2scad.py
3931/// Arguments:
3932/// shape = The path or region to sweep/extrude.
3933/// 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 the revolution surface. See {{texture()}} for what named textures are supported.
3934/// 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]`
3935/// tex_scale = Scaling multiplier for the texture depth.
3936/// ---
3937/// inset = If numeric, lowers the texture into the surface by that amount, before the tex_scale multiplier is applied. If `true`, insets by exactly `1`. Default: `false`
3938/// rot = If true, rotates the texture 90º.
3939/// shift = [X,Y] amount to translate the top, relative to the bottom. Default: [0,0]
3940/// 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`
3941/// taper = If given, and `closed=false`, 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)
3942/// angle = The number of degrees counter-clockwise from X+ to revolve around the Z axis. Default: `360`
3943/// style = The triangulation style used. See {{vnf_vertex_array()}} for valid styles. Used only with heightfield type textures. Default: `"min_edge"`
3944/// counts = If given instead of tex_size, gives the tile repetition counts for textures over the surface length and height.
3945/// samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
3946/// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
3947/// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
3948/// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
3949/// Anchor Types:
3950/// "hull" = Anchors to the virtual convex hull of the shape.
3951/// "intersect" = Anchors to the surface of the shape.
3952
3953function _textured_revolution(
3954 shape, texture, tex_size, tex_scale=1,
3955 inset=false, rot=false, shift=[0,0],
3956 taper, closed=true, angle=360,
3957 inhibit_y_slicing=false,
3958 counts, samples,
3959 style="min_edge", atype="intersect",
3960 anchor=CENTER, spin=0, orient=UP
3961) =
3962 assert(angle>0 && angle<=360)
3963 assert(is_path(shape,[2]) || is_region(shape))
3964 assert(is_undef(samples) || is_int(samples))
3965 assert(is_bool(closed))
3966 assert(counts==undef || is_vector(counts,2))
3967 assert(tex_size==undef || is_vector(tex_size,2))
3968 assert(is_bool(rot) || in_list(rot,[0,90,180,270]))
3969 let( taper_is_ok = is_undef(taper) || (is_finite(taper) && taper>=0 && taper<50) || is_path(taper,2) )
3970 assert(taper_is_ok, "Bad taper= value.")
3971 assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"")
3972 let(
3973 regions = !is_path(shape,2)? region_parts(shape) :
3974 closed? region_parts([shape]) :
3975 let(
3976 clpoly = [[0,shape[0].y], each shape, [0,last(shape).y]],
3977 dpoly = deduplicate(clpoly),
3978 cwpoly = is_polygon_clockwise(dpoly) ? dpoly : reverse(dpoly)
3979 )
3980 [[ select(cwpoly,1,-2) ]],
3981 checks = [
3982 for (rgn=regions, path=rgn)
3983 assert(all(path, function(pt) pt.x>=0))
3984 ]
3985 )
3986 assert(closed || is_path(shape,2))
3987 let(
3988 tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
3989 dummy = assert(is_undef(samples) || is_vnf(tex), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
3990 dummy2=is_bool(rot)?echo("boolean value for tex_rot is deprecated. Use a numerical angle, one of 0, 90, 180, or 270.")0:0,
3991 texture = !rot? tex :
3992 is_vnf(tex)? zrot(is_num(rot)?rot:90, cp=[1/2,1/2], p=tex) :
3993 rot==180? reverse([for (row=tex) reverse(row)]) :
3994 rot==270? [for (row=transpose(tex)) reverse(row)] :
3995 reverse(transpose(tex)),
3996 check_tex = _validate_texture(texture),
3997 inset = is_num(inset)? inset : inset? 1 : 0,
3998 samples = !is_vnf(texture)? len(texture) :
3999 is_num(samples)? samples : 8,
4000 bounds = pointlist_bounds(flatten(flatten(regions))),
4001 maxx = bounds[1].x,
4002 miny = bounds[0].y,
4003 maxy = bounds[1].y,
4004 h = maxy - miny,
4005 circumf = 2 * PI * maxx,
4006 tile = !is_vnf(texture)? texture :
4007 let(
4008 utex = samples<=1? texture :
4009 let(
4010 s = 1 / samples,
4011 slices = list([s : s : 1-s/2]),
4012 vnfx = vnf_slice(texture, "X", slices),
4013 vnfy = inhibit_y_slicing? vnfx : vnf_slice(vnfx, "Y", slices),
4014 vnft = vnf_triangulate(vnfy),
4015 zvnf = [
4016 [
4017 for (p=vnft[0]) [
4018 approx(p.x,0)? 0 : approx(p.x,1)? 1 : p.x,
4019 approx(p.y,0)? 0 : approx(p.y,1)? 1 : p.y,
4020 p.z
4021 ]
4022 ],
4023 vnft[1]
4024 ]
4025 ) zvnf
4026 ) _vnf_sort_vertices(utex, idx=[0,1]),
4027 vertzs = is_vnf(texture)? group_sort(tile[0], idx=0) : undef,
4028 bpath = is_vnf(tile)
4029 ? _find_vnf_tile_edge_path(tile,1)
4030 : let(
4031 row = tile[0],
4032 rlen = len(row)
4033 ) [for (i = [0:1:rlen]) [i/rlen, row[i%rlen]]],
4034 counts_x = is_vector(counts,2)? counts.x :
4035 is_vector(tex_size,2)
4036 ? max(1,round(angle/360*circumf/tex_size.x))
4037 : ceil(6*angle/360*circumf/h),
4038 taper_lup = closed || is_undef(taper)? [[-1,1],[2,1]] :
4039 is_num(taper)? [[-1,0], [0,0], [taper/100+EPSILON,1], [1-taper/100-EPSILON,1], [1,0], [2,0]] :
4040 is_path(taper,2)? let(
4041 retaper = [
4042 for (t=taper)
4043 assert(t[0]>=0 && t[0]<=100, "taper lookup indices must be between 0 and 100 inclusive.")
4044 [t[0]/100, t[1]]
4045 ],
4046 taperout = [[-1,retaper[0][1]], each retaper, [2,last(retaper)[1]]]
4047 ) taperout :
4048 assert(false, "Bad taper= argument value."),
4049 full_vnf = vnf_join([
4050 for (rgn = regions) let(
4051 rgn_wall_vnf = vnf_join([
4052 for (path = rgn) let(
4053 plen = path_length(path, closed=closed),
4054 counts_y = is_vector(counts,2)? counts.y :
4055 is_vector(tex_size,2)? max(1,round(plen/tex_size.y)) : 6,
4056 obases = resample_path(path, n=counts_y * samples + (closed?0:1), closed=closed),
4057 onorms = path_normals(obases, closed=closed),
4058 rbases = closed? list_wrap(obases) : obases,
4059 rnorms = closed? list_wrap(onorms) : onorms,
4060 bases = xrot(90, p=path3d(rbases)),
4061 norms = xrot(90, p=path3d(rnorms)),
4062 vnf = is_vnf(texture)
4063 ? vnf_join([ // VNF tile texture
4064 for (j = [0:1:counts_y-1])
4065 [
4066 [
4067 for (group = vertzs) each [
4068 for (vert = group) let(
4069 part = (j + (1-vert.y)) * samples,
4070 u = floor(part),
4071 uu = part - u,
4072 base = lerp(select(bases,u), select(bases,u+1), uu),
4073 norm = unit(lerp(select(norms,u), select(norms,u+1), uu)),
4074 tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
4075 texh = tex_scale<0 ? -(1-vert.z - inset) * tex_scale * (base.x / maxx)
4076 : (vert.z - inset) * tex_scale * (base.x / maxx),
4077 xyz = base - norm * texh
4078 ) zrot(vert.x*angle/counts_x, p=xyz)
4079 ]
4080 ],
4081 tile[1]
4082 ]
4083 ])
4084 : let( // Heightfield texture
4085 texcnt = [len(texture[0]), len(texture)],
4086 tiles = transpose([
4087 for (j = [0,1], tj = [0:1:texcnt.x-1])
4088 if (j == 0 || tj == 0)
4089 let(
4090 v = (j + (tj/texcnt.x)) / counts_x,
4091 mat = zrot(v*angle)
4092 ) apply(mat, [
4093 for (i = [0:1:counts_y-(closed?1:0)], ti = [0:1:texcnt.y-1])
4094 if (i != counts_y || ti == 0)
4095 let(
4096 part = (i + (ti/texcnt.y)) * samples,
4097 u = floor(part),
4098 uu = part - u,
4099 base = lerp(bases[u], select(bases,u+1), uu),
4100 norm = unit(lerp(norms[u], select(norms,u+1), uu)),
4101 tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
4102 texh = tex_scale<0 ? -(1-texture[ti][tj] - inset) * tex_scale * (base.x / maxx)
4103 : (texture[ti][tj] - inset) * tex_scale * (base.x / maxx),
4104 xyz = base - norm * texh
4105 ) xyz
4106 ])
4107 ])
4108 ) vnf_vertex_array(
4109 tiles, caps=false, style=style,
4110 col_wrap=false, row_wrap=closed
4111 )
4112 ) vnf
4113 ]),
4114 walls_vnf = vnf_join([
4115 for (i = [0:1:counts_x-1])
4116 zrot(i*angle/counts_x, rgn_wall_vnf)
4117 ]),
4118 endcap_vnf = angle == 360? EMPTY_VNF :
4119 let(
4120 cap_rgn = [
4121 for (path = rgn) let(
4122 plen = path_length(path, closed=closed),
4123 counts_y = is_vector(counts,2)? counts.y :
4124 is_vector(tex_size,2)? max(1,round(plen/tex_size.y)) : 6,
4125 obases = resample_path(path, n=counts_y * samples + (closed?0:1), closed=closed),
4126 onorms = path_normals(obases, closed=closed),
4127 bases = closed? list_wrap(obases) : obases,
4128 norms = closed? list_wrap(onorms) : onorms,
4129 ppath = is_vnf(texture)
4130 ? [ // VNF tile texture
4131 for (j = [0:1:counts_y-1])
4132 for (group = vertzs, vert = reverse(group))
4133 if (approx(vert.x, 0)) let(
4134 part = (j + (1 - vert.y)) * samples,
4135 u = floor(part),
4136 uu = part - u,
4137 base = lerp(select(bases,u), select(bases,u+1), uu),
4138 norm = unit(lerp(select(norms,u), select(norms,u+1), uu)),
4139 tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
4140 texh = tex_scale<0 ? -(1-vert.z - inset) * tex_scale * (base.x / maxx)
4141 : (vert.z - inset) * tex_scale * (base.x / maxx),
4142 xyz = base - norm * texh
4143 ) xyz
4144 ]
4145 : let( // Heightfield texture
4146 texcnt = [len(texture[0]), len(texture)]
4147 ) [
4148 for (i = [0:1:counts_y-(closed?1:0)], ti = [0:1:texcnt.y-1])
4149 if (i != counts_y || ti == 0)
4150 let(
4151 part = (i + (ti/texcnt.y)) * samples,
4152 u = floor(part),
4153 uu = part - u,
4154 base = lerp(bases[u], select(bases,u+1), uu),
4155 norm = unit(lerp(norms[u], select(norms,u+1), uu)),
4156 tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
4157 texh = tex_scale<0 ? -(1-texture[ti][0] - inset) * tex_scale * (base.x / maxx)
4158 : (texture[ti][0] - inset) * tex_scale * (base.x / maxx),
4159 xyz = base - norm * texh
4160 ) xyz
4161 ],
4162 path = closed? ppath : [
4163 [0, ppath[0].y],
4164 each ppath,
4165 [0, last(ppath).y],
4166 ]
4167 ) deduplicate(path, closed=closed)
4168 ],
4169 vnf2 = vnf_from_region(cap_rgn, xrot(90), reverse=false),
4170 vnf3 = vnf_from_region(cap_rgn, rot([90,0,angle]), reverse=true)
4171 ) vnf_join([vnf2, vnf3]),
4172 allcaps_vnf = closed? EMPTY_VNF :
4173 let(
4174 plen = path_length(rgn[0], closed=closed),
4175 counts_y = is_vector(counts,2)? counts.y :
4176 is_vector(tex_size,2)? max(1,round(plen/tex_size.y)) : 6,
4177 obases = resample_path(rgn[0], n=counts_y * samples + (closed?0:1), closed=closed),
4178 onorms = path_normals(obases, closed=closed),
4179 rbases = closed? list_wrap(obases) : obases,
4180 rnorms = closed? list_wrap(onorms) : onorms,
4181 bases = xrot(90, p=path3d(rbases)),
4182 norms = xrot(90, p=path3d(rnorms)),
4183 caps_vnf = vnf_join([
4184 for (j = [-1,0]) let(
4185 base = select(bases,j),
4186 norm = unit(select(norms,j)),
4187 ppath = [
4188 for (vert = bpath) let(
4189 uang = vert.x / counts_x,
4190 tex_scale = tex_scale * lookup([0,1][j+1], taper_lup),
4191 texh = tex_scale<0 ? -(1-vert.y - inset) * tex_scale * (base.x / maxx)
4192 : (vert.y - inset) * tex_scale * (base.x / maxx),
4193 xyz = base - norm * texh
4194 ) zrot(angle*uang, p=xyz)
4195 ],
4196 pplen = len(ppath),
4197 zed = j<0? max(column(ppath,2)) :
4198 min(column(ppath,2)),
4199 slice_vnf = [
4200 [
4201 each ppath,
4202 [0, 0, zed],
4203 ], [
4204 for (i = [0:1:pplen-2])
4205 j<0? [pplen, i, (i+1)%pplen] :
4206 [pplen, (i+1)%pplen, i]
4207 ]
4208 ],
4209 cap_vnf = vnf_join([
4210 for (i = [0:1:counts_x-1])
4211 zrot(i*angle/counts_x, p=slice_vnf)
4212 ])
4213 ) cap_vnf
4214 ])
4215 ) caps_vnf
4216 ) vnf_join([walls_vnf, endcap_vnf, allcaps_vnf])
4217 ]),
4218 skmat = down(-miny) * skew(sxz=shift.x/h, syz=shift.y/h) * up(-miny),
4219 skvnf = apply(skmat, full_vnf),
4220 geom = atype=="intersect"
4221 ? attach_geom(vnf=skvnf, extent=false)
4222 : attach_geom(vnf=skvnf, extent=true)
4223 ) reorient(anchor,spin,orient, geom=geom, p=skvnf);
4224
4225
4226module _textured_revolution(
4227 shape, texture, tex_size, tex_scale=1,
4228 inset=false, rot=false, shift=[0,0],
4229 taper, closed=true, angle=360,
4230 style="min_edge", atype="intersect",
4231 inhibit_y_slicing=false,
4232 convexity=10, counts, samples,
4233 anchor=CENTER, spin=0, orient=UP
4234) {
4235 dummy = assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"");
4236 vnf = _textured_revolution(
4237 shape, texture, tex_size=tex_size,
4238 tex_scale=tex_scale, inset=inset, rot=rot,
4239 taper=taper, closed=closed, style=style,
4240 shift=shift, angle=angle,
4241 samples=samples, counts=counts,
4242 inhibit_y_slicing=inhibit_y_slicing
4243 );
4244 geom = atype=="intersect"
4245 ? attach_geom(vnf=vnf, extent=false)
4246 : attach_geom(vnf=vnf, extent=true);
4247 attachable(anchor,spin,orient, geom=geom) {
4248 vnf_polyhedron(vnf, convexity=convexity);
4249 children();
4250 }
4251}
4252
4253
4254
4255// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap