1//////////////////////////////////////////////////////////////////////
2// LibFile: attachments.scad
3// The modules in this file allows you to attach one object to another by making one object the child of another object.
4// You can place the child object in relation to its parent object and control the position and orientation
5// relative to the parent. The modifiers allow you to treat children in ways different from simple union, such
6// as differencing them from the parent, or changing their color. Attachment only works when the parent and child
7// are both written to support attachment. Also included in this file are the tools to make your own "attachable" objects.
8// Includes:
9// include <BOSL2/std.scad>
10// FileGroup: Basic Modeling
11// FileSummary: Positioning objects on or relative to other objects. Making your own objects support attachment.
12// FileFootnotes: STD=Included in std.scad
13//////////////////////////////////////////////////////////////////////
14
15
16// Default values for attachment code.
17$tags=undef; // for backward compatibility
18$tag = "";
19$tag_prefix = "";
20$overlap = 0;
21$color = "default";
22$save_color = undef; // Saved color to revert back for children
23
24$anchor_override = undef;
25$attach_to = undef;
26$attach_anchor = [CENTER, CENTER, UP, 0];
27$attach_norot = false;
28
29$parent_anchor = BOTTOM;
30$parent_spin = 0;
31$parent_orient = UP;
32
33$parent_size = undef;
34$parent_geom = undef;
35
36$tags_shown = "ALL";
37$tags_hidden = [];
38
39_ANCHOR_TYPES = ["intersect","hull"];
40
41
42// Section: Terminology and Shortcuts
43// This library adds the concept of anchoring, spin and orientation to the `cube()`, `cylinder()`
44// and `sphere()` builtins, as well as to most of the shapes provided by this library itself.
45// - An anchor is a place on an object which you can align the object to, or attach other objects
46// to using `attach()` or `position()`. An anchor has a position, a direction, and a spin.
47// The direction and spin are used to orient other objects to match when using `attach()`.
48// - Spin is a simple rotation around the Z axis.
49// - Orientation is rotating an object so that its top is pointed towards a given vector.
50// .
51// An object will first be translated to its anchor position, then spun, then oriented.
52// For a detailed step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
53// .
54// For describing directions, faces, edges, and corners the library provides a set of shortcuts
55// all based on combinations of unit direction vectors. You can use these for anchoring and orienting
56// attachable objects. You can also them to specify edge sets for rounding or chamfering cuboids,
57// or for placing edge, face and corner masks.
58// Subsection: Anchor
59// Anchoring is specified with the `anchor` argument in most shape modules. Specifying `anchor`
60// when creating an object will translate the object so that the anchor point is at the origin
61// (0,0,0). Anchoring always occurs before spin and orientation are applied.
62// .
63// An anchor can be referred to in one of two ways; as a directional vector, or as a named anchor string.
64// .
65// When given as a vector, it points, in a general way, towards the face, edge, or corner of the
66// object that you want the anchor for, relative to the center of the object. You can simply
67// specify a vector like `[0,0,1]` to anchor an object at the Z+ end, but you can also use
68// directional constants with names like `TOP`, `BOTTOM`, `LEFT`, `RIGHT` and `BACK` that you can add together
69// to specify anchor points. See [specifying directions](attachments.scad#subsection-specifying-directions)
70// below for the full list of pre-defined directional constants.
71// .
72// For example:
73// - `[0,0,1]` is the same as `TOP` and refers to the center of the top face.
74// - `[-1,0,1]` is the same as `TOP+LEFT`, and refers to the center of the top-left edge.
75// - `[1,1,-1]` is the same as `BOTTOM+BACK+RIGHT`, and refers to the bottom-back-right corner.
76// .
77// When the object is cubical or rectangular in shape the anchors must have zero or one values
78// for their components and they refer to the face centers, edge centers, or corners of the object.
79// The direction of a face anchor will be perpendicular to the face, pointing outward. The direction of a edge anchor
80// will be the average of the anchor directions of the two faces the edge is between. The direction
81// of a corner anchor will be the average of the anchor directions of the three faces the corner is
82// on.
83// .
84// When the object is cylindrical, conical, or spherical in nature, the anchors will be located
85// around the surface of the cylinder, cone, or sphere, relative to the center.
86// You can generally use an arbitrary vector to get an anchor positioned anywhere on the curved
87// surface of such an object, and the anchor direction will be the surface normal at the anchor location.
88// However, for anchor component pointing toward the flat face should be either -1, 1, or 0, and
89// anchors that point diagonally toward one of the flat faces will select a point on the edge.
90// .
91// For objects in two dimensions, the natural expectation is for TOP and BOTTOM to refer to the Y direction
92// of the shape. To support this, if you give an anchor in 2D that has anchor.y=0 then the Z component
93// will be mapped to the Y direction. This means you can use TOP and BOTTOM for anchors of 2D objects.
94// But remember that TOP and BOTTOM are three dimensional vectors and this is a special interpretation
95// for 2d anchoring.
96// .
97// Some more complex objects, like screws and stepper motors, have named anchors to refer to places
98// on the object that are not at one of the standard faces, edges or corners. For example, stepper
99// motors have anchors for `"screw1"`, `"screw2"`, etc. to refer to the various screwholes on the
100// stepper motor shape. The names, positions, directions, and spins of these anchors are
101// specific to the object, and are documented when they exist.
102// Subsection: Spin
103// Spin is specified with the `spin` argument in most shape modules. Specifying a scalar `spin`
104// when creating an object will rotate the object counter-clockwise around the Z axis by the given
105// number of degrees. If given as a 3D vector, the object will be rotated around each of the X, Y, Z
106// axes by the number of degrees in each component of the vector. Spin is always applied after
107// anchoring, and before orientation. Since spin is applied after anchoring it is not what
108// you might think of intuitively as spinning the shape. To do that, apply `zrot()` to the shape before anchoring.
109// Subsection: Orient
110// Orientation is specified with the `orient` argument in most shape modules. Specifying `orient`
111// when creating an object will rotate the object such that the top of the object will be pointed
112// at the vector direction given in the `orient` argument. Orientation is always applied after
113// anchoring and spin. The constants `UP`, `DOWN`, `FRONT`, `BACK`, `LEFT`, and `RIGHT` can be
114// added together to form the directional vector for this. ie: `LEFT+BACK`
115// Subsection: Specifying Directions
116// You can use direction vectors to specify anchors for objects or to specify edges, faces, and
117// corners of cubes. You can simply specify these direction vectors numerically, but another
118// option is to use named constants for direction vectors. These constants define unit vectors
119// for the six axis directions as shown below.
120// Figure(3D,Big,VPD=6): Named constants for direction vectors. Some directions have more than one name.
121// $fn=12;
122// stroke([[0,0,0],RIGHT], endcap2="arrow2", width=.05);
123// color("black")right(.05)up(.05)move(RIGHT) text3d("RIGHT",size=.1,h=.01,anchor=LEFT,orient=FRONT);
124// stroke([[0,0,0],LEFT], endcap2="arrow2", width=.05);
125// color("black")left(.05)up(.05)move(LEFT) text3d("LEFT",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
126// stroke([[0,0,0],FRONT], endcap2="arrow2", width=.05);
127// color("black")
128// left(.1){
129// up(.12)move(FRONT) text3d("FRONT",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
130// move(FRONT) text3d("FWD",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
131// down(.12)move(FRONT) text3d("FORWARD",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
132// }
133// stroke([[0,0,0],BACK], endcap2="arrow2", width=.05);
134// right(.05)
135// color("black")move(BACK) text3d("BACK",size=.1,h=.01,anchor=LEFT,orient=FRONT);
136// stroke([[0,0,0],DOWN], endcap2="arrow2", width=.05);
137// color("black")
138// right(.1){
139// up(.12)move(BOT) text3d("DOWN",size=.1,h=.01,anchor=LEFT,orient=FRONT);
140// move(BOT) text3d("BOTTOM",size=.1,h=.01,anchor=LEFT,orient=FRONT);
141// down(.12)move(BOT) text3d("BOT",size=.1,h=.01,anchor=LEFT,orient=FRONT);
142// }
143// stroke([[0,0,0],TOP], endcap2="arrow2", width=.05);
144// color("black")left(.05){
145// up(.12)move(TOP) text3d("TOP",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
146// move(TOP) text3d("UP",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
147// }
148// Figure(2D,Big): Named constants for direction vectors in 2D. For anchors the TOP and BOTTOM directions are collapsed into 2D as shown here, but do not try to use TOP or BOTTOM as 2D directions in other situations.
149// $fn=12;
150// stroke(path2d([[0,0,0],RIGHT]), endcap2="arrow2", width=.05);
151// color("black")fwd(.22)left(.05)move(RIGHT) text("RIGHT",size=.1,anchor=RIGHT);
152// stroke(path2d([[0,0,0],LEFT]), endcap2="arrow2", width=.05);
153// color("black")right(.05)fwd(.22)move(LEFT) text("LEFT",size=.1,anchor=LEFT);
154// stroke(path2d([[0,0,0],FRONT]), endcap2="arrow2", width=.05);
155// color("black")
156// fwd(.2)
157// right(.15)
158// color("black")move(BACK) { text("BACK",size=.1,anchor=LEFT); back(.14) text("(TOP)", size=.1, anchor=LEFT);}
159// color("black")
160// left(.15)back(.2+.14)move(FRONT){
161// back(.14) text("FRONT",size=.1,anchor=RIGHT);
162// text("FWD",size=.1,anchor=RIGHT);
163// fwd(.14) text("FORWARD",size=.1,anchor=RIGHT);
164// fwd(.28) text("(BOTTOM)",size=.1,anchor=RIGHT);
165// fwd(.14*3) text("(BOT)",size=.1,anchor=RIGHT);
166// }
167// stroke(path2d([[0,0,0],BACK]), endcap2="arrow2", width=.05);
168// Subsection: Specifying Faces
169// Modules operating on faces accept a list of faces to describe the faces to operate on. Each
170// face is given by a vector that points to that face. Attachments of cuboid objects onto their faces also
171// work by choosing an attachment face with a single vector in the same manner.
172// Figure(3D,Big,NoScales,VPD=275): The six faces of the cube. Some have faces have more than one name.
173// ydistribute(50) {
174// xdistribute(35){
175// _show_cube_faces([BACK], botlabel=["BACK"]);
176// _show_cube_faces([UP],botlabel=["TOP","UP"]);
177// _show_cube_faces([RIGHT],botlabel=["RIGHT"]);
178// }
179// xdistribute(35){
180// _show_cube_faces([FRONT],toplabel=["FRONT","FWD", "FORWARD"]);
181// _show_cube_faces([DOWN],toplabel=["BOTTOM","BOT","DOWN"]);
182// _show_cube_faces([LEFT],toplabel=["LEFT"]);
183// }
184// }
185// Subsection: Specifying Edges
186// Modules operating on edges use two arguments to describe the edge set they will use: The `edges` argument
187// is a list of edge set descriptors to include in the edge set, and the `except` argument is a list of
188// edge set descriptors to remove from the edge set.
189// The default value for `edges` is `"ALL"`, the set of all edges.
190// The default value for `except` is the empty set, meaning no edges are removed.
191// If either argument is just a single edge set
192// descriptor it can be passed directly rather than in a singleton list.
193// Each edge set descriptor must be one of:
194// - A vector pointing towards an edge, indicating that single edge.
195// - A vector pointing towards a face, indicating all edges surrounding that face.
196// - A vector pointing towards a corner, indicating all edges touching that corner.
197// - The string `"X"`, indicating all X axis aligned edges.
198// - The string `"Y"`, indicating all Y axis aligned edges.
199// - The string `"Z"`, indicating all Z axis aligned edges.
200// - The string `"ALL"`, indicating all edges.
201// - The string `"NONE"`, indicating no edges at all.
202// - A 3x4 array, where each entry corresponds to one of the 12 edges and is set to 1 if that edge is included and 0 if the edge is not. The edge ordering is:
203// ```
204// [
205// [Y-Z-, Y+Z-, Y-Z+, Y+Z+],
206// [X-Z-, X+Z-, X-Z+, X+Z+],
207// [X-Y-, X+Y-, X-Y+, X+Y+]
208// ]
209// ```
210// .
211// You can specify edge descriptors directly by giving a vector, or you can use sums of the
212// named direction vectors described above. Below we show all of the edge sets you can
213// describe with sums of the direction vectors, and then we show some examples of combining
214// edge set descriptors.
215// Figure(3D,Big,VPD=300,NoScales): Vectors pointing toward an edge select that single edge
216// ydistribute(50) {
217// xdistribute(30) {
218// _show_edges(edges=BOT+RIGHT);
219// _show_edges(edges=BOT+BACK);
220// _show_edges(edges=BOT+LEFT);
221// _show_edges(edges=BOT+FRONT);
222// }
223// xdistribute(30) {
224// _show_edges(edges=FWD+RIGHT);
225// _show_edges(edges=BACK+RIGHT);
226// _show_edges(edges=BACK+LEFT);
227// _show_edges(edges=FWD+LEFT);
228// }
229// xdistribute(30) {
230// _show_edges(edges=TOP+RIGHT);
231// _show_edges(edges=TOP+BACK);
232// _show_edges(edges=TOP+LEFT);
233// _show_edges(edges=TOP+FRONT);
234// }
235// }
236// Figure(3D,Med,VPD=205,NoScales): Vectors pointing toward a face select all edges surrounding that face.
237// ydistribute(50) {
238// xdistribute(30) {
239// _show_edges(edges=LEFT);
240// _show_edges(edges=FRONT);
241// _show_edges(edges=RIGHT);
242// }
243// xdistribute(30) {
244// _show_edges(edges=TOP);
245// _show_edges(edges=BACK);
246// _show_edges(edges=BOTTOM);
247// }
248// }
249// Figure(3D,Big,VPD=300,NoScales): Vectors pointing toward a corner select all edges surrounding that corner.
250// ydistribute(50) {
251// xdistribute(30) {
252// _show_edges(edges=FRONT+LEFT+TOP);
253// _show_edges(edges=FRONT+RIGHT+TOP);
254// _show_edges(edges=FRONT+LEFT+BOT);
255// _show_edges(edges=FRONT+RIGHT+BOT);
256// }
257// xdistribute(30) {
258// _show_edges(edges=TOP+LEFT+BACK);
259// _show_edges(edges=TOP+RIGHT+BACK);
260// _show_edges(edges=BOT+LEFT+BACK);
261// _show_edges(edges=BOT+RIGHT+BACK);
262// }
263// }
264// Figure(3D,Med,VPD=205,NoScales): Named Edge Sets
265// ydistribute(50) {
266// xdistribute(30) {
267// _show_edges(edges="X");
268// _show_edges(edges="Y");
269// _show_edges(edges="Z");
270// }
271// xdistribute(30) {
272// _show_edges(edges="ALL");
273// _show_edges(edges="NONE");
274// }
275// }
276// Figure(3D,Big,VPD=310,NoScales): Next are some examples showing how you can combine edge descriptors to obtain different edge sets. You can specify the top front edge with a numerical vector or by combining the named direction vectors. If you combine them as a list you get all the edges around the front and top faces. Adding `except` removes an edge.
277// xdistribute(43){
278// _show_edges(_edges([0,-1,1]),toplabel=["edges=[0,-1,1]"]);
279// _show_edges(_edges(TOP+FRONT),toplabel=["edges=TOP+FRONT"]);
280// _show_edges(_edges([TOP,FRONT]),toplabel=["edges=[TOP,FRONT]"]);
281// _show_edges(_edges([TOP,FRONT],TOP+FRONT),toplabel=["edges=[TOP,FRONT]","except=TOP+FRONT"]);
282// }
283// Figure(3D,Big,VPD=310,NoScales): Using `except=BACK` removes the four edges surrounding the back face if they are present in the edge set. In the first example only one edge needs to be removed. In the second example we remove two of the Z-aligned edges. The third example removes all four back edges from the default edge set of all edges. You can explicitly give `edges="ALL"` but it is not necessary, since this is the default. In the fourth example, the edge set of Y-aligned edges contains no back edges, so the `except` parameter has no effect.
284// xdistribute(43){
285// _show_edges(_edges(BOT,BACK), toplabel=["edges=BOT","except=BACK"]);
286// _show_edges(_edges("Z",BACK), toplabel=["edges=\"Z\"", "except=BACK"]);
287// _show_edges(_edges("ALL",BACK), toplabel=["(edges=\"ALL\")", "except=BACK"]);
288// _show_edges(_edges("Y",BACK), toplabel=["edges=\"Y\"","except=BACK"]);
289// }
290// Figure(3D,Big,NoScales,VPD=310): On the left `except` is a list to remove two edges. In the center we show a corner edge set defined by a numerical vector, and at the right we remove that same corner edge set with named direction vectors.
291// xdistribute(52){
292// _show_edges(_edges("ALL",[FRONT+RIGHT,FRONT+LEFT]),
293// toplabel=["except=[FRONT+RIGHT,"," FRONT+LEFT]"]);
294// _show_edges(_edges([1,-1,1]),toplabel=["edges=[1,-1,1]"]);
295// _show_edges(_edges([TOP,BOT], TOP+RIGHT+FRONT),toplabel=["edges=[TOP,BOT]","except=TOP+RIGHT+FRONT"]);
296// }
297// Subsection: Specifying Corners
298// Modules operating on corners use two arguments to describe the corner set they will use: The `corners` argument
299// is a list of corner set descriptors to include in the corner set, and the `except` argument is a list of
300// corner set descriptors to remove from the corner set.
301// The default value for `corners` is `"ALL"`, the set of all corners.
302// The default value for `except` is the empty set, meaning no corners are removed.
303// If either argument is just a single corner set
304// descriptor it can be passed directly rather than in a singleton list.
305// Each corner set descriptor must be one of:
306// - A vector pointing towards a corner, indicating that corner.
307// - A vector pointing towards an edge indicating both corners at the ends of that edge.
308// - A vector pointing towards a face, indicating all the corners of that face.
309// - The string `"ALL"`, indicating all corners.
310// - The string `"NONE"`, indicating no corners at all.
311// - A length 8 vector where each entry corresponds to a corner and is 1 if the corner is included and 0 if it is excluded. The corner ordering is
312// ```
313// [X-Y-Z-, X+Y-Z-, X-Y+Z-, X+Y+Z-, X-Y-Z+, X+Y-Z+, X-Y+Z+, X+Y+Z+]
314// ```
315// .
316// You can specify corner descriptors directly by giving a vector, or you can use sums of the
317// named direction vectors described above. Below we show all of the corner sets you can
318// describe with sums of the direction vectors and then we show some examples of combining
319// corner set descriptors.
320// Figure(3D,Big,NoScales,VPD=300): Vectors pointing toward a corner select that corner.
321// ydistribute(55) {
322// xdistribute(35) {
323// _show_corners(corners=FRONT+LEFT+TOP);
324// _show_corners(corners=FRONT+RIGHT+TOP);
325// _show_corners(corners=FRONT+LEFT+BOT);
326// _show_corners(corners=FRONT+RIGHT+BOT);
327// }
328// xdistribute(35) {
329// _show_corners(corners=TOP+LEFT+BACK);
330// _show_corners(corners=TOP+RIGHT+BACK);
331// _show_corners(corners=BOT+LEFT+BACK);
332// _show_corners(corners=BOT+RIGHT+BACK);
333// }
334// }
335// Figure(3D,Big,NoScales,VPD=340): Vectors pointing toward an edge select the corners and the ends of the edge.
336// ydistribute(55) {
337// xdistribute(35) {
338// _show_corners(corners=BOT+RIGHT);
339// _show_corners(corners=BOT+BACK);
340// _show_corners(corners=BOT+LEFT);
341// _show_corners(corners=BOT+FRONT);
342// }
343// xdistribute(35) {
344// _show_corners(corners=FWD+RIGHT);
345// _show_corners(corners=BACK+RIGHT);
346// _show_corners(corners=BACK+LEFT);
347// _show_corners(corners=FWD+LEFT);
348// }
349// xdistribute(35) {
350// _show_corners(corners=TOP+RIGHT);
351// _show_corners(corners=TOP+BACK);
352// _show_corners(corners=TOP+LEFT);
353// _show_corners(corners=TOP+FRONT);
354// }
355// }
356// Figure(3D,Med,NoScales,VPD=225): Vectors pointing toward a face select the corners of the face.
357// ydistribute(55) {
358// xdistribute(35) {
359// _show_corners(corners=LEFT);
360// _show_corners(corners=FRONT);
361// _show_corners(corners=RIGHT);
362// }
363// xdistribute(35) {
364// _show_corners(corners=TOP);
365// _show_corners(corners=BACK);
366// _show_corners(corners=BOTTOM);
367// }
368// }
369// Figure(3D,Med,NoScales,VPD=200): Corners by name
370// xdistribute(35) {
371// _show_corners(corners="ALL");
372// _show_corners(corners="NONE");
373// }
374// Figure(3D,Big,NoScales,VPD=300): Next are some examples showing how you can combine corner descriptors to obtain different corner sets. You can specify corner sets numerically or by adding together named directions. The third example shows a list of two corner specifications, giving all the corners on the front face or the right face.
375// xdistribute(52){
376// _show_corners(_corners([1,-1,-1]),toplabel=["corners=[1,-1,-1]"]);
377// _show_corners(_corners(BOT+RIGHT+FRONT),toplabel=["corners=BOT+RIGHT+FRONT"]);
378// _show_corners(_corners([FRONT,RIGHT]), toplabel=["corners=[FRONT,RIGHT]"]);
379// }
380// Figure(3D,Big,NoScales,VPD=300): Corners for one edge, two edges, and all the edges except the two on one edge. Note that since the default is all edges, you only need to give the except argument in this case:
381// xdistribute(52){
382// _show_corners(_corners(FRONT+TOP), toplabel=["corners=FRONT+TOP"]);
383// _show_corners(_corners([FRONT+TOP,BOT+BACK]), toplabel=["corners=[FRONT+TOP,"," BOT+BACK]"]);
384// _show_corners(_corners("ALL",FRONT+TOP), toplabel=["(corners=\"ALL\")","except=FRONT+TOP"]);
385// }
386// Figure(3D,Med,NoScales,VPD=240): The first example shows a single corner removed from the top corners using a numerical vector. The second one shows removing a set of two corner descriptors from the implied set of all corners.
387// xdistribute(58){
388// _show_corners(_corners(TOP,[1,1,1]), toplabel=["corners=TOP","except=[1,1,1]"]);
389// _show_corners(_corners("ALL",[FRONT+RIGHT+TOP,FRONT+LEFT+BOT]),
390// toplabel=["except=[FRONT+RIGHT+TOP,"," FRONT+LEFT+BOT]"]);
391// }
392// Subsection: Anchoring of Non-Rectangular Objects and Anchor Type (atype)
393// We focused above on rectangular objects that have well-defined faces and edges aligned with the coordinate axes.
394// Things get difficult when the objects are curved, or even when their edges are not neatly aligned with the coordinate axes.
395// In these cases, the library may provide multiple different anchoring schemes, called the anchor types. When a module supports
396// multiple anchor types, use the `atype=` parameter to select the anchor type you need.
397// .
398// First consider the case of a simple rectangle whose corners have been rounded. Where should the anchors lie?
399// The default anchor type puts them in the same location as the anchors of an unrounded rectangle, which means that for
400// positive rounding radii, they are not even located on the perimeter of the object.
401// Figure(2D,Med,NoAxes): Default "box" atype anchors for a rounded {{rect()}}
402// rect([100,50], rounding=[10,0,0,-20],chamfer=[0,10,-20,0]) show_anchors();
403// Continues:
404// This choice enables you to position the box, or attach things to it, without regard to its rounding or chamfers. If you need to
405// anchor onto the roundovers or chamfers then you can use the "perim" anchor type:
406// Figure(2D,Med,NoAxes): The "perim" atype for a rounded and chamfered {{rect()}}
407// rect([100,50], rounding=[10,0,0,-20],chamfer=[0,10,-20,0],atype="perim") show_anchors();
408// Continues:
409// With this anchor type, the anchors are located on the perimeter. For positive roundings they point in the standard anchor direction;
410// for negative roundings they are parallel to the base. As noted above, for circles, cylinders, and spheres, the anchor point is
411// determined by choosing the point where the anchor vector intersects the shape. On a circle, this results in an anchor whose direction
412// matches the user provided anchor vector. But on an ellipse, something else happens:
413// Figure(2D,Med,NoAxes): Anchors on an ellipse. The red arrow shows a TOP+RIGHT anchor direction.
414// ellipse([70,30]) show_anchors();
415// stroke([[0,0],[45,45]], color="red",endcap2="arrow2");
416// Continues:
417// For a TOP+RIGHT anchor direction, the surface normal at the intersection point does not match the anchor direction,
418// so the direction of the anchor shown in blue does not match the direction specified, in red.
419// Anchors computed this way have anchor type "intersect". When a shape is concave, intersection anchors can produce
420// a result buried inside the shape's concavity. Consider the RIGHT anchor of this supershape example:
421// Figure(2D,Med,NoAxes): A supershape with "intersect" anchor type:
422// supershape(n=150,r=75, m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9,atype="intersect") show_anchors();
423// Continues:
424// A different anchor type called "hull" finds anchors that are on the convex hull of the shape.
425// Figure(2D,Med,NoAxes): A supershape with "hull" anchor type:
426// supershape(n=150,r=55, m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9,atype="hull") show_anchors();
427// Continues:
428// Hull anchoring works by creating the line (or plane in 3D) that is normal to the specified anchor direction, and
429// finding the point farthest from the center that intersects that line (or plane).
430// Figure(2D,Med,NoAxes): Finding the RIGHT and BACK+LEFT "hull" anchors
431// supershape(n=128,r=55, m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9,atype="hull") {
432// position(RIGHT) color_this("red")rect([1,90],anchor=LEFT);
433// attach(RIGHT)anchor_arrow2d(13);
434// attach(BACK+LEFT) {
435// anchor_arrow2d(13);
436// color_this("red")rect([30,1]);
437// }
438// }
439// Continues:
440// In the example the RIGHT anchor is found when the normal line (shown in red) is tangent to the shape at two points.
441// The anchor is then taken to be the midpoint. The BACK+LEFT anchor occurs with a single tangent point, and the
442// anchor point is located at the tangent point. For circles intersection is done to the exact circle, but for other
443// shapes these calculations are done on the point lists that defines the shape, so if you change the number of points
444// in the list, the precise location of the anchors can change. You can also get surprising results if your point list is badly chosen.
445// Figure(2D,Med,NoAxes): Circle anchor in blue. The red anchor is computed to a point list of a circle with 17 segments.
446// circle(r=31,$fn=128) attach(TOP)anchor_arrow2d(15);
447// region(circle(r=33,$fn=17)) {color("red")attach(TOP)anchor_arrow2d(13);}
448// Continues:
449// The figure shows a large horizontal offset due to a poor choice of sampling for the circular shape when using the "hull" anchor type.
450// The determination of "hull" or "intersect" anchors may depend on the location of the centerpoint used in the computation.
451// Some of the modules allow you to change the centerpoint using a `cp=` argument. If you need to change the centerpoint for
452// a module that does not provide this option, you can use the generic {{region()}} module, which will let you specify a centerpoint.
453// The default center point is the centroid, specified by "centroid". You can also choose "mean", which gives the mean of all
454// the data points, or "bbox", which gives the centerpoint of the bounding box for the data. Your last option for centerpoint is to
455// choose an arbitrary point that meets your needs.
456// Figure(2D,Med,NoAxes): The centerpoint for "intersect" anchors is located at the red dot
457// region(supershape(n=128,r=55, m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9),atype="intersect",cp=[0,30]) show_anchors();
458// color("red")back(30)circle(r=2,$fn=16);
459// Continues:
460// Note that all the anchors for an object have to be determined based on one anchor type and relative to the same centerpoint.
461// The supported anchor types for each module appear in the "Anchor Types" section of its entry.
462
463
464
465
466
467// Section: Attachment Positioning
468
469// Module: position()
470// Synopsis: Attaches children to a parent object at an anchor point.
471// SynTags: Trans
472// Topics: Attachments
473// See Also: attachable(), attach(), orient()
474// Usage:
475// PARENT() position(at) CHILDREN;
476// Description:
477// Attaches children to a parent object at an anchor point. For a step-by-step explanation
478// of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
479// Arguments:
480// at = The vector, or name of the parent anchor point to attach to.
481// Side Effects:
482// `$attach_anchor` for each `from=` anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
483// `$attach_to` is set to `undef`.
484// `$attach_norot` is set to `true`.
485// Example:
486// spheroid(d=20) {
487// position(TOP) cyl(l=10, d1=10, d2=5, anchor=BOTTOM);
488// position(RIGHT) cyl(l=10, d1=10, d2=5, anchor=BOTTOM);
489// position(FRONT) cyl(l=10, d1=10, d2=5, anchor=BOTTOM);
490// }
491module position(at,from)
492{
493 if (is_def(from)){
494 echo("'from' argument of position() has changed to 'at' and will be removed in a future version");
495 }
496 dummy0=assert(num_defined([at,from])==1, "Cannot give both `at` argument and the deprectated `from` argument to position()");
497 at = first_defined([at,from]);
498 req_children($children);
499 dummy1=assert($parent_geom != undef, "No object to position relative to.");
500 anchors = (is_vector(at)||is_string(at))? [at] : at;
501 two_d = _attach_geom_2d($parent_geom);
502 for (anchr = anchors) {
503 anch = _find_anchor(anchr, $parent_geom);
504 $attach_to = undef;
505 $attach_anchor = anch;
506 $attach_norot = true;
507 translate(anch[1]) children();
508 }
509}
510
511
512
513// Module: orient()
514// Synopsis: Orients children's tops in the directon of the specified anchor.
515// SynTags: Trans
516// Topics: Attachments
517// See Also: attachable(), attach(), position()
518// Usage:
519// PARENT() orient(anchor, [spin]) CHILDREN;
520// Description:
521// Orients children such that their top is tilted in the direction of the specified parent anchor point.
522// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
523// Arguments:
524// anchor = The anchor on the parent which you want to match the orientation of.
525// spin = The spin to add to the children. (Overrides anchor spin.)
526// Side Effects:
527// `$attach_to` is set to `undef`.
528// `$attach_norot` is set to `true`.
529// Example: When orienting to an anchor, the spin of the anchor may cause confusion:
530// prismoid([50,50],[30,30],h=40) {
531// position(TOP+RIGHT)
532// orient(RIGHT)
533// prismoid([30,30],[0,5],h=20,anchor=BOT+LEFT);
534// }
535// Example: You can override anchor spin with `spin=`.
536// prismoid([50,50],[30,30],h=40) {
537// position(TOP+RIGHT)
538// orient(RIGHT,spin=0)
539// prismoid([30,30],[0,5],h=20,anchor=BOT+LEFT);
540// }
541// Example: Or you can anchor the child from the back
542// prismoid([50,50],[30,30],h=40) {
543// position(TOP+RIGHT)
544// orient(RIGHT)
545// prismoid([30,30],[0,5],h=20,anchor=BOT+BACK);
546// }
547module orient(anchor, spin) {
548 req_children($children);
549 check=
550 assert($parent_geom != undef, "No parent to orient from!")
551 assert(is_string(anchor) || is_vector(anchor));
552 anch = _find_anchor(anchor, $parent_geom);
553 two_d = _attach_geom_2d($parent_geom);
554 fromvec = two_d? BACK : UP;
555 spin = default(spin, anch[3]);
556 dummy=assert(is_finite(spin));
557
558 $attach_to = undef;
559 $attach_norot = true;
560 if (two_d)
561 rot(spin)rot(from=fromvec, to=anch[2]) children();
562 else
563 rot(spin, from=fromvec, to=anch[2]) children();
564}
565
566
567
568// Module: align()
569// Synopsis: Position and orient children with alignment to parent edges.
570// SynTags: Trans
571// Topics: Attachments
572// See Also: attachable(), attach(), position(), orient()
573// Usage:
574// PARENT() align(anchor, [orient], [spin], [inside=]) CHILDREN;
575// Description:
576// Positions children to the specified anchor(s) on the parent and anchors the
577// children so that they are aligned with the edge(s) of the parent at those parent anchors.
578// You can specify a parent anchor point in `orient` and in this case, the top of the child
579// is tilted in the direction of that anchor.
580// This means you can easily place children so they are aligned flush with edges of the parent.
581// In contrast, with {{position()}} you will have to work out the correct anchor for the children
582// which is not always obvious. It also enables you to place several children that have different
583// anchors, which would otherwise require several {{position()}} calls. The inside argument
584// causes the object to appear inside the parent for use with {{diff()}}.
585// .
586// When you use `align()`, the `orient=` and `anchor=` arguments to the child objects are overriden,
587// so they do not have any effect. The `spin=` argument to the child still applies.
588// Arguments:
589// anchor = parent anchor or list of parent anchors for positioning children
590// orient = parent anchor to give direction for orienting the children. Default: UP
591// spin = spin in degrees for rotating the children. Default: Derived from orient anchor
592// ---
593// inside = if true, place object inside the parent instead of outside. Default: false
594// Side Effects:
595// `$attach_anchor` for each anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
596// `$attach_to` is set to `undef`.
597// `$attach_norot` is set to `true`.
598// `$anchor_override` is set to the anchor required for proper positioning of the child.
599// if inside is true then set default tag to "remove"
600// Example: Child would require anchor of RIGHT+FRONT+BOT if placed with {{position()}}.
601// cuboid([50,40,15])
602// align(RIGHT+FRONT+TOP)
603// color("lightblue")prismoid([10,5],[7,4],height=4);
604// Example: Child requires a different anchor for each position, so explicit specification of the anchor for children is impossible in this case, without using two separate commands.
605// cuboid([50,40,15])
606// align([RIGHT+TOP,LEFT+TOP])
607// color("lightblue")prismoid([10,5],[7,4],height=4);
608// Example: If you try to spin your child, the spin happens after the alignment anchor, so the child will not be flush:
609// cuboid([50,40,15])
610// align([RIGHT+TOP])
611// color("lightblue")
612// prismoid([10,5],[7,4],height=4,spin=90);
613// Example: You can instead spin the attached children using the spin parameter to `align()`. In this example, the required anchor is BOT+FWD, which is less obvious.
614// cuboid([50,40,15])
615// align(RIGHT+TOP,spin=90)
616// color("lightblue")prismoid([10,5],[7,4],height=4);
617// Example: Here the child is oriented to the RIGHT, so it appears flush with the top. In this case you don't have to figure out that the required child anchor is BOT+BACK.
618// cuboid([50,40,15])
619// align(RIGHT+TOP,RIGHT)
620// color("lightblue")prismoid([10,5],[7,4],height=4);
621// Example: If you change the orientation the child still appears aligned flush in its changed orientation:
622// cuboid([50,40,15])
623// align(RIGHT+TOP,DOWN)
624// color("lightblue")prismoid([10,5],[7,4],height=4);
625// Example: Objects on the right already have nonzero spin by default, so setting spin=0 changes the spin:
626// prismoid(50,30,25){
627// align(RIGHT+TOP,RIGHT,spin=0)
628// color("lightblue")prismoid([10,5],[7,4],height=4);
629// align(RIGHT+BOT,RIGHT)
630// color("green")prismoid([10,5],[7,4],height=4);
631// }
632// Example: Setting inside=true enables us to subtract the child from the parent with {{diff()}. The "remove" tag is automatically applied when you set `inside=true`.
633// diff()
634// cuboid([40,30,10])
635// move(.1*[0,-1,1])
636// align(FRONT+TOP,inside=true)
637// prismoid([10,5],[7,5],height=4);
638module align(anchor,orient=UP,spin,inside=false)
639{
640 req_children($children);
641 dummy1=assert($parent_geom != undef, "No object to align to.")
642 assert(is_string(orient) || is_vector(orient),"Bad orient value");
643 position_anchors = (is_vector(anchor)||is_string(anchor))? [anchor] : anchor;
644 two_d = _attach_geom_2d($parent_geom);
645 fromvec = two_d? BACK : UP;
646
647 orient_anch = _find_anchor(orient, $parent_geom);
648 spin = default(spin, orient_anch[3]);
649 dummy2=assert(is_finite(spin));
650
651 $attach_to = undef;
652 $attach_norot = true;
653
654 factor = inside?1:-1;
655
656 for (thisanch = position_anchors) {
657 pos_anch = _find_anchor(thisanch, $parent_geom);
658 init_anch = two_d ? rot(from=orient_anch[2], to=fromvec, p=zrot(-spin,pos_anch[0]))
659 : rot(spin, from=fromvec, to=orient_anch[2], reverse=true, p=pos_anch[0]);
660 quant_anch = [for(v=init_anch) sign(round(v))];
661 $anchor_override = two_d && quant_anch.y!=0 ? [quant_anch.x,factor*quant_anch.y]
662 : !two_d && quant_anch.z!=0 ? [quant_anch.x,quant_anch.y, factor*quant_anch.z]
663 : factor*quant_anch;
664 $attach_anchor = pos_anch;
665 translate(pos_anch[1]) {
666 if (two_d)
667 rot(spin)rot(from=fromvec, to=orient_anch[2])
668 default_tag("remove",inside) children();
669 else
670 rot(spin, from=fromvec, to=orient_anch[2])
671 default_tag("remove",inside) children();
672 }
673 }
674}
675
676
677
678
679
680// Module: attach()
681// Synopsis: Attaches children to a parent object at an anchor point and orientation.
682// SynTags: Trans
683// Topics: Attachments
684// See Also: attachable(), position(), face_profile(), edge_profile(), corner_profile()
685// Usage:
686// PARENT() attach(from, [overlap=], [norot=]) CHILDREN;
687// PARENT() attach(from, to, [overlap=], [norot=]) CHILDREN;
688// Description:
689// Attaches children to a parent object at an anchor point and orientation. Attached objects will
690// be overlapped into the parent object by a little bit, as specified by the `$overlap`
691// value (0 by default), or by the overriding `overlap=` argument. This is to prevent OpenSCAD
692// from making non-manifold objects. You can define `$overlap=` as an argument in a parent
693// module to set the default for all attachments to it. For a step-by-step explanation of
694// attachments, see the [Attachments Tutorial](Tutorial-Attachments).
695// Arguments:
696// from = The vector, or name of the parent anchor point to attach to.
697// to = Optional name of the child anchor point. If given, orients the child such that the named anchors align together rotationally.
698// ---
699// overlap = Amount to sink child into the parent. Equivalent to `down(X)` after the attach. This defaults to the value in `$overlap`, which is `0` by default.
700// norot = If true, don't rotate children when attaching to the anchor point. Only translate to the anchor point.
701// Side Effects:
702// `$idx` is set to the index number of each anchor if a list of anchors is given. Otherwise is set to `0`.
703// `$attach_anchor` for each `from=` anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
704// `$attach_to` is set to the value of the `to=` argument, if given. Otherwise, `undef`
705// `$attach_norot` is set to the value of the `norot=` argument.
706// Example:
707// spheroid(d=20) {
708// attach(TOP) down(1.5) cyl(l=11.5, d1=10, d2=5, anchor=BOTTOM);
709// attach(RIGHT, BOTTOM) down(1.5) cyl(l=11.5, d1=10, d2=5);
710// attach(FRONT, BOTTOM, overlap=1.5) cyl(l=11.5, d1=10, d2=5);
711// }
712module attach(from, to, overlap, norot=false)
713{
714 req_children($children);
715 assert($parent_geom != undef, "No object to attach to!");
716 overlap = (overlap!=undef)? overlap : $overlap;
717 anchors = (is_vector(from)||is_string(from))? [from] : from;
718 for ($idx = idx(anchors)) {
719 anchr = anchors[$idx];
720 anch = _find_anchor(anchr, $parent_geom);
721 two_d = _attach_geom_2d($parent_geom);
722 $attach_to = to;
723 $attach_anchor = anch;
724 $attach_norot = norot;
725 olap = two_d? [0,-overlap,0] : [0,0,-overlap];
726 if (norot || (norm(anch[2]-UP)<1e-9 && anch[3]==0)) {
727 translate(anch[1]) translate(olap) children();
728 } else {
729 fromvec = two_d? BACK : UP;
730 translate(anch[1]) rot(anch[3],from=fromvec,to=anch[2]) translate(olap) children();
731 }
732 }
733}
734
735// Section: Tagging
736
737// Module: tag()
738// Synopsis: Assigns a tag to an object
739// Topics: Attachments
740// See Also: force_tag(), recolor(), hide(), show_only(), diff(), intersect()
741// Usage:
742// PARENT() tag(tag) CHILDREN;
743// Description:
744// Assigns the specified tag to all of the children. Note that if you want
745// to apply a tag to non-tag-aware objects you need to use {{force_tag()}} instead.
746// This works by setting the `$tag` variable, but it provides extra error checking and
747// handling of scopes. You may set `$tag` directly yourself, but this is not recommended.
748// .
749// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
750// Arguments:
751// tag = tag string, which must not contain any spaces.
752// Side Effects:
753// Sets `$tag` to the tag you specify, possibly with a scope prefix.
754// Example(3D): Applies the tag to both cuboids instead of having to repeat `$tag="remove"` for each one.
755// diff("remove")
756// cuboid(10){
757// position(TOP) cuboid(3);
758// tag("remove")
759// {
760// position(FRONT) cuboid(3);
761// position(RIGHT) cuboid(3);
762// }
763// }
764module tag(tag)
765{
766 req_children($children);
767 check=
768 assert(is_string(tag),"tag must be a string")
769 assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
770 $tag = str($tag_prefix,tag);
771 children();
772}
773
774
775// Module: force_tag()
776// Synopsis: Assigns a tag to a non-attachable object.
777// Topics: Attachments
778// See Also: tag(), recolor(), hide(), show_only(), diff(), intersect()
779// Usage:
780// PARENT() force_tag([tag]) CHILDREN;
781// Description:
782// You use this module when you want to make a non-attachable or non-BOSL2 module respect tags.
783// It applies to its children the tag specified (or the tag currently in force if you don't specify a tag),
784// making a final determination about whether to show or hide the children.
785// This means that tagging in children's children will be ignored.
786// This module is specifically provided for operating on children that are not tag aware such as modules
787// that don't use {{attachable()}} or built in modules such as
788// - `polygon()`
789// - `projection()`
790// - `polyhedron()` (or use [`vnf_polyhedron()`](vnf.scad#vnf_polyhedron))
791// - `linear_extrude()` (or use [`linear_sweep()`](regions.scad#linear_sweep))
792// - `rotate_extrude()`
793// - `surface()`
794// - `import()`
795// - `difference()`
796// - `intersection()`
797// - `hull()`
798// .
799// When you use tag-based modules like {{diff()}} with a non-attachable module, the result may be puzzling.
800// Any time a test occurs for display of child() that test will succeed. This means that when diff() checks
801// to see if it should show a module it will show it, and when diff() checks to see if it should subtract the module
802// it will subtract it. The result will be a hole, possibly with zero-thickness edges or faces. In order to
803// get the correct behavior, every non-attachable module needs an invocation of force_tag, even ones
804// that are not tagged.
805// .
806// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
807// Arguments:
808// tag = tag string, which must not contain any spaces
809// Side Effects:
810// Sets `$tag` to the tag you specify, possibly with a scope prefix.
811// Example(2D): This example produces the full square without subtracting the "remove" item. When you use non-attachable modules with tags, results are unpredictable.
812// diff()
813// {
814// polygon(square(10));
815// move(-[.01,.01])polygon(square(5),$tag="remove");
816// }
817// Example(2D): Adding force_tag() fixes the model. Note you need to add it to *every* non-attachable module, even the untagged ones, as shown here.
818// diff()
819// {
820// force_tag()
821// polygon(square(10));
822// force_tag("remove")
823// move(-[.01,.01])polygon(square(5));
824// }
825module force_tag(tag)
826{
827 req_children($children);
828 check1=assert(is_undef(tag) || is_string(tag),"tag must be a string");
829 $tag = str($tag_prefix,default(tag,$tag));
830 assert(undef==str_find($tag," "),str("Tag string \"",$tag,"\" contains a space, which is not allowed"));
831 if(_is_shown())
832 show_all()
833 children();
834}
835
836
837
838// Module: default_tag()
839// Synopsis: Sets a default tag for all children.
840// Topics: Attachments
841// See Also: force_tag(), recolor(), hide(), show_only(), diff(), intersect()
842// Usage:
843// PARENT() default_tag(tag) CHILDREN;
844// Description:
845// Sets a default tag for all of the children. This is intended to be used to set a tag for a whole module
846// that is then used outside the module, such as setting the tag to "remove" for easy operation with {{diff()}}.
847// The default_tag() module sets the `$tag` variable only if it is not already
848// set so you can have a module set a default tag of "remove" but that tag can be overridden by a {{tag()}}
849// in force from a parent. If you use {{tag()}} it will override any previously
850// specified tag from a parent, which can be very confusing to a user trying to change the tag on a module.
851// The `do_tag` parameter allows you to apply a default tag conditionally without having to repeat the children.
852// .
853// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
854// Arguments:
855// tag = tag string, which must not contain any spaces.
856// do_tag = if false do not set the tag.
857// Side Effects:
858// Sets `$tag` to the tag you specify, possibly with a scope prefix.
859// Example(3D): The module thing() is defined with {{tag()}} and the user applied tag of "keep_it" is ignored, leaving the user puzzled.
860// module thing() { tag("remove") cuboid(10);}
861// diff()
862// cuboid(20){
863// position(TOP) thing();
864// position(RIGHT) tag("keep_it") thing();
865// }
866// Example(3D): Using default_tag() fixes this problem: the user applied tag does not get overridden by the tag hidden in the module definition.
867// module thing() { default_tag("remove") cuboid(10);}
868// diff()
869// cuboid(20){
870// position(TOP) thing();
871// position(RIGHT) tag("keep_it") thing();
872// }
873module default_tag(tag,do_tag=true)
874{
875 if ($tag=="" && do_tag) tag(tag) children();
876 else children();
877}
878
879
880// Module: tag_scope()
881// Synopsis: Creates a new tag scope.
882// See Also: tag(), force_tag(), default_tag()
883// Topics: Attachments
884// Usage:
885// tag_scope([scope]) CHILDREN;
886// Description:
887// Creates a tag scope with locally altered tag names to avoid tag name conflict with other code.
888// This is necessary when writing modules because the module's caller might happen to use the same tags.
889// Note that if you directly set the `$tag` variable then tag scoping will not work correctly.
890// Side Effects:
891// `$tag_prefix` is set to the value of `scope=` if given, otherwise is set to a random string.
892// Example: In this example the ring module uses "remove" tags which will conflict with use of the same tags by the parent.
893// module ring(r,h,w=1,anchor,spin,orient)
894// {
895// tag_scope("ringscope")
896// attachable(anchor,spin,orient,r=r,h=h){
897// diff()
898// cyl(r=r,h=h)
899// tag("remove") cyl(r=r-w,h=h+1);
900// children();
901// }
902// }
903// // Calling the module using "remove" tags
904// // will conflict with internal tag use in
905// // the ring module.
906// $fn=32;
907// diff(){
908// ring(10,7,w=4);
909// tag("remove")ring(8,8);
910// tag("remove")diff("rem"){
911// ring(9.5,8,w=1);
912// tag("rem")ring(9.5,8,w=.3);
913// }
914// }
915module tag_scope(scope){
916 req_children($children);
917 scope = is_undef(scope) ? rand_str(20) : scope;
918 assert(is_string(scope), "scope must be a string");
919 assert(undef==str_find(scope," "),str("Scope string \"",scope,"\" contains a space, which is not allowed"));
920 $tag_prefix=scope;
921 children();
922}
923
924
925// Section: Attachment Modifiers
926
927// Module: diff()
928// Synopsis: Performs a differencing operation using tags rather than hierarchy to control what happens.
929// Topics: Attachments
930// See Also: tag(), force_tag(), recolor(), show_only(), hide(), tag_diff(), intersect(), tag_intersect()
931// Usage:
932// diff([remove], [keep]) PARENT() CHILDREN;
933// Description:
934// Performs a differencing operation using tags to control what happens. This is specifically intended to
935// address the situation where you want differences between a parent and child object, something
936// that is impossible with the native difference() module.
937// The children to diff are grouped into three categories, regardless of nesting level.
938// The `remove` argument is a space delimited list of tags specifying objects to
939// subtract. The `keep` argument is a similar list of tags giving objects to be kept.
940// Objects not matching either the `remove` or `keep` lists form the third category of base objects.
941// To produce its output, diff() forms the union of all the base objects and then
942// subtracts all the objects with tags in `remove`. Finally it adds in objects listed in `keep`.
943// Attachable objects should be tagged using {{tag()}}
944// and non-attachable objects with {{force_tag()}}.
945// .
946// Remember when using tagged operations with that the operations don't happen in hierarchical order, since
947// the point of tags is to break the hierarchy. If you tag an object with a keep tag, nothing will be
948// subtracted from it, no matter where it appears because kept objects are unioned in at the end.
949// If you want a child of an object tagged with a remove tag to stay in the model it may be
950// better to give it a tag that is not a remove tag or a keep tag. Such an object *will* be subject to
951// subtractions from other remove-tagged objects.
952// .
953// Note that `diff()` invokes its children three times.
954// .
955// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
956// Arguments:
957// remove = String containing space delimited set of tag names of children to difference away. Default: `"remove"`
958// keep = String containing space delimited set of tag names of children to keep; that is, to union into the model after differencing is completed. Default: `"keep"`
959// Example: Diffing using default tags
960// diff()
961// cuboid(50) {
962// tag("remove") attach(TOP) sphere(d=40);
963// tag("keep") attach(CTR) cylinder(h=40, d=10);
964// }
965// Example: The "hole" items are subtracted from everything else. The other tags can be anything you find convenient.
966// diff("hole")
967// tag("body")sphere(d=100) {
968// tag("pole") zcyl(d=55, h=100); // attach() not needed for center-to-center.
969// tag("hole") {
970// xcyl(d=55, h=101);
971// ycyl(d=55, h=101);
972// }
973// tag("axle")zcyl(d=15, h=140);
974// }
975// Example:
976// diff(keep="axle")
977// sphere(d=100) {
978// tag("axle")xcyl(d=40, l=120);
979// tag("remove")cuboid([40,120,100]);
980// }
981// Example: Masking
982// diff()
983// cube([80,90,100], center=true) {
984// edge_mask(FWD)
985// rounding_edge_mask(l=max($parent_size)*1.01, r=25);
986// }
987// Example: Here we subtract the parent object from the child. Because tags propagate to children we need to clear the "remove" tag from the child.
988// diff()
989// tag("remove")cuboid(10)
990// tag("")position(RIGHT+BACK)cyl(r=8,h=9);
991// Example(3D,VPR=[104,0,200], VPT=[-0.9,3.03, -0.74], VPD=19,NoAxes,NoScales): A pipe module that subtracts its interior when you call it using diff(). Normally if you union two pipes together, you'll get interfering walls at the intersection, but not here:
992// $fn=16;
993// // This module must be called by subtracting with "diff"
994// module pipe(length, od, id) {
995// // Strip the tag the user is using to subtract
996// tag("")cylinder(h=length, d=od, center=true);
997// // Leave the tag alone here, so this one is removed
998// cylinder(h=length+.02, d=id, center=true);
999// }
1000// // Draw some intersecting pipes
1001// diff(){
1002// tag("remove"){
1003// pipe(length=5, od=2, id=1.9);
1004// zrot(10)xrot(75)
1005// pipe(length=5, od=2, id=1.9);
1006// }
1007// // The orange bar has its center removed
1008// color("orange") down(1) xcyl(h=8, d=1);
1009// // "keep" preserves the interior of the blue bar intact
1010// tag("keep") recolor("blue") up(1) xcyl(h=8, d=1);
1011// }
1012// // Objects outside the diff don't have pipe interiors removed
1013// color("purple") down(2.2) ycyl(h=8, d=0.3);
1014// Example(3D,NoScales,NoAxes): Nested diff() calls work as expected, but be careful of reusing tag names, even hidden in submodules.
1015// $fn=32;
1016// diff("rem1")
1017// cyl(r=10,h=10){
1018// diff("rem2",$tag="rem1"){
1019// cyl(r=8,h=11);
1020// tag("rem2")diff("rem3"){
1021// cyl(r=6,h=12);
1022// tag("rem3")cyl(r=4,h=13);
1023// }
1024// }
1025// }
1026// Example: This example shows deep nesting, where all the differences cross levels. Unlike the preceding example, each cylinder is positioned relative to its parent. Note that it suffices to use two remove tags, alternating between them at each level.
1027// $fn=32;
1028// diff("remA")
1029// cyl(r=9, h=6)
1030// tag("remA")diff("remB")
1031// left(.2)position(RIGHT)cyl(r=8,h=7,anchor=RIGHT)
1032// tag("remB")diff("remA")
1033// left(.2)position(LEFT)cyl(r=7,h=7,anchor=LEFT)
1034// tag("remA")diff("remB")
1035// left(.2)position(LEFT)cyl(r=6,h=8,anchor=LEFT)
1036// tag("remB")diff("remA")
1037// right(.2)position(RIGHT)cyl(r=5,h=9,anchor=RIGHT)
1038// tag("remA")diff("remB")
1039// right(.2)position(RIGHT)cyl(r=4,h=10,anchor=RIGHT)
1040// tag("remB")left(.2)position(LEFT)cyl(r=3,h=11,anchor=LEFT);
1041// Example(3D,NoAxes,NoScales): When working with Non-Attachables like rotate_extrude() you must apply {{force_tag()}} to every non-attachable object.
1042// back_half()
1043// diff("remove")
1044// cuboid(40) {
1045// attach(TOP)
1046// recolor("lightgreen")
1047// cyl(l=10,d=30);
1048// position(TOP+RIGHT)
1049// force_tag("remove")
1050// xrot(90)
1051// rotate_extrude()
1052// right(20)
1053// circle(5);
1054// }
1055// Example: Here is another example where two children are intersected using the native intersection operator, and then tagged with {{force_tag()}}. Note that because the children are at the same level, you don't need to use a tagged operator for their intersection.
1056// $fn=32;
1057// diff()
1058// cuboid(10){
1059// force_tag("remove")intersection()
1060// {
1061// position(RIGHT) cyl(r=7,h=15);
1062// position(LEFT) cyl(r=7,h=15);
1063// }
1064// tag("keep")cyl(r=1,h=9);
1065// }
1066// Example: In this example the children that are subtracted are each at different nesting levels, with a kept object in between.
1067// $fn=32;
1068// diff()
1069// cuboid(10){
1070// tag("remove")cyl(r=4,h=11)
1071// tag("keep")cyl(r=3,h=17)
1072// tag("remove")position(RIGHT)cyl(r=2,h=18);
1073// }
1074// Example: Combining tag operators can be tricky. Here the `diff()` operation keeps two tags, "fullkeep" and "keep". Then {{intersect()}} intersects the "keep" tagged item with everything else, but keeps the "fullkeep" object.
1075// $fn=32;
1076// intersect("keep","fullkeep")
1077// diff(keep="fullkeep keep")
1078// cuboid(10){
1079// tag("remove")cyl(r=4,h=11);
1080// tag("keep") position(RIGHT)cyl(r=8,h=12);
1081// tag("fullkeep")cyl(r=1,h=12);
1082// }
1083// Example: In this complex example we form an intersection, subtract an object, and keep some objects. Note that for the small cylinders on either side, marking them as "keep" or removing their tag gives the same effect. This is because without a tag they become part of the intersection and the result ends up the same. For the two cylinders at the back, however, the result is different. With "keep" the cylinder on the left appears whole, but without it, the cylinder at the back right is subject to intersection.
1084// $fn=64;
1085// diff()
1086// intersect(keep="remove keep")
1087// cuboid(10,$thing="cube"){
1088// tag("intersect"){
1089// position(RIGHT) cyl(r=5.5,h=15)
1090// tag("")cyl(r=2,h=10);
1091// position(LEFT) cyl(r=5.54,h=15)
1092// tag("keep")cyl(r=2,h=10);
1093// }
1094// // Untagged it is in the intersection
1095// tag("") position(BACK+RIGHT)
1096// cyl(r=2,h=10,anchor=CTR);
1097// // With keep the full cylinder appears
1098// tag("keep") position(BACK+LEFT)
1099// cyl(r=2,h=10,anchor=CTR);
1100// tag("remove") cyl(r=3,h=15);
1101// }
1102module diff(remove="remove", keep="keep")
1103{
1104 req_children($children);
1105 assert(is_string(remove),"remove must be a string of tags");
1106 assert(is_string(keep),"keep must be a string of tags");
1107 if (_is_shown())
1108 {
1109 difference() {
1110 hide(str(remove," ",keep)) children();
1111 show_only(remove) children();
1112 }
1113 }
1114 show_int(keep)children();
1115}
1116
1117
1118// Module: tag_diff()
1119// Synopsis: Performs a {{diff()}} and then sets a tag on the result.
1120// Topics: Attachments
1121// See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), intersect(), tag_intersect()
1122// Usage:
1123// tag_diff(tag, [remove], [keep]) PARENT() CHILDREN;
1124// Description:
1125// Perform a differencing operation in the manner of {{diff()}} using tags to control what happens,
1126// and then tag the resulting difference object with the specified tag. This forces the specified
1127// tag to be resolved at the level of the difference operation. In most cases, this is not necessary,
1128// but if you have kept objects and want to operate on this difference object as a whole object using
1129// more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
1130// .
1131// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1132// Arguments:
1133// tag = Tag string to apply to this difference object
1134// remove = String containing space delimited set of tag names of children to difference away. Default: `"remove"`
1135// keep = String containing space delimited set of tag names of children to keep; that is, to union into the model after differencing is completed. Default: `"keep"`
1136// Side Effects:
1137// Sets `$tag` to the tag you specify, possibly with a scope prefix.
1138// Example: In this example we have a difference with a kept object that is then subtracted from a cube, but we don't want the kept object to appear in the final output, so this result is wrong:
1139// diff("rem"){
1140// cuboid([20,10,30],anchor=FRONT);
1141// tag("rem")diff("remove","keep"){
1142// cuboid([10,10,20]);
1143// tag("remove")cuboid([11,11,5]);
1144// tag("keep")cuboid([2,2,20]);
1145// }
1146// }
1147// Example: Using tag_diff corrects the problem:
1148// diff("rem"){
1149// cuboid([20,10,30],anchor=FRONT);
1150// tag_diff("rem","remove","keep"){
1151// cuboid([10,10,20]);
1152// tag("remove")cuboid([11,11,5]);
1153// tag("keep")cuboid([2,2,20]);
1154// }
1155// }
1156// Example: This concentric cylinder example uses "keep" and produces the wrong result. The kept cylinder gets kept in the final output instead of subtracted. This happens even when we make sure to change the `keep` argument at the top level {{diff()}} call.
1157// diff("rem","nothing")
1158// cyl(r=8,h=6)
1159// tag("rem")diff()
1160// cyl(r=7,h=7)
1161// tag("remove")cyl(r=6,h=8)
1162// tag("keep")cyl(r=5,h=9);
1163// Example: Changing to tag_diff() causes the kept cylinder to be subtracted, producing the desired result:
1164// diff("rem")
1165// cyl(r=8,h=6)
1166// tag_diff("rem")
1167// cyl(r=7,h=7)
1168// tag("remove")cyl(r=6,h=8)
1169// tag("keep")cyl(r=5,h=9);
1170module tag_diff(tag,remove="remove", keep="keep")
1171{
1172 req_children($children);
1173 assert(is_string(remove),"remove must be a string of tags");
1174 assert(is_string(keep),"keep must be a string of tags");
1175 assert(is_string(tag),"tag must be a string");
1176 assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
1177 $tag=str($tag_prefix,tag);
1178 if (_is_shown())
1179 show_all(){
1180 difference() {
1181 hide(str(remove," ",keep)) children();
1182 show_only(remove) children();
1183 }
1184 show_only(keep)children();
1185 }
1186}
1187
1188
1189// Module: intersect()
1190// Synopsis: Perform an intersection operation on children using tags rather than hierarchy to control what happens.
1191// Topics: Attachments
1192// See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), tag_diff(), tag_intersect()
1193// Usage:
1194// intersect([intersect], [keep]) PARENT() CHILDREN;
1195// Description:
1196// Performs an intersection operation on its children, using tags to
1197// determine what happens. This is specifically intended to address
1198// the situation where you want intersections involving a parent and
1199// child object, something that is impossible with the native
1200// intersection() module. This module treats the children in three
1201// groups: objects matching the tags listed in `intersect`, objects
1202// matching tags listed in `keep`, and the remaining objects that
1203// don't match any of the listed tags. The intersection is computed
1204// between the union of the `intersect` tagged objects and union of the objects that don't
1205// match any of the listed tags. Finally the objects listed in `keep` are
1206// unioned with the result. Attachable objects should be tagged using {{tag()}}
1207// and non-attachable objects with {{force_tag()}}.
1208// .
1209// Note that `intersect()` invokes its children three times.
1210// .
1211// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1212// Arguments:
1213// intersect = String containing space delimited set of tag names of children to intersect. Default: "intersect"
1214// keep = String containing space delimited set of tag names of children to keep whole. Default: "keep"
1215// Example:
1216// intersect("mask", keep="axle")
1217// sphere(d=100) {
1218// tag("mask")cuboid([40,100,100]);
1219// tag("axle")xcyl(d=40, l=100);
1220// }
1221// Example: Combining tag operators can be tricky. Here the {{diff()}} operation keeps two tags, "fullkeep" and "keep". Then `intersect()` intersects the "keep" tagged item with everything else, but keeps the "fullkeep" object.
1222// $fn=32;
1223// intersect("keep","fullkeep")
1224// diff(keep="fullkeep keep")
1225// cuboid(10){
1226// tag("remove")cyl(r=4,h=11);
1227// tag("keep") position(RIGHT)cyl(r=8,h=12);
1228// tag("fullkeep")cyl(r=1,h=12);
1229// }
1230// Example: In this complex example we form an intersection, subtract an object, and keep some objects. Note that for the small cylinders on either side, marking them as "keep" or removing their tag gives the same effect. This is because without a tag they become part of the intersection and the result ends up the same. For the two cylinders at the back, however, the result is different. With "keep" the cylinder on the left appears whole, but without it, the cylinder at the back right is subject to intersection.
1231// $fn=64;
1232// diff()
1233// intersect(keep="remove keep")
1234// cuboid(10,$thing="cube"){
1235// tag("intersect"){
1236// position(RIGHT) cyl(r=5.5,h=15)
1237// tag("")cyl(r=2,h=10);
1238// position(LEFT) cyl(r=5.54,h=15)
1239// tag("keep")cyl(r=2,h=10);
1240// }
1241// // Untagged it is in the intersection
1242// tag("") position(BACK+RIGHT)
1243// cyl(r=2,h=10,anchor=CTR);
1244// // With keep the full cylinder appears
1245// tag("keep") position(BACK+LEFT)
1246// cyl(r=2,h=10,anchor=CTR);
1247// tag("remove") cyl(r=3,h=15);
1248// }
1249module intersect(intersect="intersect",keep="keep")
1250{
1251 assert(is_string(intersect),"intersect must be a string of tags");
1252 assert(is_string(keep),"keep must be a string of tags");
1253 intersection(){
1254 show_only(intersect) children();
1255 hide(str(intersect," ",keep)) children();
1256 }
1257 show_int(keep) children();
1258}
1259
1260
1261// Module: tag_intersect()
1262// Synopsis: Performs an {{intersect()}} and then tags the result.
1263// Topics: Attachments
1264// See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), tag_diff(), intersect()
1265// Usage:
1266// tag_intersect(tag, [intersect], [keep]) PARENT() CHILDREN;
1267// Description:
1268// Perform an intersection operation in the manner of {{intersect()}} using tags to control what happens,
1269// and then tag the resulting difference object with the specified tag. This forces the specified
1270// tag to be resolved at the level of the intersect operation. In most cases, this is not necessary,
1271// but if you have kept objects and want to operate on this difference object as a whole object using
1272// more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
1273// .
1274// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1275// Arguments:
1276// tag = Tag to set for the intersection
1277// intersect = String containing space delimited set of tag names of children to intersect. Default: "intersect"
1278// keep = String containing space delimited set of tag names of children to keep whole. Default: "keep"
1279// Side Effects:
1280// Sets `$tag` to the tag you specify, possibly with a scope prefix.
1281// Example: Without `tag_intersect()` the kept object is not included in the difference.
1282// $fn=32;
1283// diff()
1284// cuboid([20,15,9])
1285// tag("remove")intersect()
1286// cuboid(10){
1287// tag("intersect")position(RIGHT) cyl(r=7,h=10);
1288// tag("keep")position(LEFT)cyl(r=4,h=10);
1289// }
1290// Example: Using tag_intersect corrects the problem.
1291// $fn=32;
1292// diff()
1293// cuboid([20,15,9])
1294// tag_intersect("remove")
1295// cuboid(10){
1296// tag("intersect")position(RIGHT) cyl(r=7,h=10);
1297// tag("keep")position(LEFT)cyl(r=4,h=10);
1298// }
1299module tag_intersect(tag,intersect="intersect",keep="keep")
1300{
1301 assert(is_string(intersect),"intersect must be a string of tags");
1302 assert(is_string(keep),"keep must be a string of tags");
1303 assert(is_string(tag),"tag must be a string");
1304 assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
1305 $tag=str($tag_prefix,tag);
1306 if (_is_shown())
1307 show_all(){
1308 intersection(){
1309 show_only(intersect) children();
1310 hide(str(intersect," ",keep)) children();
1311 }
1312 show_only(keep) children();
1313 }
1314}
1315
1316
1317// Module: conv_hull()
1318// Synopsis: Performs a hull operation on the children using tags to determine what happens.
1319// Topics: Attachments, Hulling
1320// See Also: tag(), recolor(), show_only(), hide(), diff(), intersect(), hull()
1321// Usage:
1322// conv_hull([keep]) CHILDREN;
1323// Description:
1324// Performs a hull operation on the children using tags to determine what happens. The items
1325// not tagged with the `keep` tags are combined into a convex hull, and the children tagged with the keep tags
1326// are unioned with the result.
1327// .
1328// Note that `conv_hull()` invokes its children twice.
1329// .
1330// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1331// Arguments:
1332// keep = String containing space delimited set of tag names of children to keep out of the hull. Default: "keep"
1333// Example:
1334// conv_hull("keep")
1335// sphere(d=100, $fn=64) {
1336// cuboid([40,90,90]);
1337// tag("keep")xcyl(d=40, l=120);
1338// }
1339// Example: difference combined with hull where all objects are relative to each other.
1340// $fn=32;
1341// diff()
1342// conv_hull("remove")
1343// cuboid(10)
1344// position(RIGHT+BACK)cyl(r=4,h=10)
1345// tag("remove")cyl(r=2,h=12);
1346module conv_hull(keep="keep")
1347{
1348 req_children($children);
1349 assert(is_string(keep),"keep must be a string of tags");
1350 if (_is_shown())
1351 hull() hide(keep) children();
1352 show_int(keep) children();
1353}
1354
1355
1356// Module: tag_conv_hull()
1357// Synopsis: Performs a {{conv_hull()}} and then sets a tag on the result.
1358// Topics: Attachments
1359// See Also: tag(), recolor(), show_only(), hide(), diff(), intersect()
1360// Usage:
1361// tag_conv_hull(tag, [keep]) CHILDREN;
1362// Description:
1363// Perform a convex hull operation in the manner of {{conv_hull()}} using tags to control what happens,
1364// and then tag the resulting hull object with the specified tag. This forces the specified
1365// tag to be resolved at the level of the hull operation. In most cases, this is not necessary,
1366// but if you have kept objects and want to operate on the hull object as a whole object using
1367// more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
1368// .
1369// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1370// Arguments:
1371// keep = String containing space delimited set of tag names of children to keep out of the hull. Default: "keep"
1372// Side Effects:
1373// Sets `$tag` to the tag you specify, possibly with a scope prefix.
1374// Example: With a regular tag, the kept object is not handled as desired:
1375// diff(){
1376// cuboid([30,30,9])
1377// tag("remove")conv_hull("remove")
1378// cuboid(10,anchor=LEFT+FRONT){
1379// position(RIGHT+BACK)cyl(r=4,h=10);
1380// tag("keep")position(FRONT+LEFT)cyl(r=4,h=10);
1381// }
1382// }
1383// Example: Using `tag_conv_hull()` fixes the problem:
1384// diff(){
1385// cuboid([30,30,9])
1386// tag_conv_hull("remove")
1387// cuboid(10,anchor=LEFT+FRONT){
1388// position(RIGHT+BACK)cyl(r=4,h=10);
1389// tag("keep")position(FRONT+LEFT)cyl(r=4,h=10);
1390// }
1391// }
1392module tag_conv_hull(tag,keep="keep")
1393{
1394 req_children($children);
1395 assert(is_string(keep),"keep must be a string of tags");
1396 assert(is_string(tag),"tag must be a string");
1397 assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
1398 $tag=str($tag_prefix,tag);
1399 if (_is_shown())
1400 show_all(){
1401 hull() hide(keep) children();
1402 show_only(keep) children();
1403 }
1404}
1405
1406
1407// Module: hide()
1408// Synopsis: Hides attachable children with the given tags.
1409// Topics: Attachments
1410// See Also: tag(), recolor(), show_only(), show_all(), show_int(), diff(), intersect()
1411// Usage:
1412// hide(tags) CHILDREN;
1413// Description:
1414// Hides all attachable children with the given tags, which you supply as a space separated string. Previously hidden objects remain hidden, so hiding is cumulative, unlike `show_only()`.
1415// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1416// Side Effects:
1417// Sets `$tags_hidden` to include the tags you specify.
1418// Example: Hides part of the model.
1419// hide("A")
1420// tag("main") cube(50, anchor=CENTER, $tag="Main") {
1421// tag("A")attach(LEFT, BOTTOM) cylinder(d=30, h=30);
1422// tag("B")attach(RIGHT, BOTTOM) cylinder(d=30, h=30);
1423// }
1424// Example: Use an invisible parent to position children. Note that children must be retagged because they inherit the parent tag.
1425// $fn=16;
1426// hide("hidden")
1427// tag("hidden")cuboid(10)
1428// tag("visible") {
1429// position(RIGHT) cyl(r=1,h=12);
1430// position(LEFT) cyl(r=1,h=12);
1431// }
1432module hide(tags)
1433{
1434 req_children($children);
1435 dummy=assert(is_string(tags), "tags must be a string");
1436 taglist = [for(s=str_split(tags," ",keep_nulls=false)) str($tag_prefix,s)];
1437 $tags_hidden = concat($tags_hidden,taglist);
1438 children();
1439}
1440
1441
1442// Module: show_only()
1443// Synopsis: Show only the children with the listed tags.
1444// See Also: tag(), recolor(), show_all(), show_int(), diff(), intersect()
1445// Topics: Attachments
1446// Usage:
1447// show_only(tags) CHILDREN;
1448// Description:
1449// Show only the children with the listed tags, which you sply as a space separated string. Only unhidden objects will be shown, so if an object is hidden either before or after the `show_only()` call then it will remain hidden. This overrides any previous `show_only()` calls. Unlike `hide()`, calls to `show_only()` are not cumulative.
1450// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1451// Side Effects:
1452// Sets `$tags_shown` to the tag you specify.
1453// Example: Display the attachments but not the parent
1454// show_only("visible")
1455// cube(50, anchor=CENTER)
1456// tag("visible"){
1457// attach(LEFT, BOTTOM) cylinder(d=30, h=30);
1458// attach(RIGHT, BOTTOM) cylinder(d=30, h=30);
1459// }
1460module show_only(tags)
1461{
1462 req_children($children);
1463 dummy=assert(is_string(tags), str("tags must be a string",tags));
1464 taglist = [for(s=str_split(tags," ",keep_nulls=false)) str($tag_prefix,s)];
1465 $tags_shown = taglist;
1466 children();
1467}
1468
1469// Module: show_all()
1470// Synopsis: Shows all children and clears tags.
1471// See Also: tag(), recolor(), show_only(), show_int(), diff(), intersect()
1472// Topics: Attachments
1473// Usage;
1474// show_all() CHILDREN;
1475// Description:
1476// Shows all children. Clears the list of hidden tags and shown tags so that all child objects will be
1477// fully displayed.
1478// Side Effects:
1479// Sets `$tags_shown="ALL"`
1480// Sets `$tags_hidden=[]`
1481module show_all()
1482{
1483 req_children($children);
1484 $tags_shown="ALL";
1485 $tags_hidden=[];
1486 children();
1487}
1488
1489
1490// Module: show_int()
1491// Synopsis: Shows children with the listed tags which were already shown in the parent context.
1492// See Also: tag(), recolor(), show_only(), show_all(), show_int(), diff(), intersect()
1493// Topics: Attachments
1494// Usage:
1495// show_int(tags) CHILDREN;
1496// Description:
1497// Show only the children with the listed tags which were already shown in the parent context.
1498// This intersects the current show list with the list of tags you provide.
1499// Arguments:
1500// tags = list of tags to show
1501// Side Effects:
1502// Sets `$tags_shown`
1503module show_int(tags)
1504{
1505 req_children($children);
1506 dummy=assert(is_string(tags), str("tags must be a string",tags));
1507 taglist = [for(s=str_split(tags," ",keep_nulls=false)) str($tag_prefix,s)];
1508 $tags_shown = $tags_shown == "ALL" ? taglist : set_intersection($tags_shown,taglist);
1509 children();
1510}
1511
1512
1513// Section: Mask Attachment
1514
1515
1516// Module: face_mask()
1517// Synopsis: Ataches a 3d mask shape to the given faces of the parent.
1518// SynTags: Trans
1519// Topics: Attachments, Masking
1520// See Also: attachable(), position(), attach(), edge_mask(), corner_mask(), face_profile(), edge_profile(), corner_profile()
1521// Usage:
1522// PARENT() face_mask(faces) CHILDREN;
1523// Description:
1524// Takes a 3D mask shape, and attaches it to the given faces, with the appropriate orientation to be
1525// differenced away. The mask shape should be vertically oriented (Z-aligned) with the bottom half
1526// (Z-) shaped to be diffed away from the face of parent attachable shape. If no tag is set then
1527// `face_mask()` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1528// For details on specifying the faces to mask see [Specifying Faces](attachments.scad#subsection-specifying-faces).
1529// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1530// Arguments:
1531// edges = Faces to mask. See [Specifying Faces](attachments.scad#subsection-specifying-faces) for information on specifying faces. Default: All faces
1532// Side Effects:
1533// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1534// `$idx` is set to the index number of each face in the list of faces given.
1535// `$attach_anchor` is set for each face given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1536// Example:
1537// diff()
1538// cylinder(r=30, h=60)
1539// face_mask(TOP) {
1540// rounding_cylinder_mask(r=30,rounding=5);
1541// cuboid([5,61,10]);
1542// }
1543// Example: Using `$idx`
1544// diff()
1545// cylinder(r=30, h=60)
1546// face_mask([TOP, BOT])
1547// zrot(45*$idx) zrot_copies([0,90]) cuboid([5,61,10]);
1548module face_mask(faces=[LEFT,RIGHT,FRONT,BACK,BOT,TOP]) {
1549 req_children($children);
1550 faces = is_vector(faces)? [faces] : faces;
1551 assert(all([for (face=faces) is_vector(face) && sum([for (x=face) x!=0? 1 : 0])==1]), "Vector in faces doesn't point at a face.");
1552 assert($parent_geom != undef, "No object to attach to!");
1553 attach(faces) {
1554 default_tag("remove") children();
1555 }
1556}
1557
1558
1559// Module: edge_mask()
1560// Synopsis: Attaches a 3D mask shape to the given edges of the parent.
1561// SynTags: Trans
1562// Topics: Attachments, Masking
1563// See Also: attachable(), position(), attach(), face_mask(), corner_mask(), face_profile(), edge_profile(), corner_profile()
1564// Usage:
1565// PARENT() edge_mask([edges], [except]) CHILDREN;
1566// Description:
1567// Takes a 3D mask shape, and attaches it to the given edges, with the appropriate orientation to be
1568// differenced away. The mask shape should be vertically oriented (Z-aligned) with the back-right
1569// quadrant (X+Y+) shaped to be diffed away from the edge of parent attachable shape. If no tag is set
1570// then `edge_mask` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1571// For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
1572// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1573// Figure: A Typical Edge Rounding Mask
1574// module roundit(l,r) difference() {
1575// translate([-1,-1,-l/2])
1576// cube([r+1,r+1,l]);
1577// translate([r,r])
1578// cylinder(h=l+1,r=r,center=true, $fn=quantup(segs(r),4));
1579// }
1580// roundit(l=30,r=10);
1581// Arguments:
1582// edges = Edges to mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: All edges.
1583// except = Edges to explicitly NOT mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: No edges.
1584// Side Effects:
1585// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1586// `$idx` is set to the index number of each edge.
1587// `$attach_anchor` is set for each edge given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1588// `$parent_size` is set to the size of the parent object.
1589// Example:
1590// diff()
1591// cube([50,60,70],center=true)
1592// edge_mask([TOP,"Z"],except=[BACK,TOP+LEFT])
1593// rounding_edge_mask(l=71,r=10);
1594module edge_mask(edges=EDGES_ALL, except=[]) {
1595 req_children($children);
1596 assert($parent_geom != undef, "No object to attach to!");
1597 edges = _edges(edges, except=except);
1598 vecs = [
1599 for (i = [0:3], axis=[0:2])
1600 if (edges[axis][i]>0)
1601 EDGE_OFFSETS[axis][i]
1602 ];
1603 for ($idx = idx(vecs)) {
1604 vec = vecs[$idx];
1605 vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0);
1606 dummy=assert(vcount == 2, "Not an edge vector!");
1607 anch = _find_anchor(vec, $parent_geom);
1608 $attach_to = undef;
1609 $attach_anchor = anch;
1610 $attach_norot = true;
1611 rotang =
1612 vec.z<0? [90,0,180+v_theta(vec)] :
1613 vec.z==0 && sign(vec.x)==sign(vec.y)? 135+v_theta(vec) :
1614 vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+v_theta(vec)] :
1615 [-90,0,180+v_theta(vec)];
1616 translate(anch[1]) rot(rotang)
1617 default_tag("remove") children();
1618 }
1619}
1620
1621
1622// Module: corner_mask()
1623// Synopsis: Attaches a 3d mask shape to the given corners of the parent.
1624// SynTags: Trans
1625// Topics: Attachments, Masking
1626// See Also: attachable(), position(), attach(), face_mask(), edge_mask(), face_profile(), edge_profile(), corner_profile()
1627// Usage:
1628// PARENT() corner_mask([corners], [except]) CHILDREN;
1629// Description:
1630// Takes a 3D mask shape, and attaches it to the specified corners, with the appropriate orientation to
1631// be differenced away. The 3D corner mask shape should be designed to mask away the X+Y+Z+ octant. If no tag is set
1632// then `corner_mask` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1633// See [Specifying Corners](attachments.scad#subsection-specifying-corners) for information on how to specify corner sets.
1634// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1635// Arguments:
1636// corners = Corners to mask. See [Specifying Corners](attachments.scad#subsection-specifying-corners). Default: All corners.
1637// except = Corners to explicitly NOT mask. See [Specifying Corners](attachments.scad#subsection-specifying-corners). Default: No corners.
1638// Side Effects:
1639// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1640// `$idx` is set to the index number of each corner.
1641// `$attach_anchor` is set for each corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1642// Example:
1643// diff()
1644// cube(100, center=true)
1645// corner_mask([TOP,FRONT],LEFT+FRONT+TOP)
1646// difference() {
1647// translate(-0.01*[1,1,1]) cube(20);
1648// translate([20,20,20]) sphere(r=20);
1649// }
1650module corner_mask(corners=CORNERS_ALL, except=[]) {
1651 req_children($children);
1652 assert($parent_geom != undef, "No object to attach to!");
1653 corners = _corners(corners, except=except);
1654 vecs = [for (i = [0:7]) if (corners[i]>0) CORNER_OFFSETS[i]];
1655 for ($idx = idx(vecs)) {
1656 vec = vecs[$idx];
1657 vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0);
1658 dummy=assert(vcount == 3, "Not an edge vector!");
1659 anch = _find_anchor(vec, $parent_geom);
1660 $attach_to = undef;
1661 $attach_anchor = anch;
1662 $attach_norot = true;
1663 rotang = vec.z<0?
1664 [ 0,0,180+v_theta(vec)-45] :
1665 [180,0,-90+v_theta(vec)-45];
1666 translate(anch[1]) rot(rotang)
1667 default_tag("remove") children();
1668 }
1669}
1670
1671
1672// Module: face_profile()
1673// Synopsis: Extrudes a 2D edge profile into a mask for all edges and corners of the given faces on the parent.
1674// SynTags: Geom
1675// Topics: Attachments, Masking
1676// See Also: attachable(), position(), attach(), edge_profile(), corner_profile(), face_mask(), edge_mask(), corner_mask()
1677// Usage:
1678// PARENT() face_profile(faces, r|d=, [convexity=]) CHILDREN;
1679// Description:
1680// Given a 2D edge profile, extrudes it into a mask for all edges and corners bounding each given face. If no tag is set
1681// then `face_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1682// See [Specifying Faces](attachments.scad#subsection-specifying-faces) for information on specifying faces.
1683// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1684// Arguments:
1685// faces = Faces to mask edges and corners of.
1686// r = Radius of corner mask.
1687// ---
1688// d = Diameter of corner mask.
1689// excess = Excess length to extrude the profile to make edge masks. Default: 0.01
1690// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10
1691// Side Effects:
1692// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1693// `$idx` is set to the index number of each face.
1694// `$attach_anchor` is set for each edge or corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1695// `$profile_type` is set to `"edge"` or `"corner"`, depending on what is being masked.
1696// Example:
1697// diff()
1698// cube([50,60,70],center=true)
1699// face_profile(TOP,r=10)
1700// mask2d_roundover(r=10);
1701module face_profile(faces=[], r, d, excess=0.01, convexity=10) {
1702 req_children($children);
1703 faces = is_vector(faces)? [faces] : faces;
1704 assert(all([for (face=faces) is_vector(face) && sum([for (x=face) x!=0? 1 : 0])==1]), "Vector in faces doesn't point at a face.");
1705 r = get_radius(r=r, d=d, dflt=undef);
1706 assert(is_num(r) && r>=0);
1707 edge_profile(faces, excess=excess) children();
1708 corner_profile(faces, convexity=convexity, r=r) children();
1709}
1710
1711
1712// Module: edge_profile()
1713// Synopsis: Extrudes a 2d edge profile into a mask on the given edges of the parent.
1714// SynTags: Geom
1715// Topics: Attachments, Masking
1716// See Also: attachable(), position(), attach(), face_profile(), edge_profile_asym(), corner_profile(), edge_mask(), face_mask(), corner_mask()
1717// Usage:
1718// PARENT() edge_profile([edges], [except], [convexity]) CHILDREN;
1719// Description:
1720// Takes a 2D mask shape and attaches it to the selected edges, with the appropriate orientation and
1721// extruded length to be `diff()`ed away, to give the edge a matching profile. If no tag is set
1722// then `edge_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1723// For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
1724// For a step-by-step
1725// explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1726// Arguments:
1727// edges = Edges to mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: All edges.
1728// except = Edges to explicitly NOT mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: No edges.
1729// excess = Excess length to extrude the profile to make edge masks. Default: 0.01
1730// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10
1731// Side Effects:
1732// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1733// `$idx` is set to the index number of each edge.
1734// `$attach_anchor` is set for each edge given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1735// `$profile_type` is set to `"edge"`.
1736// `$edge_angle` is set to the inner angle of the current edge.
1737// Example:
1738// diff()
1739// cube([50,60,70],center=true)
1740// edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT])
1741// mask2d_roundover(r=10, inset=2);
1742// Example: Using $edge_angle on a Conoid
1743// diff()
1744// cyl(d1=50, d2=30, l=40, anchor=BOT) {
1745// edge_profile([TOP,BOT], excess=10, convexity=6) {
1746// mask2d_roundover(r=8, inset=1, excess=1, mask_angle=$edge_angle);
1747// }
1748// }
1749// Example: Using $edge_angle on a Prismoid
1750// diff()
1751// prismoid([60,50],[30,20],h=40,shift=[-25,15]) {
1752// edge_profile(excess=10, convexity=20) {
1753// mask2d_roundover(r=5,inset=1,mask_angle=$edge_angle);
1754// }
1755// }
1756
1757module edge_profile(edges=EDGES_ALL, except=[], excess=0.01, convexity=10) {
1758 req_children($children);
1759 check1 = assert($parent_geom != undef, "No object to attach to!");
1760 conoid = $parent_geom[0] == "conoid";
1761 edges = !conoid? _edges(edges, except=except) :
1762 edges==EDGES_ALL? [TOP,BOT] :
1763 assert(all([for (e=edges) in_list(e,[TOP,BOT])]), "Invalid conoid edge spec.")
1764 edges;
1765 vecs = conoid
1766 ? [for (e=edges) e+FWD]
1767 : [
1768 for (i = [0:3], axis=[0:2])
1769 if (edges[axis][i]>0)
1770 EDGE_OFFSETS[axis][i]
1771 ];
1772 all_vecs_are_edges = all([for (vec = vecs) sum(v_abs(vec))==2]);
1773 check2 = assert(all_vecs_are_edges, "All vectors must be edges.");
1774 default_tag("remove")
1775 for ($idx = idx(vecs)) {
1776 vec = vecs[$idx];
1777 anch = _find_anchor(vec, $parent_geom);
1778 path_angs_T = _attach_geom_edge_path($parent_geom, vec);
1779 path = path_angs_T[0];
1780 vecs = path_angs_T[1];
1781 post_T = path_angs_T[2];
1782 $attach_to = undef;
1783 $attach_anchor = anch;
1784 $attach_norot = true;
1785 $profile_type = "edge";
1786 multmatrix(post_T) {
1787 for (i = idx(path,e=-2)) {
1788 pt1 = select(path,i);
1789 pt2 = select(path,i+1);
1790 cp = (pt1 + pt2) / 2;
1791 v1 = vecs[i][0];
1792 v2 = vecs[i][1];
1793 $edge_angle = 180 - vector_angle(v1,v2);
1794 if (!approx(pt1,pt2)) {
1795 seglen = norm(pt2-pt1) + 2 * excess;
1796 move(cp) {
1797 frame_map(x=-v2, z=unit(pt2-pt1)) {
1798 linear_extrude(height=seglen, center=true, convexity=convexity)
1799 mirror([-1,1]) children();
1800 }
1801 }
1802 }
1803 }
1804 }
1805 }
1806}
1807
1808
1809// Module: edge_profile_asym()
1810// Synopsis: Extrudes an asymmetric 2D profile into a mask on the given edges and corners of the parent.
1811// SynTags: Geom
1812// Topics: Attachments, Masking
1813// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_profile(), edge_mask(), face_mask(), corner_mask()
1814// Usage:
1815// PARENT() edge_profile([edges], [except=], [convexity=], [flip=], [corner_type=]) CHILDREN;
1816// Description:
1817// Takes an asymmetric 2D mask shape and attaches it to the selected edges and corners, with the appropriate
1818// orientation and extruded length to be `diff()`ed away, to give the edges and corners a matching profile.
1819// If no tag is set then `edge_profile_asym()` sets the tag for children to "remove" so that it will work
1820// with the default {{diff()}} tag. For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
1821// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1822// .
1823// The asymmetric profiles are joined consistently at the corners. This is impossible if all three edges at a corner use the profile, hence
1824// this situation is not permitted. The profile orientation can be inverted using the `flip=true` parameter.
1825// .
1826// The standard profiles are located in the first quadrant and have positive X values. If you provide a profile located in the second quadrant,
1827// where the X values are negative, then it will produce a fillet. You can flip any of the standard profiles using {{xflip()}}.
1828// Fillets are always asymmetric because at a given edge, they can blend in two different directions, so even for symmetric profiles,
1829// the asymmetric logic is required. You can set the `corner_type` parameter to select rounded, chamfered or sharp corners.
1830// However, when the corners are inside (concave) corners, you must provide the size of the profile ([width,height]), because the
1831// this information is required to produce the correct corner and cannot be obtain from the profile itself, which is a child object.
1832// Arguments:
1833// edges = Edges to mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: All edges.
1834// except = Edges to explicitly NOT mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: No edges.
1835// excess = Excess length to extrude the profile to make edge masks. Default: 0.01
1836// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10
1837// flip = If true, reverses the orientation of any external profile parts at each edge. Default false
1838// corner_type = Specifies how exterior corners should be formed. Must be one of `"none"`, `"chamfer"`, `"round"`, or `"sharp"`. Default: `"none"`
1839// size = If given the width and height of the 2D profile, will enable rounding and chamfering of internal corners when given a negative profile.
1840// Side Effects:
1841// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1842// `$idx` is set to the index number of each edge.
1843// `$attach_anchor` is set for each edge given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1844// `$profile_type` is set to `"edge"`.
1845// `$edge_angle` is set to the inner angle of the current edge.
1846// Example:
1847// ogee = [
1848// "xstep",1, "ystep",1, // Starting shoulder.
1849// "fillet",5, "round",5, // S-curve.
1850// "ystep",1, "xstep",1 // Ending shoulder.
1851// ];
1852// diff()
1853// cuboid(50) {
1854// edge_profile_asym(FRONT)
1855// mask2d_ogee(ogee);
1856// }
1857// Example: Flipped
1858// ogee = [
1859// "xstep",1, "ystep",1, // Starting shoulder.
1860// "fillet",5, "round",5, // S-curve.
1861// "ystep",1, "xstep",1 // Ending shoulder.
1862// ];
1863// diff()
1864// cuboid(50) {
1865// edge_profile_asym(FRONT, flip=true)
1866// mask2d_ogee(ogee);
1867// }
1868// Example: Negative Chamfering
1869// cuboid(50) {
1870// edge_profile_asym(FWD, flip=false)
1871// xflip() mask2d_chamfer(10);
1872// edge_profile_asym(BACK, flip=true, corner_type="sharp")
1873// xflip() mask2d_chamfer(10);
1874// }
1875// Example: Negative Roundings
1876// cuboid(50) {
1877// edge_profile_asym(FWD, flip=false)
1878// xflip() mask2d_roundover(10);
1879// edge_profile_asym(BACK, flip=true, corner_type="round")
1880// xflip() mask2d_roundover(10);
1881// }
1882// Example: Cornerless
1883// cuboid(50) {
1884// edge_profile_asym(
1885// "ALL", except=[TOP+FWD+RIGHT, BOT+BACK+LEFT]
1886// ) xflip() mask2d_roundover(10);
1887// }
1888// Example: More complicated edge sets
1889// cuboid(50) {
1890// edge_profile_asym(
1891// [FWD,BACK,BOT+RIGHT], except=[FWD+RIGHT,BOT+BACK],
1892// corner_type="round"
1893// ) xflip() mask2d_roundover(10);
1894// }
1895// Example: Mixing it up a bit.
1896// diff()
1897// cuboid(60) {
1898// tag("keep") edge_profile_asym(LEFT, flip=true, corner_type="chamfer")
1899// xflip() mask2d_chamfer(10);
1900// edge_profile_asym(RIGHT)
1901// mask2d_roundover(10);
1902// }
1903// Example: Chamfering internal corners.
1904// cuboid(40) {
1905// edge_profile_asym(
1906// [FWD+DOWN,FWD+LEFT],
1907// corner_type="chamfer", size=[10,10]/sqrt(2)
1908// ) xflip() mask2d_chamfer(10);
1909// }
1910// Example: Rounding internal corners.
1911// cuboid(40) {
1912// edge_profile_asym(
1913// [FWD+DOWN,FWD+LEFT],
1914// corner_type="round", size=[10,10]
1915// ) xflip() mask2d_roundover(10);
1916// }
1917
1918module edge_profile_asym(
1919 edges=EDGES_ALL, except=[],
1920 excess=0.01, convexity=10,
1921 flip=false, corner_type="none",
1922 size=[0,0]
1923) {
1924 function _corner_orientation(pos,pvec) =
1925 let(
1926 j = [for (i=[0:2]) if (pvec[i]) i][0],
1927 T = (pos.x>0? xflip() : ident(4)) *
1928 (pos.y>0? yflip() : ident(4)) *
1929 (pos.z>0? zflip() : ident(4)) *
1930 rot(-120*(2-j), v=[1,1,1])
1931 ) T;
1932
1933 function _default_edge_orientation(edge) =
1934 edge.z < 0? [[-edge.x,-edge.y,0], UP] :
1935 edge.z > 0? [[-edge.x,-edge.y,0], DOWN] :
1936 edge.y < 0? [[-edge.x,0,0], BACK] :
1937 [[-edge.x,0,0], FWD] ;
1938
1939 function _edge_transition_needs_flip(from,to) =
1940 let(
1941 flip_edges = [
1942 [BOT+FWD, [FWD+LEFT, FWD+RIGHT]],
1943 [BOT+BACK, [BACK+LEFT, BACK+RIGHT]],
1944 [BOT+LEFT, []],
1945 [BOT+RIGHT, []],
1946 [TOP+FWD, [FWD+LEFT, FWD+RIGHT]],
1947 [TOP+BACK, [BACK+LEFT, BACK+RIGHT]],
1948 [TOP+LEFT, []],
1949 [TOP+RIGHT, []],
1950 [FWD+LEFT, [TOP+FWD, BOT+FWD]],
1951 [FWD+RIGHT, [TOP+FWD, BOT+FWD]],
1952 [BACK+LEFT, [TOP+BACK, BOT+BACK]],
1953 [BACK+RIGHT, [TOP+BACK, BOT+BACK]],
1954 ],
1955 i = search([from], flip_edges, num_returns_per_match=1)[0],
1956 check = assert(i!=[], "Bad edge vector.")
1957 ) in_list(to,flip_edges[i][1]);
1958
1959 function _edge_corner_numbers(vec) =
1960 let(
1961 v2 = [for (i=idx(vec)) vec[i]? (vec[i]+1)/2*pow(2,i) : 0],
1962 off = v2.x + v2.y + v2.z,
1963 xs = [0, if (!vec.x) 1],
1964 ys = [0, if (!vec.y) 2],
1965 zs = [0, if (!vec.z) 4]
1966 ) [for (x=xs, y=ys, z=zs) x+y+z + off];
1967
1968 function _gather_contiguous_edges(edge_corners) =
1969 let(
1970 no_tri_corners = all([for(cn = [0:7]) len([for (ec=edge_corners) if(in_list(cn,ec[1])) 1])<3]),
1971 check = assert(no_tri_corners, "Cannot have three edges that meet at the same corner.")
1972 )
1973 _gather_contiguous_edges_r(
1974 [for (i=idx(edge_corners)) if(i) edge_corners[i]],
1975 edge_corners[0][1],
1976 [edge_corners[0][0]], []);
1977
1978 function _gather_contiguous_edges_r(edge_corners, ecns, curr, out) =
1979 len(edge_corners)==0? [each out, curr] :
1980 let(
1981 i1 = [
1982 for (i = idx(edge_corners))
1983 if (in_list(ecns[0], edge_corners[i][1]))
1984 i
1985 ],
1986 i2 = [
1987 for (i = idx(edge_corners))
1988 if (in_list(ecns[1], edge_corners[i][1]))
1989 i
1990 ]
1991 ) !i1 && !i2? _gather_contiguous_edges_r(
1992 [for (i=idx(edge_corners)) if(i) edge_corners[i]],
1993 edge_corners[0][1],
1994 [edge_corners[0][0]],
1995 [each out, curr]
1996 ) : let(
1997 nu_curr = [
1998 if (i1) edge_corners[i1[0]][0],
1999 each curr,
2000 if (i2) edge_corners[i2[0]][0],
2001 ],
2002 nu_ecns = [
2003 if (!i1) ecns[0] else [
2004 for (ecn = edge_corners[i1[0]][1])
2005 if (ecn != ecns[0]) ecn
2006 ][0],
2007 if (!i2) ecns[1] else [
2008 for (ecn = edge_corners[i2[0]][1])
2009 if (ecn != ecns[1]) ecn
2010 ][0],
2011 ],
2012 rem = [
2013 for (i = idx(edge_corners))
2014 if (i != i1[0] && i != i2[0])
2015 edge_corners[i]
2016 ]
2017 )
2018 _gather_contiguous_edges_r(rem, nu_ecns, nu_curr, out);
2019
2020 function _edge_transition_inversions(edge_string) =
2021 let(
2022 // boolean cumulative sum
2023 bcs = function(list, i=0, inv=false, out=[])
2024 i>=len(list)? out :
2025 let( nu_inv = list[i]? !inv : inv )
2026 bcs(list, i+1, nu_inv, [each out, nu_inv]),
2027 inverts = bcs([
2028 false,
2029 for(i = idx(edge_string)) if (i)
2030 _edge_transition_needs_flip(
2031 edge_string[i-1],
2032 edge_string[i]
2033 )
2034 ]),
2035 boti = [for(i = idx(edge_string)) if (edge_string[i].z<0) i],
2036 topi = [for(i = idx(edge_string)) if (edge_string[i].z>0) i],
2037 lfti = [for(i = idx(edge_string)) if (edge_string[i].x<0) i],
2038 rgti = [for(i = idx(edge_string)) if (edge_string[i].x>0) i],
2039 idx = [for (m = [boti, topi, lfti, rgti]) if(m) m[0]][0],
2040 rinverts = inverts[idx] == false? inverts : [for (x = inverts) !x]
2041 ) rinverts;
2042
2043 function _is_closed_edge_loop(edge_string) =
2044 let(
2045 e1 = edge_string[0],
2046 e2 = last(edge_string)
2047 )
2048 len([for (i=[0:2]) if (abs(e1[i])==1 && e1[i]==e2[i]) 1]) == 1 &&
2049 len([for (i=[0:2]) if (e1[i]==0 && abs(e2[i])==1) 1]) == 1 &&
2050 len([for (i=[0:2]) if (e2[i]==0 && abs(e1[i])==1) 1]) == 1;
2051
2052 function _edge_pair_perp_vec(e1,e2) =
2053 [for (i=[0:2]) if (abs(e1[i])==1 && e1[i]==e2[i]) -e1[i] else 0];
2054
2055 req_children($children);
2056 check1 = assert($parent_geom != undef, "No object to attach to!")
2057 assert(in_list(corner_type, ["none", "round", "chamfer", "sharp"]))
2058 assert(is_bool(flip));
2059 edges = _edges(edges, except=except);
2060 vecs = [
2061 for (i = [0:3], axis=[0:2])
2062 if (edges[axis][i]>0)
2063 EDGE_OFFSETS[axis][i]
2064 ];
2065 all_vecs_are_edges = all([for (vec = vecs) sum(v_abs(vec))==2]);
2066 check2 = assert(all_vecs_are_edges, "All vectors must be edges.");
2067 edge_corners = [for (vec = vecs) [vec, _edge_corner_numbers(vec)]];
2068 edge_strings = _gather_contiguous_edges(edge_corners);
2069 default_tag("remove")
2070 for (edge_string = edge_strings) {
2071 inverts = _edge_transition_inversions(edge_string);
2072 flipverts = [for (x = inverts) flip? !x : x];
2073 vecpairs = [
2074 for (i = idx(edge_string))
2075 let (p = _default_edge_orientation(edge_string[i]))
2076 flipverts[i]? [p.y,p.x] : p
2077 ];
2078 is_loop = _is_closed_edge_loop(edge_string);
2079 for (i = idx(edge_string)) {
2080 if (corner_type!="none" && (i || is_loop)) {
2081 e1 = select(edge_string,i-1);
2082 e2 = select(edge_string,i);
2083 vp1 = select(vecpairs,i-1);
2084 vp2 = select(vecpairs,i);
2085 pvec = _edge_pair_perp_vec(e1,e2);
2086 pos = [for (i=[0:2]) e1[i]? e1[i] : e2[i]];
2087 mirT = _corner_orientation(pos, pvec);
2088 $attach_to = undef;
2089 $attach_anchor = _find_anchor(pos, $parent_geom);
2090 $attach_norot = true;
2091 $profile_type = "corner";
2092 position(pos) {
2093 multmatrix(mirT) {
2094 if (vp1.x == vp2.x && size.y > 0) {
2095 zflip() {
2096 if (corner_type=="chamfer") {
2097 fn = $fn;
2098 move([size.y,size.y]) {
2099 rotate_extrude(angle=90, $fn=4)
2100 left_half(planar=true, $fn=fn)
2101 zrot(-90) fwd(size.y) children();
2102 }
2103 linear_extrude(height=size.x) {
2104 mask2d_roundover(size.y, inset=0.01, $fn=4);
2105 }
2106 } else if (corner_type=="round") {
2107 move([size.y,size.y]) {
2108 rotate_extrude(angle=90)
2109 left_half(planar=true)
2110 zrot(-90) fwd(size.y) children();
2111 }
2112 linear_extrude(height=size.x) {
2113 mask2d_roundover(size.y, inset=0.01);
2114 }
2115 }
2116 }
2117 } else if (vp1.y == vp2.y) {
2118 if (corner_type=="chamfer") {
2119 fn = $fn;
2120 rotate_extrude(angle=90, $fn=4)
2121 right_half(planar=true, $fn=fn)
2122 children();
2123 rotate_extrude(angle=90, $fn=4)
2124 left_half(planar=true, $fn=fn)
2125 children();
2126 } else if (corner_type=="round") {
2127 rotate_extrude(angle=90)
2128 right_half(planar=true)
2129 children();
2130 rotate_extrude(angle=90)
2131 left_half(planar=true)
2132 children();
2133 } else { //corner_type == "sharp"
2134 intersection() {
2135 rot([90,0, 0]) linear_extrude(height=100,center=true,convexity=convexity) children();
2136 rot([90,0,90]) linear_extrude(height=100,center=true,convexity=convexity) children();
2137 }
2138 }
2139 }
2140 }
2141 }
2142 }
2143 }
2144 for (i = idx(edge_string)) {
2145 $attach_to = undef;
2146 $attach_anchor = _find_anchor(edge_string[i], $parent_geom);
2147 $attach_norot = true;
2148 $profile_type = "edge";
2149 edge_profile(edge_string[i], excess=excess, convexity=convexity) {
2150 if (flipverts[i]) {
2151 mirror([-1,1]) children();
2152 } else {
2153 children();
2154 }
2155 }
2156 }
2157 }
2158}
2159
2160
2161
2162// Module: corner_profile()
2163// Synopsis: Rotationally extrudes a 2d edge profile into corner mask on the given corners of the parent.
2164// SynTags: Geom
2165// Topics: Attachments, Masking
2166// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_mask(), face_mask(), edge_mask()
2167// Usage:
2168// PARENT() corner_profile([corners], [except], [r=|d=], [convexity=]) CHILDREN;
2169// Description:
2170// Takes a 2D mask shape, rotationally extrudes and converts it into a corner mask, and attaches it
2171// to the selected corners with the appropriate orientation. If no tag is set then `corner_profile()`
2172// sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
2173// See [Specifying Corners](attachments.scad#subsection-specifying-corners) for information on how to specify corner sets.
2174// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
2175// Arguments:
2176// corners = Corners to mask. See [Specifying Corners](attachments.scad#subsection-specifying-corners). Default: All corners.
2177// except = Corners to explicitly NOT mask. See [Specifying Corners](attachments.scad#subsection-specifying-corners). Default: No corners.
2178// ---
2179// r = Radius of corner mask.
2180// d = Diameter of corner mask.
2181// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10
2182// Side Effects:
2183// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
2184// `$idx` is set to the index number of each corner.
2185// `$attach_anchor` is set for each corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
2186// `$profile_type` is set to `"corner"`.
2187// Example:
2188// diff()
2189// cuboid([50,60,70],rounding=10,edges="Z",anchor=CENTER) {
2190// corner_profile(TOP,r=10)
2191// mask2d_teardrop(r=10, angle=40);
2192// }
2193module corner_profile(corners=CORNERS_ALL, except=[], r, d, convexity=10) {
2194 check1 = assert($parent_geom != undef, "No object to attach to!");
2195 r = max(0.01, get_radius(r=r, d=d, dflt=undef));
2196 check2 = assert(is_num(r), "Bad r/d argument.");
2197 corners = _corners(corners, except=except);
2198 vecs = [for (i = [0:7]) if (corners[i]>0) CORNER_OFFSETS[i]];
2199 all_vecs_are_corners = all([for (vec = vecs) sum(v_abs(vec))==3]);
2200 check3 = assert(all_vecs_are_corners, "All vectors must be corners.");
2201 for ($idx = idx(vecs)) {
2202 vec = vecs[$idx];
2203 anch = _find_anchor(vec, $parent_geom);
2204 $attach_to = undef;
2205 $attach_anchor = anch;
2206 $attach_norot = true;
2207 $profile_type = "corner";
2208 rotang = vec.z<0?
2209 [ 0,0,180+v_theta(vec)-45] :
2210 [180,0,-90+v_theta(vec)-45];
2211 default_tag("remove"){
2212 translate(anch[1]) {
2213 rot(rotang) {
2214 down(0.01) {
2215 linear_extrude(height=r+0.01, center=false) {
2216 difference() {
2217 translate(-[0.01,0.01]) square(r);
2218 translate([r,r]) circle(r=r*0.999);
2219 }
2220 }
2221 }
2222 translate([r,r]) zrot(180) {
2223 rotate_extrude(angle=90, convexity=convexity) {
2224 right(r) xflip() {
2225 children();
2226 }
2227 }
2228 }
2229 }
2230 }
2231 }
2232 }
2233}
2234
2235
2236// Section: Making your objects attachable
2237
2238
2239// Module: attachable()
2240// Synopsis: Manages the anchoring, spin, orientation, and attachments for an object.
2241// Topics: Attachments
2242// See Also: reorient()
2243// Usage: Square/Trapezoid Geometry
2244// attachable(anchor, spin, two_d=true, size=, [size2=], [shift=], [override=], ...) {OBJECT; children();}
2245// Usage: Circle/Oval Geometry
2246// attachable(anchor, spin, two_d=true, r=|d=, ...) {OBJECT; children();}
2247// Usage: 2D Path/Polygon Geometry
2248// attachable(anchor, spin, two_d=true, path=, [extent=], ...) {OBJECT; children();}
2249// Usage: 2D Region Geometry
2250// attachable(anchor, spin, two_d=true, region=, [extent=], ...) {OBJECT; children();}
2251// Usage: Cubical/Prismoidal Geometry
2252// attachable(anchor, spin, [orient], size=, [size2=], [shift=], [override=], ...) {OBJECT; children();}
2253// Usage: Cylindrical Geometry
2254// attachable(anchor, spin, [orient], r=|d=, l=, [axis=], ...) {OBJECT; children();}
2255// Usage: Conical Geometry
2256// attachable(anchor, spin, [orient], r1=|d1=, r2=|d2=, l=, [axis=], ...) {OBJECT; children();}
2257// Usage: Spheroid/Ovoid Geometry
2258// attachable(anchor, spin, [orient], r=|d=, ...) {OBJECT; children();}
2259// Usage: Extruded Path/Polygon Geometry
2260// attachable(anchor, spin, path=, l=|h=, [extent=], ...) {OBJECT; children();}
2261// Usage: Extruded Region Geometry
2262// attachable(anchor, spin, region=, l=|h=, [extent=], ...) {OBJECT; children();}
2263// Usage: VNF Geometry
2264// attachable(anchor, spin, [orient], vnf=, [extent=], ...) {OBJECT; children();}
2265// Usage: Pre-Specified Geometry
2266// attachable(anchor, spin, [orient], geom=) {OBJECT; children();}
2267//
2268// Description:
2269// Manages the anchoring, spin, orientation, and attachments for OBJECT, located in a 3D volume or 2D area.
2270// A managed 3D volume is assumed to be vertically (Z-axis) oriented, and centered.
2271// A managed 2D area is just assumed to be centered. The shape to be managed is given
2272// as the first child to this module, and the second child should be given as `children()`.
2273// For example, to manage a conical shape:
2274// ```openscad
2275// attachable(anchor, spin, orient, r1=r1, r2=r2, l=h) {
2276// cyl(r1=r1, r2=r2, l=h);
2277// children();
2278// }
2279// ```
2280// .
2281// If this is *not* run as a child of `attach()` with the `to` argument
2282// given, then the following transformations are performed in order:
2283// * Translates so the `anchor` point is at the origin (0,0,0).
2284// * Rotates around the Z axis by `spin` degrees counter-clockwise.
2285// * Rotates so the top of the part points towards the vector `orient`.
2286// .
2287// If this is called as a child of `attach(from,to)`, then the info
2288// for the anchor points referred to by `from` and `to` are fetched,
2289// which will include position, direction, and spin. With that info,
2290// the following transformations are performed:
2291// * Translates this part so it's anchor position matches the parent's anchor position.
2292// * Rotates this part so it's anchor direction vector exactly opposes the parent's anchor direction vector.
2293// * Rotates this part so it's anchor spin matches the parent's anchor spin.
2294// .
2295// In addition to handling positioning of the attachable object,
2296// this module is also responsible for handing coloring of objects with {{recolor()}} and {{color_this()}}, and
2297// it is responsible for processing tags and determining whether the object should
2298// display or not in the current context. The determination based on the tags of whether to display the attachable object
2299// often occurs in this module, which means that an object which does not display (e.g. a "remove" tagged object
2300// inside {{diff()}}) cannot have internal {{tag()}} calls that change its tags and cause submodel
2301// portions to display: the entire object simply does not run. If you want the use the attachable object's internal tags outside
2302// of the attachable object you can set `expose_tags=true` which delays the determination to display objects to the children.
2303// For this to work correctly, all of the children must be attachables. An example situation where you should set
2304// `expose_tags=true` is when you want to have negative space in an attachable object that gets removed from the parent via
2305// a "remove" tagged component of your attachable.
2306// .
2307// Application of {{recolor()}} and {{color_this()}} also happens in this module and normally it applies to the
2308// entire attachable object, so coloring commands that you give internally in the first child to `attachable()` have no effect.
2309// Generally it makes sense that if a user specifies a color for an attachable object, the entire object is displayed
2310// in that color, but if you want to retain control of color for sub-parts of an attachable object, you can use
2311// the `keep_color=true` option, which delays the assignment of colors to the child level. For this to work
2312// correctly, all of the sub-parts of your attachable object must be attachables. Also note that this option could
2313// be confusing to users who don't understand why color commands are not working on the object.
2314// .
2315// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
2316//
2317// Arguments:
2318// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
2319// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
2320// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
2321// ---
2322// size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length.
2323// size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape.
2324// shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift.
2325// r = Radius of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2326// d = Diameter of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2327// r1 = Radius of the bottom of the conical volume. Can be a scalar, or a list of sizes per axis.
2328// r2 = Radius of the top of the conical volume. Can be a scalar, or a list of sizes per axis.
2329// d1 = Diameter of the bottom of the conical volume. Can be a scalar, a list of sizes per axis.
2330// d2 = Diameter of the top of the conical volume. Can be a scalar, a list of sizes per axis.
2331// l/h = Length of the cylindrical, conical, or extruded path volume along axis.
2332// vnf = The [VNF](vnf.scad) of the volume.
2333// path = The path to generate a polygon from.
2334// region = The region to generate a shape from.
2335// extent = If true, calculate anchors by extents, rather than intersection, for VNFs and paths. Default: true.
2336// cp = If given, specifies the centerpoint of the volume. Default: `[0,0,0]`
2337// offset = If given, offsets the perimeter of the volume around the centerpoint.
2338// anchors = If given as a list of anchor points, allows named anchor points.
2339// two_d = If true, the attachable shape is 2D. If false, 3D. Default: false (3D)
2340// axis = The vector pointing along the axis of a geometry. Default: UP
2341// override = Function that takes an anchor and for 3d returns a triple `[position, direction, spin]` or for 2d returns a pair `[position,direction]` to use for that anchor to override the normal one. You can also supply a lookup table that is a list of `[anchor, [position, direction, spin]]` entries. If the direction/position/spin that is returned is undef then the default will be used. This option applies only to the "trapezoid" and "prismoid" geometry types.
2342// geom = If given, uses the pre-defined (via {{attach_geom()}} geometry.
2343// expose_tags = If true then delay the decision to display or not display this object to the children, which it possible for tags to respond to operations like {{diff()}} used outside the attachble object. Only works correctly if everything in the attachable is also attachable. Default: false
2344// keep_color = If true then delay application of color to the children, which means that externally applied color is overridden by color specified within the attachable. Only works properly if everything in the attachable is also attacahble. Default: false
2345//
2346// Side Effects:
2347// `$parent_anchor` is set to the parent object's `anchor` value.
2348// `$parent_spin` is set to the parent object's `spin` value.
2349// `$parent_orient` is set to the parent object's `orient` value.
2350// `$parent_geom` is set to the parent object's `geom` value.
2351// `$parent_size` is set to the parent object's cubical `[X,Y,Z]` volume size.
2352// `$color` is used to set the color of the object
2353// `$save_color` is used to revert color to the parent's color
2354//
2355// Example(NORENDER): Cubical Shape
2356// attachable(anchor, spin, orient, size=size) {
2357// cube(size, center=true);
2358// children();
2359// }
2360//
2361// Example(NORENDER): Prismoidal Shape
2362// attachable(
2363// anchor, spin, orient,
2364// size=point3d(botsize,h),
2365// size2=topsize,
2366// shift=shift
2367// ) {
2368// prismoid(botsize, topsize, h=h, shift=shift);
2369// children();
2370// }
2371//
2372// Example(NORENDER): Cylindrical Shape, Z-Axis Aligned
2373// attachable(anchor, spin, orient, r=r, l=h) {
2374// cyl(r=r, l=h);
2375// children();
2376// }
2377//
2378// Example(NORENDER): Cylindrical Shape, Y-Axis Aligned
2379// attachable(anchor, spin, orient, r=r, l=h, axis=BACK) {
2380// cyl(r=r, l=h);
2381// children();
2382// }
2383//
2384// Example(NORENDER): Cylindrical Shape, X-Axis Aligned
2385// attachable(anchor, spin, orient, r=r, l=h, axis=RIGHT) {
2386// cyl(r=r, l=h);
2387// children();
2388// }
2389//
2390// Example(NORENDER): Conical Shape, Z-Axis Aligned
2391// attachable(anchor, spin, orient, r1=r1, r2=r2, l=h) {
2392// cyl(r1=r1, r2=r2, l=h);
2393// children();
2394// }
2395//
2396// Example(NORENDER): Conical Shape, Y-Axis Aligned
2397// attachable(anchor, spin, orient, r1=r1, r2=r2, l=h, axis=BACK) {
2398// cyl(r1=r1, r2=r2, l=h);
2399// children();
2400// }
2401//
2402// Example(NORENDER): Conical Shape, X-Axis Aligned
2403// attachable(anchor, spin, orient, r1=r1, r2=r2, l=h, axis=RIGHT) {
2404// cyl(r1=r1, r2=r2, l=h);
2405// children();
2406// }
2407//
2408// Example(NORENDER): Spherical Shape
2409// attachable(anchor, spin, orient, r=r) {
2410// sphere(r=r);
2411// children();
2412// }
2413//
2414// Example(NORENDER): Extruded Polygon Shape, by Extents
2415// attachable(anchor, spin, orient, path=path, l=length) {
2416// linear_extrude(height=length, center=true)
2417// polygon(path);
2418// children();
2419// }
2420//
2421// Example(NORENDER): Extruded Polygon Shape, by Intersection
2422// attachable(anchor, spin, orient, path=path, l=length, extent=false) {
2423// linear_extrude(height=length, center=true)
2424// polygon(path);
2425// children();
2426// }
2427//
2428// Example(NORENDER): Arbitrary VNF Shape, by Extents
2429// attachable(anchor, spin, orient, vnf=vnf) {
2430// vnf_polyhedron(vnf);
2431// children();
2432// }
2433//
2434// Example(NORENDER): Arbitrary VNF Shape, by Intersection
2435// attachable(anchor, spin, orient, vnf=vnf, extent=false) {
2436// vnf_polyhedron(vnf);
2437// children();
2438// }
2439//
2440// Example(NORENDER): 2D Rectangular Shape
2441// attachable(anchor, spin, orient, two_d=true, size=size) {
2442// square(size, center=true);
2443// children();
2444// }
2445//
2446// Example(NORENDER): 2D Trapezoidal Shape
2447// attachable(
2448// anchor, spin, orient,
2449// two_d=true,
2450// size=[x1,y],
2451// size2=x2,
2452// shift=shift
2453// ) {
2454// trapezoid(w1=x1, w2=x2, h=y, shift=shift);
2455// children();
2456// }
2457//
2458// Example(NORENDER): 2D Circular Shape
2459// attachable(anchor, spin, orient, two_d=true, r=r) {
2460// circle(r=r);
2461// children();
2462// }
2463//
2464// Example(NORENDER): Arbitrary 2D Polygon Shape, by Extents
2465// attachable(anchor, spin, orient, two_d=true, path=path) {
2466// polygon(path);
2467// children();
2468// }
2469//
2470// Example(NORENDER): Arbitrary 2D Polygon Shape, by Intersection
2471// attachable(anchor, spin, orient, two_d=true, path=path, extent=false) {
2472// polygon(path);
2473// children();
2474// }
2475//
2476// Example(NORENDER): Using Pre-defined Geometry
2477// geom = atype=="perim"? attach_geom(two_d=true, path=path, extent=false) :
2478// atype=="extents"? attach_geom(two_d=true, path=path, extent=true) :
2479// atype=="circle"? attach_geom(two_d=true, r=r) :
2480// assert(false, "Bad atype");
2481// attachable(anchor, spin, orient, geom=geom) {
2482// polygon(path);
2483// children();
2484// }
2485//
2486// Example: An object can be designed to attach as negative space using {{diff()}}, but if you want an object to include both positive and negative space then you run into trouble because tags inside the `attachable()` are ignored. One solution is to call attachable() twice. This example shows how two calls to attachable can create an object with positive and negative space. Note, however, that children in the negative space are differenced away: the highlighted little cube does not survive into the final model.
2487// module thing(anchor,spin,orient) {
2488// tag("remove") attachable(size=[15,15,15],anchor=anchor,spin=spin,orient=orient){
2489// cuboid([10,10,16]);
2490// union(){} // dummy children
2491// }
2492// attachable(size=[15,15,15], anchor=anchor, spin=spin, orient=orient){
2493// cuboid([15,15,15]);
2494// children();
2495// }
2496// }
2497// diff()
2498// cube([19,10,19])
2499// attach([FRONT],overlap=-4)
2500// thing(anchor=TOP)
2501// # attach(TOP) cuboid(2,anchor=TOP);
2502// Example: Here is an example where the "keep" tag allows children to appear in the negative space. That tag is also needed for this module to produce the desired output. As above, the tag must be applied outside the attachable() call.
2503// module thing(anchor = CENTER, spin = 0, orient = UP) {
2504// tag("remove") attachable(anchor, spin, orient, d1=0,d2=95,h=33) {
2505// cylinder(h = 33.1, d1 = 0, d2 = 95, anchor=CENTER);
2506// union(){} // dummy children
2507// }
2508// tag("keep") attachable(anchor, spin, orient,d1=0,d2=95,h=33) {
2509// cylinder(h = 33, d = 10,anchor=CENTER);
2510// children();
2511// }
2512// }
2513// diff()
2514// cube(100)
2515// attach([FRONT,TOP],overlap=-4)
2516// thing(anchor=TOP)
2517// tube(ir=12,h=10);
2518// Example: A different way to achieve similar effects to the above to examples is to use the `expose_tags` parameter. This parameter allows you to use just one call to attachable. The second example above can also be rewritten like this.
2519// module thing(anchor,spin,orient) {
2520// attachable(size=[15,15,15],anchor=anchor,spin=spin,orient=orient,expose_tags=true){
2521// union(){
2522// cuboid([15,15,15]);
2523// tag("remove")cuboid([10,10,16]);
2524// }
2525// children();
2526// }
2527// }
2528// diff()
2529// cube([19,10,19])
2530// attach([FRONT],overlap=-4)
2531// thing(anchor=TOP);
2532// Example: An advantage of using `expose_tags` is that it can work on nested constructions. Here the child cylinder is aligned relative to its parent and removed from the calling parent object.
2533// $fn=64;
2534// module thing(anchor=BOT){
2535// attachable(anchor = anchor,d=9,h=6,expose_tags=true){
2536// cyl(d = 9, h = 6)
2537// tag("remove")
2538// align(RIGHT+TOP,inside=true)
2539// left(1)up(1)cyl(l=11, d=3);
2540// children();
2541// }
2542// }
2543// back_half()
2544// diff()
2545// cuboid(10)
2546// position(TOP)thing(anchor=BOT);
2547// Example(3D,NoAxes): Here an attachable module uses {{recolor()}} to change the color of a sub-part, producing the result shown on the left. But if the caller applies color to the attachable, then both the green and yellow are changed, as shown on the right.
2548// module thing(anchor=CENTER) {
2549// attachable(anchor,size=[10,10,10]) {
2550// cuboid(10)
2551// position(TOP) recolor("green")
2552// cuboid(5,anchor=BOT);
2553// children();
2554// }
2555// }
2556// move([-15,-15])
2557// thing()
2558// attach(RIGHT,BOT)
2559// recolor("blue") cyl(d=5,h=5);
2560// recolor("pink") thing()
2561// attach(RIGHT,BOT)
2562// recolor("blue") cyl(d=5,h=5);
2563// Example(3D,NoAxes): Using the `keep_color=true` option enables the green color to persist, even when the user specifies a color.
2564// module thing(anchor=CENTER) {
2565// attachable(anchor,size=[10,10,10],keep_color=true) {
2566// cuboid(10)
2567// position(TOP) recolor("green")
2568// cuboid(5,anchor=BOT);
2569// children();
2570// }
2571// }
2572// recolor("pink") thing()
2573// attach(RIGHT,BOT)
2574// recolor("blue") cyl(d=5,h=5);
2575
2576module attachable(
2577 anchor, spin, orient,
2578 size, size2, shift,
2579 r,r1,r2, d,d1,d2, l,h,
2580 vnf, path, region,
2581 extent=true,
2582 cp=[0,0,0],
2583 offset=[0,0,0],
2584 anchors=[],
2585 two_d=false,
2586 axis=UP,override,
2587 geom,
2588 expose_tags=false, keep_color=false
2589) {
2590 dummy1 =
2591 assert($children==2, "attachable() expects exactly two children; the shape to manage, and the union of all attachment candidates.")
2592 assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor))
2593 assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin))
2594 assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient));
2595 anchor = first_defined([$anchor_override, anchor, CENTER]);
2596 spin = default(spin, 0);
2597 orient = is_def($anchor_override)? UP : default(orient, UP);
2598 region = !is_undef(region)? region :
2599 !is_undef(path)? [path] :
2600 undef;
2601 geom = is_def(geom)? geom :
2602 attach_geom(
2603 size=size, size2=size2, shift=shift,
2604 r=r, r1=r1, r2=r2, h=h,
2605 d=d, d1=d1, d2=d2, l=l,
2606 vnf=vnf, region=region, extent=extent,
2607 cp=cp, offset=offset, anchors=anchors,
2608 two_d=two_d, axis=axis, override=override
2609 );
2610 m = _attach_transform(anchor,spin,orient,geom);
2611 multmatrix(m) {
2612 $parent_anchor = anchor;
2613 $parent_spin = spin;
2614 $parent_orient = orient;
2615 $parent_geom = geom;
2616 $parent_size = _attach_geom_size(geom);
2617 $attach_to = undef;
2618 $anchor_override=undef;
2619 if (expose_tags || _is_shown()){
2620 if (!keep_color)
2621 _color($color) children(0);
2622 else {
2623 $save_color=undef; // Force color_this() color in effect to persist for the entire object
2624 children(0);
2625 }
2626 }
2627 if (is_def($save_color)) {
2628 $color=$save_color; // Revert to the color before color_this() call
2629 $save_color=undef;
2630 children(1);
2631 }
2632 else children(1);
2633 }
2634}
2635
2636// Function: reorient()
2637// Synopsis: Calculates the transformation matrix needed to reorient an object.
2638// SynTags: Trans, Path, VNF
2639// Topics: Attachments
2640// See Also: reorient(), attachable()
2641// Usage: Square/Trapezoid Geometry
2642// mat = reorient(anchor, spin, [orient], two_d=true, size=, [size2=], [shift=], ...);
2643// pts = reorient(anchor, spin, [orient], two_d=true, size=, [size2=], [shift=], p=, ...);
2644// Usage: Circle/Oval Geometry
2645// mat = reorient(anchor, spin, [orient], two_d=true, r=|d=, ...);
2646// pts = reorient(anchor, spin, [orient], two_d=true, r=|d=, p=, ...);
2647// Usage: 2D Path/Polygon Geometry
2648// mat = reorient(anchor, spin, [orient], two_d=true, path=, [extent=], ...);
2649// pts = reorient(anchor, spin, [orient], two_d=true, path=, [extent=], p=, ...);
2650// Usage: 2D Region/Polygon Geometry
2651// mat = reorient(anchor, spin, [orient], two_d=true, region=, [extent=], ...);
2652// pts = reorient(anchor, spin, [orient], two_d=true, region=, [extent=], p=, ...);
2653// Usage: Cubical/Prismoidal Geometry
2654// mat = reorient(anchor, spin, [orient], size=, [size2=], [shift=], ...);
2655// vnf = reorient(anchor, spin, [orient], size=, [size2=], [shift=], p=, ...);
2656// Usage: Cylindrical Geometry
2657// mat = reorient(anchor, spin, [orient], r=|d=, l=, [axis=], ...);
2658// vnf = reorient(anchor, spin, [orient], r=|d=, l=, [axis=], p=, ...);
2659// Usage: Conical Geometry
2660// mat = reorient(anchor, spin, [orient], r1=|d1=, r2=|d2=, l=, [axis=], ...);
2661// vnf = reorient(anchor, spin, [orient], r1=|d1=, r2=|d2=, l=, [axis=], p=, ...);
2662// Usage: Spheroid/Ovoid Geometry
2663// mat = reorient(anchor, spin, [orient], r|d=, ...);
2664// vnf = reorient(anchor, spin, [orient], r|d=, p=, ...);
2665// Usage: Extruded Path/Polygon Geometry
2666// mat = reorient(anchor, spin, [orient], path=, l=|h=, [extent=], ...);
2667// vnf = reorient(anchor, spin, [orient], path=, l=|h=, [extent=], p=, ...);
2668// Usage: Extruded Region Geometry
2669// mat = reorient(anchor, spin, [orient], region=, l=|h=, [extent=], ...);
2670// vnf = reorient(anchor, spin, [orient], region=, l=|h=, [extent=], p=, ...);
2671// Usage: VNF Geometry
2672// mat = reorient(anchor, spin, [orient], vnf, [extent], ...);
2673// vnf = reorient(anchor, spin, [orient], vnf, [extent], p=, ...);
2674//
2675// Description:
2676// Given anchor, spin, orient, and general geometry info for a managed volume, this calculates
2677// the transformation matrix needed to be applied to the contents of that volume. A managed 3D
2678// volume is assumed to be vertically (Z-axis) oriented, and centered. A managed 2D area is just
2679// assumed to be centered.
2680// .
2681// If `p` is not given, then the transformation matrix will be returned.
2682// If `p` contains a VNF, a new VNF will be returned with the vertices transformed by the matrix.
2683// If `p` contains a path, a new path will be returned with the vertices transformed by the matrix.
2684// If `p` contains a point, a new point will be returned, transformed by the matrix.
2685// .
2686// If `$attach_to` is not defined, then the following transformations are performed in order:
2687// * Translates so the `anchor` point is at the origin (0,0,0).
2688// * Rotates around the Z axis by `spin` degrees counter-clockwise.
2689// * Rotates so the top of the part points towards the vector `orient`.
2690// .
2691// If `$attach_to` is defined, as a consequence of `attach(from,to)`, then
2692// the following transformations are performed in order:
2693// * Translates this part so it's anchor position matches the parent's anchor position.
2694// * Rotates this part so it's anchor direction vector exactly opposes the parent's anchor direction vector.
2695// * Rotates this part so it's anchor spin matches the parent's anchor spin.
2696// .
2697// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
2698//
2699// Arguments:
2700// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
2701// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
2702// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
2703// ---
2704// size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length.
2705// size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape.
2706// shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift.
2707// r = Radius of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2708// d = Diameter of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2709// r1 = Radius of the bottom of the conical volume. Can be a scalar, or a list of sizes per axis.
2710// r2 = Radius of the top of the conical volume. Can be a scalar, or a list of sizes per axis.
2711// d1 = Diameter of the bottom of the conical volume. Can be a scalar, a list of sizes per axis.
2712// d2 = Diameter of the top of the conical volume. Can be a scalar, a list of sizes per axis.
2713// l/h = Length of the cylindrical, conical, or extruded path volume along axis.
2714// vnf = The [VNF](vnf.scad) of the volume.
2715// path = The path to generate a polygon from.
2716// region = The region to generate a shape from.
2717// extent = If true, calculate anchors by extents, rather than intersection. Default: false.
2718// cp = If given, specifies the centerpoint of the volume. Default: `[0,0,0]`
2719// offset = If given, offsets the perimeter of the volume around the centerpoint.
2720// anchors = If given as a list of anchor points, allows named anchor points.
2721// two_d = If true, the attachable shape is 2D. If false, 3D. Default: false (3D)
2722// axis = The vector pointing along the axis of a geometry. Default: UP
2723// p = The VNF, path, or point to transform.
2724function reorient(
2725 anchor, spin, orient,
2726 size, size2, shift,
2727 r,r1,r2, d,d1,d2, l,h,
2728 vnf, path, region,
2729 extent=true,
2730 offset=[0,0,0],
2731 cp=[0,0,0],
2732 anchors=[],
2733 two_d=false,
2734 axis=UP, override,
2735 geom,
2736 p=undef
2737) =
2738 assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor))
2739 assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin))
2740 assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient))
2741 let(
2742 anchor = default(anchor, CENTER),
2743 spin = default(spin, 0),
2744 orient = default(orient, UP),
2745 region = !is_undef(region)? region :
2746 !is_undef(path)? [path] :
2747 undef,
2748 geom = is_def(geom)? geom :
2749 attach_geom(
2750 size=size, size2=size2, shift=shift,
2751 r=r, r1=r1, r2=r2, h=h,
2752 d=d, d1=d1, d2=d2, l=l,
2753 vnf=vnf, region=region, extent=extent,
2754 cp=cp, offset=offset, anchors=anchors,
2755 two_d=two_d, axis=axis, override=override
2756 ),
2757 $attach_to = undef
2758 ) _attach_transform(anchor,spin,orient,geom,p);
2759
2760
2761// Function: named_anchor()
2762// Synopsis: Creates an anchor data structure.
2763// Topics: Attachments
2764// See Also: reorient(), attachable()
2765// Usage:
2766// a = named_anchor(name, pos, [orient], [spin]);
2767// a = named_anchor(name, [pos], rot=, [flip=]);
2768// Description:
2769// Creates an anchor data structure. You can specify the position, orient direction and spin directly.
2770// Alternatively for the 3D case you can give a 4x4 rotation matrix which can specify the orient and spin, and optionally
2771// the position, using a translation component of the matrix. If you specify `pos` along with `rot` then the position you
2772// give overrides any translation included in `rot`. For a step-by-step explanation of attachments,
2773// see the [Attachments Tutorial](Tutorial-Attachments).
2774// Arguments:
2775// name = The string name of the anchor. Lowercase. Words separated by single dashes. No spaces.
2776// pos = The [X,Y,Z] position of the anchor.
2777// orient = A vector pointing in the direction parts should project from the anchor position. Default: UP
2778// spin = If needed, the angle to rotate the part around the direction vector. Default: 0
2779// ---
2780// rot = A 4x4 rotations matrix, which may include a translation
2781// flip = If true, flip the anchor the opposite direction. Default: false
2782function named_anchor(name, pos, orient, spin, rot, flip) =
2783 assert(num_defined([orient,spin])==0 || num_defined([rot,flip])==0, "Cannot mix orient or spin with rot or flip")
2784 assert(num_defined([pos,rot])>0, "Must give pos or rot")
2785 is_undef(rot) ? [name, pos, default(orient,UP), default(spin,0)]
2786 :
2787 let(
2788 flip = default(flip,false),
2789 pos = default(pos,apply(rot,CTR)),
2790 rotpart = _force_rot(rot),
2791 dummy = assert(approx(det4(rotpart),1), "Input rotation is not a rotation matrix"),
2792 dir = flip ? apply(rotpart,DOWN)
2793 : apply(rotpart,UP),
2794 rot = flip? affine3d_rot_by_axis(apply(rotpart,BACK),180)*rot
2795 : rot,
2796 decode=rot_decode(rot(to=UP,from=dir)*_force_rot(rot)),
2797 spin = decode[0]*sign(decode[1].z)
2798 )
2799 [name, pos, dir, spin];
2800
2801
2802function _force_rot(T) =
2803 [for(i=[0:3])
2804 [for(j=[0:3]) j<3 ? T[i][j] :
2805 i==3 ? 1
2806 : 0]];
2807
2808// Function: attach_geom()
2809// Synopsis: Returns the internal geometry description of an attachable object.
2810// Topics: Attachments
2811// See Also: reorient(), attachable()
2812// Usage: Null/Point Geometry
2813// geom = attach_geom(...);
2814// Usage: Square/Trapezoid Geometry
2815// geom = attach_geom(two_d=true, size=, [size2=], [shift=], ...);
2816// Usage: Circle/Oval Geometry
2817// geom = attach_geom(two_d=true, r=|d=, ...);
2818// Usage: 2D Path/Polygon/Region Geometry
2819// geom = attach_geom(two_d=true, region=, [extent=], ...);
2820// Usage: Cubical/Prismoidal Geometry
2821// geom = attach_geom(size=, [size2=], [shift=], ...);
2822// Usage: Cylindrical Geometry
2823// geom = attach_geom(r=|d=, l=|h=, [axis=], ...);
2824// Usage: Conical Geometry
2825// geom = attach_geom(r1|d1=, r2=|d2=, l=, [axis=], ...);
2826// Usage: Spheroid/Ovoid Geometry
2827// geom = attach_geom(r=|d=, ...);
2828// Usage: Extruded 2D Path/Polygon/Region Geometry
2829// geom = attach_geom(region=, l=|h=, [extent=], [shift=], [scale=], [twist=], ...);
2830// Usage: VNF Geometry
2831// geom = attach_geom(vnf=, [extent=], ...);
2832//
2833// Description:
2834// Given arguments that describe the geometry of an attachable object, returns the internal geometry description.
2835// This will probably not not ever need to be called by the end user.
2836//
2837// Arguments:
2838// ---
2839// size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length.
2840// size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape.
2841// shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift.
2842// scale = If given as number or a 2D vector, scales the top of the shape, relative to the bottom. Default: `[1,1]`
2843// twist = If given as number, rotates the top of the shape by the given number of degrees clockwise, relative to the bottom. Default: `0`
2844// r = Radius of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2845// d = Diameter of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2846// r1 = Radius of the bottom of the conical volume. Can be a scalar, or a list of sizes per axis.
2847// r2 = Radius of the top of the conical volume. Can be a scalar, or a list of sizes per axis.
2848// d1 = Diameter of the bottom of the conical volume. Can be a scalar, a list of sizes per axis.
2849// d2 = Diameter of the top of the conical volume. Can be a scalar, a list of sizes per axis.
2850// l/h = Length of the cylindrical, conical or extruded region volume along axis.
2851// vnf = The [VNF](vnf.scad) of the volume.
2852// region = The region to generate a shape from.
2853// extent = If true, calculate anchors by extents, rather than intersection. Default: true.
2854// cp = If given, specifies the centerpoint of the volume. Default: `[0,0,0]`
2855// offset = If given, offsets the perimeter of the volume around the centerpoint.
2856// anchors = If given as a list of anchor points, allows named anchor points.
2857// two_d = If true, the attachable shape is 2D. If false, 3D. Default: false (3D)
2858// axis = The vector pointing along the axis of a geometry. Default: UP
2859// override = Function that takes an anchor and returns a pair `[position,direction]` to use for that anchor to override the normal one. You can also supply a lookup table that is a list of `[anchor, [position, direction]]` entries. If the direction/position that is returned is undef then the default will be used.
2860//
2861// Example(NORENDER): Null/Point Shape
2862// geom = attach_geom();
2863//
2864// Example(NORENDER): Cubical Shape
2865// geom = attach_geom(size=size);
2866//
2867// Example(NORENDER): Prismoidal Shape
2868// geom = attach_geom(
2869// size=point3d(botsize,h),
2870// size2=topsize, shift=shift
2871// );
2872//
2873// Example(NORENDER): Cylindrical Shape, Z-Axis Aligned
2874// geom = attach_geom(r=r, h=h);
2875//
2876// Example(NORENDER): Cylindrical Shape, Y-Axis Aligned
2877// geom = attach_geom(r=r, h=h, axis=BACK);
2878//
2879// Example(NORENDER): Cylindrical Shape, X-Axis Aligned
2880// geom = attach_geom(r=r, h=h, axis=RIGHT);
2881//
2882// Example(NORENDER): Conical Shape, Z-Axis Aligned
2883// geom = attach_geom(r1=r1, r2=r2, h=h);
2884//
2885// Example(NORENDER): Conical Shape, Y-Axis Aligned
2886// geom = attach_geom(r1=r1, r2=r2, h=h, axis=BACK);
2887//
2888// Example(NORENDER): Conical Shape, X-Axis Aligned
2889// geom = attach_geom(r1=r1, r2=r2, h=h, axis=RIGHT);
2890//
2891// Example(NORENDER): Spherical Shape
2892// geom = attach_geom(r=r);
2893//
2894// Example(NORENDER): Ovoid Shape
2895// geom = attach_geom(r=[r_x, r_y, r_z]);
2896//
2897// Example(NORENDER): Arbitrary VNF Shape, Anchored by Extents
2898// geom = attach_geom(vnf=vnf);
2899//
2900// Example(NORENDER): Arbitrary VNF Shape, Anchored by Intersection
2901// geom = attach_geom(vnf=vnf, extent=false);
2902//
2903// Example(NORENDER): 2D Rectangular Shape
2904// geom = attach_geom(two_d=true, size=size);
2905//
2906// Example(NORENDER): 2D Trapezoidal Shape
2907// geom = attach_geom(two_d=true, size=[x1,y], size2=x2, shift=shift, override=override);
2908//
2909// Example(NORENDER): 2D Circular Shape
2910// geom = attach_geom(two_d=true, r=r);
2911//
2912// Example(NORENDER): 2D Oval Shape
2913// geom = attach_geom(two_d=true, r=[r_x, r_y]);
2914//
2915// Example(NORENDER): Arbitrary 2D Region Shape, Anchored by Extents
2916// geom = attach_geom(two_d=true, region=region);
2917//
2918// Example(NORENDER): Arbitrary 2D Region Shape, Anchored by Intersection
2919// geom = attach_geom(two_d=true, region=region, extent=false);
2920//
2921// Example(NORENDER): Extruded Region, Anchored by Extents
2922// geom = attach_geom(region=region, l=height);
2923//
2924// Example(NORENDER): Extruded Region, Anchored by Intersection
2925// geom = attach_geom(region=region, l=length, extent=false);
2926//
2927
2928function _local_struct_val(struct, key)=
2929 assert(is_def(key),"key is missing")
2930 let(ind = search([key],struct)[0])
2931 ind == [] ? undef : struct[ind][1];
2932
2933
2934function attach_geom(
2935 size, size2,
2936 shift, scale, twist,
2937 r,r1,r2, d,d1,d2, l,h,
2938 vnf, region,
2939 extent=true,
2940 cp=[0,0,0],
2941 offset=[0,0,0],
2942 anchors=[],
2943 two_d=false,
2944 axis=UP, override
2945) =
2946 assert(is_bool(extent))
2947 assert(is_vector(cp) || is_string(cp))
2948 assert(is_vector(offset))
2949 assert(is_list(anchors))
2950 assert(is_bool(two_d))
2951 assert(is_vector(axis))
2952 !is_undef(size)? (
2953 let(
2954 over_f = is_undef(override) ? function(anchor) [undef,undef,undef]
2955 : is_func(override) ? override
2956 : function(anchor) _local_struct_val(override,anchor)
2957 )
2958 two_d? (
2959 let(
2960 size2 = default(size2, size.x),
2961 shift = default(shift, 0)
2962 )
2963 assert(is_vector(size,2))
2964 assert(is_num(size2))
2965 assert(is_num(shift))
2966 ["trapezoid", point2d(size), size2, shift, over_f, cp, offset, anchors]
2967 ) : (
2968 let(
2969 size2 = default(size2, point2d(size)),
2970 shift = default(shift, [0,0])
2971 )
2972 assert(is_vector(size,3))
2973 assert(is_vector(size2,2))
2974 assert(is_vector(shift,2))
2975 ["prismoid", size, size2, shift, axis, over_f, cp, offset, anchors]
2976 )
2977 ) : !is_undef(vnf)? (
2978 assert(is_vnf(vnf))
2979 assert(two_d == false)
2980 extent? ["vnf_extent", vnf, cp, offset, anchors] :
2981 ["vnf_isect", vnf, cp, offset, anchors]
2982 ) : !is_undef(region)? (
2983 assert(is_region(region),2)
2984 let( l = default(l, h) )
2985 two_d==true
2986 ? assert(is_undef(l))
2987 extent==true
2988 ? ["rgn_extent", region, cp, offset, anchors]
2989 : ["rgn_isect", region, cp, offset, anchors]
2990 : assert(is_finite(l))
2991 let(
2992 shift = default(shift, [0,0]),
2993 scale = is_num(scale)? [scale,scale] : default(scale, [1,1]),
2994 twist = default(twist, 0)
2995 )
2996 assert(is_vector(shift,2))
2997 assert(is_vector(scale,2))
2998 assert(is_num(twist))
2999 extent==true
3000 ? ["extrusion_extent", region, l, twist, scale, shift, cp, offset, anchors]
3001 : ["extrusion_isect", region, l, twist, scale, shift, cp, offset, anchors]
3002 ) :
3003 let(
3004 r1 = get_radius(r1=r1,d1=d1,r=r,d=d,dflt=undef)
3005 )
3006 !is_undef(r1)? (
3007 let( l = default(l, h) )
3008 !is_undef(l)? (
3009 let(
3010 shift = default(shift, [0,0]),
3011 r2 = get_radius(r1=r2,d1=d2,r=r,d=d,dflt=undef)
3012 )
3013 assert(is_num(r1) || is_vector(r1,2))
3014 assert(is_num(r2) || is_vector(r2,2))
3015 assert(is_num(l))
3016 assert(is_vector(shift,2))
3017 ["conoid", r1, r2, l, shift, axis, cp, offset, anchors]
3018 ) : (
3019 two_d? (
3020 assert(is_num(r1) || is_vector(r1,2))
3021 ["ellipse", r1, cp, offset, anchors]
3022 ) : (
3023 assert(is_num(r1) || is_vector(r1,3))
3024 ["spheroid", r1, cp, offset, anchors]
3025 )
3026 )
3027 ) :
3028 ["point", cp, offset, anchors];
3029
3030
3031
3032
3033
3034
3035//////////////////////////////////////////////////////////////////////////////////////////////////////////////
3036//
3037// Attachment internal functions
3038
3039
3040/// Internal Function: _attach_geom_2d()
3041/// Topics: Attachments
3042/// See Also: reorient(), attachable()
3043/// Usage:
3044/// bool = _attach_geom_2d(geom);
3045/// Description:
3046/// Returns true if the given attachment geometry description is for a 2D shape.
3047function _attach_geom_2d(geom) =
3048 let( type = geom[0] )
3049 type == "trapezoid" || type == "ellipse" ||
3050 type == "rgn_isect" || type == "rgn_extent";
3051
3052
3053/// Internal Function: _attach_geom_size()
3054/// Usage:
3055/// bounds = _attach_geom_size(geom);
3056/// Topics: Attachments
3057/// See Also: reorient(), attachable()
3058/// Description:
3059/// Returns the `[X,Y,Z]` bounding size for the given attachment geometry description.
3060function _attach_geom_size(geom) =
3061 let( type = geom[0] )
3062 type == "point"? [0,0,0] :
3063 type == "prismoid"? ( //size, size2, shift, axis
3064 let(
3065 size=geom[1], size2=geom[2], shift=point2d(geom[3]),
3066 maxx = max(size.x,size2.x),
3067 maxy = max(size.y,size2.y),
3068 z = size.z
3069 ) [maxx, maxy, z]
3070 ) : type == "conoid"? ( //r1, r2, l, shift
3071 let(
3072 r1=geom[1], r2=geom[2], l=geom[3],
3073 shift=point2d(geom[4]), axis=point3d(geom[5]),
3074 rx1 = default(r1[0],r1),
3075 ry1 = default(r1[1],r1),
3076 rx2 = default(r2[0],r2),
3077 ry2 = default(r2[1],r2),
3078 maxxr = max(rx1,rx2),
3079 maxyr = max(ry1,ry2)
3080 )
3081 approx(axis,UP)? [2*maxxr,2*maxyr,l] :
3082 approx(axis,RIGHT)? [l,2*maxyr,2*maxxr] :
3083 approx(axis,BACK)? [2*maxxr,l,2*maxyr] :
3084 [2*maxxr, 2*maxyr, l]
3085 ) : type == "spheroid"? ( //r
3086 let( r=geom[1] )
3087 is_num(r)? [2,2,2]*r : v_mul([2,2,2],point3d(r))
3088 ) : type == "vnf_extent" || type=="vnf_isect"? ( //vnf
3089 let(
3090 vnf = geom[1]
3091 ) vnf==EMPTY_VNF? [0,0,0] :
3092 let(
3093 mm = pointlist_bounds(geom[1][0]),
3094 delt = mm[1]-mm[0]
3095 ) delt
3096 ) : type == "extrusion_isect" || type == "extrusion_extent"? ( //path, l
3097 let(
3098 mm = pointlist_bounds(flatten(geom[1])),
3099 delt = mm[1]-mm[0]
3100 ) [delt.x, delt.y, geom[2]]
3101 ) : type == "trapezoid"? ( //size, size2
3102 let(
3103 size=geom[1], size2=geom[2], shift=geom[3],
3104 maxx = max(size.x,size2+abs(shift))
3105 ) [maxx, size.y]
3106 ) : type == "ellipse"? ( //r
3107 let( r=geom[1] )
3108 is_num(r)? [2,2]*r : v_mul([2,2],point2d(r))
3109 ) : type == "rgn_isect" || type == "rgn_extent"? ( //path
3110 let(
3111 mm = pointlist_bounds(flatten(geom[1])),
3112 delt = mm[1]-mm[0]
3113 ) [delt.x, delt.y]
3114 ) :
3115 assert(false, "Unknown attachment geometry type.");
3116
3117
3118
3119/// Internal Function: _attach_geom_edge_path()
3120/// Usage:
3121/// angle = _attach_geom_edge_path(geom, edge);
3122/// Topics: Attachments
3123/// See Also: reorient(), attachable()
3124/// Description:
3125/// Returns the path and post-transform matrix of the indicated edge.
3126/// If the edge is invalid for the geometry, returns `undef`.
3127function _attach_geom_edge_path(geom, edge) =
3128 assert(is_vector(edge),str("Invalid edge: edge=",edge))
3129 let(
3130 type = geom[0],
3131 cp = _get_cp(geom),
3132 offset_raw = select(geom,-2),
3133 offset = [for (i=[0:2]) edge[i]==0? 0 : offset_raw[i]], // prevents bad centering.
3134 edge = point3d(edge)
3135 )
3136 type == "prismoid"? ( //size, size2, shift, axis
3137 let(all_comps_good = [for (c=edge) if (c!=sign(c)) 1]==[])
3138 assert(all_comps_good, "All components of an edge for a cuboid/prismoid must be -1, 0, or 1")
3139 let(edge_good = len([for (c=edge) if(c) 1])==2)
3140 assert(edge_good, "Invalid edge.")
3141 let(
3142 size = geom[1],
3143 size2 = geom[2],
3144 shift = point2d(geom[3]),
3145 axis = point3d(geom[4]),
3146 edge = rot(from=axis, to=UP, p=edge),
3147 offset = rot(from=axis, to=UP, p=offset),
3148 h = size.z,
3149 cpos = function(vec) let(
3150 u = (vec.z + 1) / 2,
3151 siz = lerp(point2d(size), size2, u) / 2,
3152 z = vec.z * h / 2,
3153 pos = point3d(v_mul(siz, point2d(vec)) + shift * u, z)
3154 ) pos,
3155 ep1 = cpos([for (c=edge) c? c : -1]),
3156 ep2 = cpos([for (c=edge) c? c : 1]),
3157 cp = (ep1 + ep2) / 2,
3158 axy = point2d(edge),
3159 bot = point3d(v_mul(point2d(size )/2, axy), -h/2),
3160 top = point3d(v_mul(point2d(size2)/2, axy) + shift, h/2),
3161 xang = atan2(h,(top-bot).x),
3162 yang = atan2(h,(top-bot).y),
3163 vecs = [
3164 if (edge.x) yrot(90-xang, p=sign(axy.x)*RIGHT),
3165 if (edge.y) xrot(yang-90, p=sign(axy.y)*BACK),
3166 if (edge.z) [0,0,sign(edge.z)]
3167 ],
3168 segvec = cross(unit(vecs[1]), unit(vecs[0])),
3169 seglen = norm(ep2 - ep1),
3170 path = [
3171 cp - segvec * seglen/2,
3172 cp + segvec * seglen/2
3173 ],
3174 m = rot(from=UP,to=axis) * move(offset)
3175 ) [path, [vecs], m]
3176 ) : type == "conoid"? ( //r1, r2, l, shift, axis
3177 assert(edge.z && edge.z == sign(edge.z), "The Z component of an edge for a cylinder/cone must be -1 or 1")
3178 let(
3179 rr1 = geom[1],
3180 rr2 = geom[2],
3181 l = geom[3],
3182 shift = point2d(geom[4]),
3183 axis = point3d(geom[5]),
3184 r1 = is_num(rr1)? [rr1,rr1] : point2d(rr1),
3185 r2 = is_num(rr2)? [rr2,rr2] : point2d(rr2),
3186 edge = rot(from=axis, to=UP, p=edge),
3187 offset = rot(from=axis, to=UP, p=offset),
3188 maxr = max([each r1, each r2]),
3189 sides = segs(maxr),
3190 top = path3d(move(shift, p=ellipse(r=r2, $fn=sides)), l/2),
3191 bot = path3d(ellipse(r=r1, $fn=sides), -l/2),
3192 path = edge.z < 0 ? bot : top,
3193 path2 = edge.z < 0 ? top : bot,
3194 zed = edge.z<0? [0,0,-l/2] : point3d(shift,l/2),
3195 vecs = [
3196 for (i = idx(top)) let(
3197 pt1 = (path[i] + select(path,i+1)) /2,
3198 pt2 = (path2[i] + select(path2,i+1)) /2,
3199 v1 = unit(zed - pt1),
3200 v2 = unit(pt2 - pt1),
3201 v3 = unit(cross(v1,v2)),
3202 v4 = cross(v3,v2),
3203 v5 = cross(v1,v3)
3204 ) [v4, v5]
3205 ],
3206 m = rot(from=UP,to=axis) * move(offset)
3207 ) edge.z>0
3208 ? [reverse(list_wrap(path)), reverse(vecs), m]
3209 : [list_wrap(path), vecs, m]
3210 ) : undef;
3211
3212
3213/// Internal Function: _attach_transform()
3214/// Usage: To Get a Transformation Matrix
3215/// mat = _attach_transform(anchor, spin, orient, geom);
3216/// Usage: To Transform Points, Paths, Patches, or VNFs
3217/// new_p = _attach_transform(anchor, spin, orient, geom, p);
3218/// Topics: Attachments
3219/// See Also: reorient(), attachable()
3220/// Description:
3221/// Returns the affine3d transformation matrix needed to `anchor`, `spin`, and `orient`
3222/// the given geometry `geom` shape into position.
3223/// Arguments:
3224/// anchor = Anchor point to translate to the origin `[0,0,0]`. See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
3225/// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
3226/// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
3227/// geom = The geometry description of the shape.
3228/// p = If given as a VNF, path, or point, applies the affine3d transformation matrix to it and returns the result.
3229function _attach_transform(anchor, spin, orient, geom, p) =
3230 assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor))
3231 assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin))
3232 assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient))
3233 let(
3234 anchor = default(anchor, CENTER),
3235 spin = default(spin, 0),
3236 orient = default(orient, UP),
3237 two_d = _attach_geom_2d(geom),
3238 m = ($attach_to != undef)? (
3239 let(
3240 anch = _find_anchor($attach_to, geom),
3241 pos = anch[1]
3242 )
3243 two_d?
3244 assert(is_num(spin))
3245 affine3d_zrot(spin)
3246 * rot(to=FWD, from=point3d(anch[2]))
3247 * affine3d_translate(point3d(-pos))
3248 :
3249 assert(is_num(spin) || is_vector(spin,3))
3250 let(
3251 ang = vector_angle(anch[2], DOWN),
3252 axis = vector_axis(anch[2], DOWN),
3253 ang2 = (anch[2]==UP || anch[2]==DOWN)? 0 : 180-anch[3],
3254 axis2 = rot(p=axis,[0,0,ang2])
3255 )
3256 affine3d_rot_by_axis(axis2,ang)
3257 * (is_num(spin)? affine3d_zrot(ang2+spin)
3258 : affine3d_zrot(spin.z) * affine3d_yrot(spin.y) * affine3d_xrot(spin.x)
3259 * affine3d_zrot(ang2))
3260 * affine3d_translate(point3d(-pos))
3261 ) : (
3262 let(
3263 pos = _find_anchor(anchor, geom)[1]
3264 )
3265 two_d?
3266 assert(is_num(spin))
3267 affine3d_zrot(spin) * affine3d_translate(point3d(-pos))
3268 :
3269 assert(is_num(spin) || is_vector(spin,3))
3270 let(
3271 axis = vector_axis(UP,orient),
3272 ang = vector_angle(UP,orient)
3273 )
3274 affine3d_rot_by_axis(axis,ang)
3275 * ( is_num(spin)? affine3d_zrot(spin)
3276 : affine3d_zrot(spin.z) * affine3d_yrot(spin.y) * affine3d_xrot(spin.x))
3277 * affine3d_translate(point3d(-pos))
3278 )
3279 )
3280 is_undef(p)? m
3281 : is_vnf(p) && p==EMPTY_VNF? p
3282 : apply(m, p);
3283
3284
3285function _get_cp(geom) =
3286 let(cp=select(geom,-3))
3287 is_vector(cp) ? cp
3288 : let(
3289 type = in_list(geom[0],["vnf_extent","vnf_isect"]) ? "vnf"
3290 : in_list(geom[0],["rgn_extent","rgn_isect"]) ? "path"
3291 : in_list(geom[0],["extrusion_extent","extrusion_isect"]) ? "xpath"
3292 : "other"
3293 )
3294 assert(type!="other", "Invalid cp value")
3295 cp=="centroid" ? (
3296 type=="vnf" && (len(geom[1][0])==0 || len(geom[1][1])==0) ? [0,0,0] :
3297 [each centroid(geom[1]), if (type=="xpath") 0]
3298 )
3299 : let(points = type=="vnf"?geom[1][0]:flatten(force_region(geom[1])))
3300 cp=="mean" ? [each mean(points), if (type=="xpath") 0]
3301 : cp=="box" ?[each mean(pointlist_bounds(points)), if (type=="xpath") 0]
3302 : assert(false,"Invalid cp specification");
3303
3304
3305function _get_cp(geom) =
3306 let(cp=select(geom,-3))
3307 is_vector(cp) ? cp
3308 : let(
3309 is_vnf = in_list(geom[0],["vnf_extent","vnf_isect"])
3310 )
3311 cp == "centroid" ? (
3312 is_vnf && len(geom[1][1])==0
3313 ? [0,0,0]
3314 : centroid(geom[1])
3315 )
3316 : let(points = is_vnf?geom[1][0]:flatten(force_region(geom[1])))
3317 cp=="mean" ? mean(points)
3318 : cp=="box" ? mean(pointlist_bounds(points))
3319 : assert(false,"Invalid cp specification");
3320
3321
3322
3323function _force_anchor_2d(anchor) =
3324 assert(anchor.y==0 || anchor.z==0, "Anchor for a 2D shape cannot be fully 3D. It must have either Y or Z component equal to zero.")
3325 anchor.y==0 ? [anchor.x,anchor.z] : point2d(anchor);
3326
3327
3328/// Internal Function: _find_anchor()
3329/// Usage:
3330/// anchorinfo = _find_anchor(anchor, geom);
3331/// Topics: Attachments
3332/// See Also: reorient(), attachable()
3333/// Description:
3334/// Calculates the anchor data for the given `anchor` vector or name, in the given attachment
3335/// geometry. Returns `[ANCHOR, POS, VEC, ANG]` where `ANCHOR` is the requested anchorname
3336/// or vector, `POS` is the anchor position, `VEC` is the direction vector of the anchor, and
3337/// `ANG` is the angle to align with around the rotation axis of th anchor direction vector.
3338/// Arguments:
3339/// anchor = Vector or named anchor string.
3340/// geom = The geometry description of the shape.
3341function _find_anchor(anchor, geom) =
3342 is_string(anchor)? (
3343 anchor=="origin"? [anchor, CENTER, UP, 0]
3344 : let(
3345 anchors = last(geom),
3346 found = search([anchor], anchors, num_returns_per_match=1)[0]
3347 )
3348 assert(found!=[], str("Unknown anchor: ",anchor))
3349 anchors[found]
3350 ) :
3351 let(
3352 cp = _get_cp(geom),
3353 offset_raw = select(geom,-2),
3354 offset = [for (i=[0:2]) anchor[i]==0? 0 : offset_raw[i]], // prevents bad centering.
3355 type = geom[0]
3356 )
3357 assert(is_vector(anchor),str("Invalid anchor: anchor=",anchor))
3358 let(
3359 anchor = point3d(anchor),
3360 oang = (
3361 approx(point2d(anchor), [0,0])? 0 :
3362 atan2(anchor.y, anchor.x)+90
3363 )
3364 )
3365 type == "prismoid"? ( //size, size2, shift, axis
3366 let(all_comps_good = [for (c=anchor) if (c!=sign(c)) 1]==[])
3367 assert(all_comps_good, "All components of an anchor for a cuboid/prismoid must be -1, 0, or 1")
3368 let(
3369 size=geom[1], size2=geom[2],
3370 shift=point2d(geom[3]), axis=point3d(geom[4]),
3371 override = geom[5](anchor)
3372 )
3373 let(
3374 size = [for (c = size) max(0,c)],
3375 size2 = [for (c = size2) max(0,c)],
3376 anch = rot(from=axis, to=UP, p=anchor),
3377 offset = rot(from=axis, to=UP, p=offset),
3378 h = size.z,
3379 u = (anch.z + 1) / 2, // u is one of 0, 0.5, or 1
3380 axy = point2d(anch),
3381 bot = point3d(v_mul(point2d(size )/2, axy), -h/2),
3382 top = point3d(v_mul(point2d(size2)/2, axy) + shift, h/2),
3383 pos = point3d(cp) + lerp(bot,top,u) + offset,
3384 vecs = anchor==CENTER? [UP]
3385 : [
3386 if (anch.x!=0) unit(rot(from=UP, to=[(top-bot).x,0,max(0.01,h)], p=[axy.x,0,0]), UP),
3387 if (anch.y!=0) unit(rot(from=UP, to=[0,(top-bot).y,max(0.01,h)], p=[0,axy.y,0]), UP),
3388 if (anch.z!=0) unit([0,0,anch.z],UP)
3389 ],
3390 vec2 = anchor==CENTER? UP
3391 : len(vecs)==1? unit(vecs[0],UP)
3392 : len(vecs)==2? vector_bisect(vecs[0],vecs[1])
3393 : let(
3394 v1 = vector_bisect(vecs[0],vecs[2]),
3395 v2 = vector_bisect(vecs[1],vecs[2]),
3396 p1 = plane_from_normal(yrot(90,p=v1)),
3397 p2 = plane_from_normal(xrot(-90,p=v2)),
3398 line = plane_intersection(p1,p2),
3399 v3 = unit(line[1]-line[0],UP) * anch.z
3400 )
3401 unit(v3,UP),
3402 vec = default(override[1],rot(from=UP, to=axis, p=vec2)),
3403 pos2 = default(override[0],rot(from=UP, to=axis, p=pos))
3404 ) [anchor, pos2, vec, default(override[2],oang)]
3405 ) : type == "conoid"? ( //r1, r2, l, shift
3406 assert(anchor.z == sign(anchor.z), "The Z component of an anchor for a cylinder/cone must be -1, 0, or 1")
3407 let(
3408 rr1=geom[1], rr2=geom[2], l=geom[3],
3409 shift=point2d(geom[4]), axis=point3d(geom[5]),
3410 r1 = is_num(rr1)? [rr1,rr1] : point2d(rr1),
3411 r2 = is_num(rr2)? [rr2,rr2] : point2d(rr2),
3412 anch = rot(from=axis, to=UP, p=anchor),
3413 offset = rot(from=axis, to=UP, p=offset),
3414 u = (anch.z+1)/2,
3415 axy = unit(point2d(anch),[0,0]),
3416 bot = point3d(v_mul(r1,axy), -l/2),
3417 top = point3d(v_mul(r2,axy)+shift, l/2),
3418 pos = point3d(cp) + lerp(bot,top,u) + offset,
3419 sidevec = rot(from=UP, to=top==bot?UP:top-bot, p=point3d(axy)),
3420 vvec = anch==CENTER? UP : unit([0,0,anch.z],UP),
3421 vec = anch==CENTER? CENTER :
3422 approx(axy,[0,0])? unit(anch,UP) :
3423 approx(anch.z,0)? sidevec :
3424 unit((sidevec+vvec)/2,UP),
3425 pos2 = rot(from=UP, to=axis, p=pos),
3426 vec2 = anch==CENTER? UP : rot(from=UP, to=axis, p=vec)
3427 ) [anchor, pos2, vec2, oang]
3428 ) : type == "point"? (
3429 let(
3430 anchor = unit(point3d(anchor),CENTER),
3431 pos = point3d(cp) + point3d(offset),
3432 vec = unit(anchor,UP)
3433 ) [anchor, pos, vec, oang]
3434 ) : type == "spheroid"? ( //r
3435 let(
3436 rr = geom[1],
3437 r = is_num(rr)? [rr,rr,rr] : point3d(rr),
3438 anchor = unit(point3d(anchor),CENTER),
3439 pos = point3d(cp) + v_mul(r,anchor) + point3d(offset),
3440 vec = unit(v_mul(r,anchor),UP)
3441 ) [anchor, pos, vec, oang]
3442 ) : type == "vnf_isect"? ( //vnf
3443 let( vnf=geom[1] )
3444 approx(anchor,CTR)? [anchor, cp, UP, 0] : // CENTER anchors anchor on cp, "origin" anchors on [0,0]
3445 vnf==EMPTY_VNF? [anchor, [0,0,0], unit(anchor), 0] :
3446 let(
3447 eps = 1/2048,
3448 points = vnf[0],
3449 faces = vnf[1],
3450 rpts = apply(rot(from=anchor, to=RIGHT) * move(-cp), points),
3451 hits = [
3452 for (face = faces)
3453 let(
3454 verts = select(rpts, face),
3455 ys = column(verts,1),
3456 zs = column(verts,2)
3457 )
3458 if (max(ys) >= -eps && max(zs) >= -eps &&
3459 min(ys) <= eps && min(zs) <= eps)
3460 let(
3461 poly = select(points, face),
3462 isect = polygon_line_intersection(poly, [cp,cp+anchor], eps=eps),
3463 ptlist = is_undef(isect) ? [] :
3464 is_vector(isect) ? [isect]
3465 : flatten(isect), // parallel to a face
3466 n = len(ptlist)>0 ? polygon_normal(poly) : undef
3467 )
3468 for(pt=ptlist) [anchor * (pt-cp), n, pt]
3469 ]
3470 )
3471 assert(len(hits)>0, "Anchor vector does not intersect with the shape. Attachment failed.")
3472 let(
3473 furthest = max_index(column(hits,0)),
3474 dist = hits[furthest][0],
3475 pos = hits[furthest][2],
3476 hitnorms = [for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]],
3477 unorms = [
3478 for (i = idx(hitnorms))
3479 let(
3480 thisnorm = hitnorms[i],
3481 isdup = [
3482 for (j = [i+1:1:len(hitnorms)-1])
3483 if (approx(thisnorm, hitnorms[j])) 1
3484 ] != []
3485 )
3486 if (!isdup) thisnorm
3487 ],
3488 n = unit(sum(unorms)),
3489 oang = approx(point2d(n), [0,0])? 0 : atan2(n.y, n.x) + 90
3490 )
3491 [anchor, pos, n, oang]
3492 ) : type == "vnf_extent"? ( //vnf
3493 let( vnf=geom[1] )
3494 approx(anchor,CTR)? [anchor, cp, UP, 0] : // CENTER anchors anchor on cp, "origin" anchors on [0,0]
3495 vnf==EMPTY_VNF? [anchor, [0,0,0], unit(anchor,UP), 0] :
3496 let(
3497 rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), vnf[0]),
3498 maxx = max(column(rpts,0)),
3499 idxs = [for (i = idx(rpts)) if (approx(rpts[i].x, maxx)) i],
3500 avep = sum(select(rpts,idxs))/len(idxs),
3501 mpt = approx(point2d(anchor),[0,0])? [maxx,0,0] : avep,
3502 pos = point3d(cp) + rot(from=RIGHT, to=anchor, p=mpt)
3503 ) [anchor, pos, anchor, oang]
3504 ) : type == "trapezoid"? ( //size, size2, shift, override
3505 let(all_comps_good = [for (c=anchor) if (c!=sign(c)) 1]==[])
3506 assert(all_comps_good, "All components of an anchor for a rectangle/trapezoid must be -1, 0, or 1")
3507 let(
3508 anchor=_force_anchor_2d(anchor),
3509 size=geom[1], size2=geom[2], shift=geom[3],
3510 u = (anchor.y+1)/2, // 0<=u<=1
3511 frpt = [size.x/2*anchor.x, -size.y/2],
3512 bkpt = [size2/2*anchor.x+shift, size.y/2],
3513 override = geom[4](anchor),
3514 pos = override[0] != undef? override[0] :
3515 point2d(cp) + lerp(frpt, bkpt, u) + point2d(offset),
3516 svec = approx(bkpt,frpt)? [anchor.x,0,0] :
3517 point3d(line_normal(bkpt,frpt)*anchor.x),
3518 vec = is_def(override[1]) ? override[1]
3519 : anchor.y == 0? ( anchor.x == 0? BACK : svec )
3520 : anchor.x == 0? [0,anchor.y,0]
3521 : unit((svec + [0,anchor.y,0]) / 2, [0,anchor.y,0])
3522 ) [anchor, pos, vec, 0]
3523 ) : type == "ellipse"? ( //r
3524 let(
3525 anchor = unit(_force_anchor_2d(anchor),[0,0]),
3526 r = force_list(geom[1],2),
3527 pos = approx(anchor.x,0)
3528 ? [0,sign(anchor.y)*r.y]
3529 : let(
3530 m = anchor.y/anchor.x,
3531 px = approx(min(r),0)? 0 :
3532 sign(anchor.x) * sqrt(1/(1/sqr(r.x) + m*m/sqr(r.y)))
3533 )
3534 [px,m*px],
3535 vec = approx(min(r),0)? (approx(norm(anchor),0)? BACK : anchor) :
3536 unit([r.y/r.x*pos.x, r.x/r.y*pos.y],BACK)
3537 ) [anchor, point2d(cp+offset)+pos, vec, 0]
3538 ) : type == "rgn_isect"? ( //region
3539 let(
3540 anchor = _force_anchor_2d(anchor),
3541 rgn = force_region(move(-point2d(cp), p=geom[1]))
3542 )
3543 approx(anchor,[0,0])? [anchor, cp, BACK, 0] : // CENTER anchors anchor on cp, "origin" anchors on [0,0]
3544 let(
3545 isects = [
3546 for (path=rgn, t=triplet(path,true)) let(
3547 seg1 = [t[0],t[1]],
3548 seg2 = [t[1],t[2]],
3549 isect = line_intersection([[0,0],anchor], seg1, RAY, SEGMENT),
3550 n = is_undef(isect)? [0,1] :
3551 !approx(isect, t[1])? line_normal(seg1) :
3552 unit((line_normal(seg1)+line_normal(seg2))/2,[0,1]),
3553 n2 = vector_angle(anchor,n)>90? -n : n
3554 )
3555 if(!is_undef(isect) && !approx(isect,t[0])) [norm(isect), isect, n2]
3556 ]
3557 )
3558 assert(len(isects)>0, "Anchor vector does not intersect with the shape. Attachment failed.")
3559 let(
3560 maxidx = max_index(column(isects,0)),
3561 isect = isects[maxidx],
3562 pos = point2d(cp) + isect[1],
3563 vec = unit(isect[2],[0,1])
3564 ) [anchor, pos, vec, 0]
3565 ) : type == "rgn_extent"? ( //region
3566 let( anchor = _force_anchor_2d(anchor) )
3567 approx(anchor,[0,0])? [anchor, cp, BACK, 0] : // CENTER anchors anchor on cp, "origin" anchors on [0,0]
3568 let(
3569 rgn = force_region(geom[1]),
3570 rpts = rot(from=anchor, to=RIGHT, p=flatten(rgn)),
3571 maxx = max(column(rpts,0)),
3572 ys = [for (pt=rpts) if (approx(pt.x, maxx)) pt.y],
3573 midy = (min(ys)+max(ys))/2,
3574 pos = rot(from=RIGHT, to=anchor, p=[maxx,midy])
3575 ) [anchor, pos, unit(anchor,BACK), 0]
3576 ) : type=="extrusion_extent" || type=="extrusion_isect" ? ( // extruded region
3577 assert(in_list(anchor.z,[-1,0,1]), "The Z component of an anchor for an extruded 2D shape must be -1, 0, or 1.")
3578 let(
3579 anchor_xy = point2d(anchor),
3580 rgn = geom[1],
3581 L = geom[2],
3582 twist = geom[3],
3583 scale = geom[4],
3584 shift = geom[5],
3585 u = (anchor.z + 1) / 2,
3586 shmat = move(lerp([0,0], shift, u)),
3587 scmat = scale(lerp([1,1], scale, u)),
3588 twmat = zrot(lerp(0, -twist, u)),
3589 mat = shmat * scmat * twmat
3590 )
3591 approx(anchor_xy,[0,0]) ? [anchor, apply(mat, point3d(cp,anchor.z*L/2)), unit(anchor, UP), oang] :
3592 let(
3593 newrgn = apply(mat, rgn),
3594 newgeom = attach_geom(two_d=true, region=newrgn, extent=type=="extrusion_extent", cp=cp),
3595 topmat = anchor.z!=0 ? []
3596 : move(shift)*scale(scale)*zrot(-twist),
3597 topgeom = anchor.z!=0? []
3598 : attach_geom(two_d=true, region=apply(topmat,rgn), extent=type=="extrusion_extent", cp=cp),
3599 top2d = anchor.z!=0? []
3600 : _find_anchor(anchor_xy, topgeom),
3601 result2d = _find_anchor(anchor_xy, newgeom),
3602 pos = point3d(result2d[1], anchor.z*L/2),
3603 vec = anchor.z==0? rot(from=UP,to=point3d(top2d[1],L/2)-point3d(result2d[1]),p=point3d(result2d[2]))
3604 : unit(point3d(result2d[2], anchor.z),UP),
3605 oang = atan2(vec.y,vec.x) + 90
3606 )
3607 [anchor, pos, vec, oang]
3608 ) :
3609 assert(false, "Unknown attachment geometry type.");
3610
3611
3612/// Internal Function: _is_shown()
3613/// Usage:
3614/// bool = _is_shown();
3615/// Topics: Attachments
3616/// See Also: reorient(), attachable()
3617/// Description:
3618/// Returns true if objects should currently be shown based on the tag settings.
3619function _is_shown() =
3620 assert(is_list($tags_shown) || $tags_shown=="ALL")
3621 assert(is_list($tags_hidden))
3622 let(
3623 dummy=is_undef($tags) ? 0 : echo("Use tag() instead of $tags for specifying an object's tag."),
3624 $tag = default($tag,$tags)
3625 )
3626 assert(is_string($tag), str("Tag value (",$tag,") is not a string"))
3627 assert(undef==str_find($tag," "),str("Tag string \"",$tag,"\" contains a space, which is not allowed"))
3628 let(
3629 shown = $tags_shown=="ALL" || in_list($tag,$tags_shown),
3630 hidden = in_list($tag, $tags_hidden)
3631 )
3632 shown && !hidden;
3633
3634
3635// Section: Visualizing Anchors
3636
3637/// Internal Function: _standard_anchors()
3638/// Usage:
3639/// anchs = _standard_anchors([two_d]);
3640/// Description:
3641/// Return the vectors for all standard anchors.
3642/// Arguments:
3643/// two_d = If true, returns only the anchors where the Z component is 0. Default: false
3644function _standard_anchors(two_d=false) = [
3645 for (
3646 zv = [
3647 if (!two_d) TOP,
3648 CENTER,
3649 if (!two_d) BOTTOM
3650 ],
3651 yv = [FRONT, CENTER, BACK],
3652 xv = [LEFT, CENTER, RIGHT]
3653 ) xv+yv+zv
3654];
3655
3656
3657
3658// Module: show_anchors()
3659// Synopsis: Shows anchors for the parent object.
3660// SynTags: Geom
3661// Topics: Attachments
3662// See Also: expose_anchors(), anchor_arrow(), anchor_arrow2d(), frame_ref()
3663// Usage:
3664// PARENT() show_anchors([s], [std=], [custom=]);
3665// Description:
3666// Show all standard anchors for the parent object.
3667// Arguments:
3668// s = Length of anchor arrows.
3669// ---
3670// std = If true show standard anchors. Default: true
3671// custom = If true show named anchors. Default: true
3672// Example(FlatSpin,VPD=333):
3673// cube(50, center=true) show_anchors();
3674module show_anchors(s=10, std=true, custom=true) {
3675 check = assert($parent_geom != undef);
3676 two_d = _attach_geom_2d($parent_geom);
3677 if (std) {
3678 for (anchor=_standard_anchors(two_d=two_d)) {
3679 if(two_d) {
3680 attach(anchor) anchor_arrow2d(s);
3681 } else {
3682 attach(anchor) anchor_arrow(s);
3683 }
3684 }
3685 }
3686 if (custom) {
3687 for (anchor=last($parent_geom)) {
3688 attach(anchor[0]) {
3689 if(two_d) {
3690 anchor_arrow2d(s, color="cyan");
3691 } else {
3692 anchor_arrow(s, color="cyan");
3693 }
3694 color("black")
3695 tag("anchor-arrow") {
3696 xrot(two_d? 0 : 90) {
3697 back(s/3) {
3698 yrot_copies(n=2)
3699 up(two_d? 0.51 : s/30) {
3700 linear_extrude(height=0.01, convexity=12, center=true) {
3701 text(text=anchor[0], size=s/4, halign="center", valign="center", font="Helvetica", $fn=36);
3702 }
3703 }
3704 }
3705 }
3706 }
3707 color([1, 1, 1, 1])
3708 tag("anchor-arrow") {
3709 xrot(two_d? 0 : 90) {
3710 back(s/3) {
3711 cube([s/4.5*len(anchor[0]), s/3, 0.01], center=true);
3712 }
3713 }
3714 }
3715 }
3716 }
3717 }
3718 children();
3719}
3720
3721
3722// Module: anchor_arrow()
3723// Synopsis: Shows a 3d anchor orientation arrow.
3724// SynTags: Geom
3725// Topics: Attachments
3726// See Also: anchor_arrow2d(), show_anchors(), expose_anchors(), frame_ref(), generic_airplane()
3727// Usage:
3728// anchor_arrow([s], [color], [flag], [anchor=], [orient=], [spin=]) [ATTACHMENTS];
3729// Description:
3730// Show an anchor orientation arrow. By default, tagged with the name "anchor-arrow".
3731// Arguments:
3732// s = Length of the arrows. Default: `10`
3733// color = Color of the arrow. Default: `[0.333, 0.333, 1]`
3734// flag = If true, draw the orientation flag on the arrowhead. Default: true
3735// ---
3736// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
3737// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
3738// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
3739// Example:
3740// anchor_arrow(s=20);
3741module anchor_arrow(s=10, color=[0.333,0.333,1], flag=true, $tag="anchor-arrow", $fn=12, anchor=BOT, spin=0, orient=UP) {
3742 attachable(anchor,spin,orient, r=s/6, l=s) {
3743 down(s/2)
3744 recolor("gray") spheroid(d=s/6) {
3745 attach(CENTER,BOT) recolor(color) cyl(h=s*2/3, d=s/15) {
3746 attach(TOP,BOT) cyl(h=s/3, d1=s/5, d2=0) {
3747 if(flag) {
3748 position(BOT)
3749 recolor([1,0.5,0.5])
3750 cuboid([s/100, s/6, s/4], anchor=FRONT+BOT);
3751 }
3752 }
3753 }
3754 }
3755 children();
3756 }
3757}
3758
3759
3760
3761// Module: anchor_arrow2d()
3762// Synopsis: Shows a 2d anchor orientation arrow.
3763// SynTags: Geom
3764// Topics: Attachments
3765// See Also: anchor_arrow(), show_anchors(), expose_anchors(), frame_ref()
3766// Usage:
3767// anchor_arrow2d([s], [color]);
3768// Description:
3769// Show an anchor orientation arrow.
3770// Arguments:
3771// s = Length of the arrows.
3772// color = Color of the arrow.
3773// Example:
3774// anchor_arrow2d(s=20);
3775module anchor_arrow2d(s=15, color=[0.333,0.333,1], $tag="anchor-arrow") {
3776 color(color) stroke([[0,0],[0,s]], width=s/10, endcap1="butt", endcap2="arrow2");
3777}
3778
3779
3780
3781// Module: expose_anchors()
3782// Synopsis: Used to show a transparent object with solid color anchor arrows.
3783// Topics: Attachments
3784// See Also: anchor_arrow2d(), show_anchors(), show_anchors(), frame_ref()
3785// Usage:
3786// expose_anchors(opacity) {child1() show_anchors(); child2() show_anchors(); ...}
3787// Description:
3788// Used in combination with show_anchors() to display an object in transparent gray with its anchors in solid color.
3789// Children will appear transparent and any anchor arrows drawn with will appear in solid color.
3790// Arguments:
3791// opacity = The opacity of the children. 0.0 is invisible, 1.0 is opaque. Default: 0.2
3792// Example(FlatSpin,VPD=333):
3793// expose_anchors() cube(50, center=true) show_anchors();
3794module expose_anchors(opacity=0.2) {
3795 show_only("anchor-arrow")
3796 children();
3797 hide("anchor-arrow")
3798 color(is_undef($color) || $color=="default" ? [0,0,0] :
3799 is_string($color) ? $color
3800 : point3d($color),
3801 opacity)
3802 children();
3803}
3804
3805
3806
3807// Module: show_transform_list()
3808// Synopsis: Shows a list of transforms and how they connect.
3809// SynTags: Geom
3810// Topics: Attachments
3811// See Also: generic_airplane(), anchor_arrow(), show_anchors(), expose_anchors(), frame_ref()
3812// Usage:
3813// show_transform_list(tlist, [s]);
3814// show_transform_list(tlist) {CHILDREN};
3815// Description:
3816// Given a list of transformation matrices, shows the position and orientation of each one.
3817// A line is drawn from each transform position to the next one, and an orientation indicator is
3818// shown at each position. If a child is passed, that child will be used as the orientation indicator.
3819// By default, a {{generic_airplane()}} is used as the orientation indicator.
3820// Arguments:
3821// s = Length of the {{generic_airplane()}}. Default: 5
3822// Example:
3823// tlist = [
3824// zrot(90),
3825// zrot(90) * fwd(30) * zrot(30),
3826// zrot(90) * fwd(30) * zrot(30) *
3827// fwd(35) * xrot(-30),
3828// zrot(90) * fwd(30) * zrot(30) *
3829// fwd(35) * xrot(-30) * fwd(40) * yrot(15),
3830// ];
3831// show_transform_list(tlist, s=20);
3832// Example:
3833// tlist = [
3834// zrot(90),
3835// zrot(90) * fwd(30) * zrot(30),
3836// zrot(90) * fwd(30) * zrot(30) *
3837// fwd(35) * xrot(-30),
3838// zrot(90) * fwd(30) * zrot(30) *
3839// fwd(35) * xrot(-30) * fwd(40) * yrot(15),
3840// ];
3841// show_transform_list(tlist) frame_ref();
3842module show_transform_list(tlist, s=5) {
3843 path = [for (m = tlist) apply(m, [0,0,0])];
3844 stroke(path, width=s*0.03);
3845 for (m = tlist) {
3846 multmatrix(m) {
3847 if ($children>0) children();
3848 else generic_airplane(s=s);
3849 }
3850 }
3851}
3852
3853
3854// Module: generic_airplane()
3855// Synopsis: Shows a generic airplane shape, useful for viewing orientations.
3856// SynTags: Geom
3857// Topics: Attachments
3858// See Also: anchor_arrow(), show_anchors(), expose_anchors(), frame_ref()
3859// Usage:
3860// generic_airplane([s]);
3861// Description:
3862// Creates a generic airplane shape. This can be useful for viewing the orientation of 3D transforms.
3863// Arguments:
3864// s = Length of the airplane. Default: 5
3865// Example:
3866// generic_airplane(s=20);
3867module generic_airplane(s=5) {
3868 $fn = max(segs(0.05*s), 12);
3869 color("#ddd")
3870 fwd(s*0.05)
3871 ycyl(l=0.7*s, d=0.1*s) {
3872 attach(FWD) top_half(s=s) zscale(2) sphere(d=0.1*s);
3873 attach(BACK,FWD) ycyl(l=0.2*s, d1=0.1*s, d2=0.05*s) {
3874 yrot_copies([-90,0,90])
3875 prismoid(s*[0.01,0.2], s*[0.01,0.05],
3876 h=0.2*s, shift=s*[0,0.15], anchor=BOT);
3877 }
3878 yrot_copies([-90,90])
3879 prismoid(s*[0.01,0.2], s*[0.01,0.05],
3880 h=0.5*s, shift=s*[0,0.15], anchor=BOT);
3881 }
3882 color("#777") zcopies(0.1*s) sphere(d=0.02*s);
3883 back(0.09*s) {
3884 color("#f00") right(0.46*s) sphere(d=0.04*s);
3885 color("#0f0") left(0.46*s) sphere(d=0.04*s);
3886 }
3887}
3888
3889
3890
3891// Module: frame_ref()
3892// Synopsis: Shows axis orientation arrows.
3893// SynTags: Geom
3894// Topics: Attachments
3895// See Also: anchor_arrow(), anchor_arrow2d(), show_anchors(), expose_anchors()
3896// Usage:
3897// frame_ref(s, opacity);
3898// Description:
3899// Displays X,Y,Z axis arrows in red, green, and blue respectively.
3900// Arguments:
3901// s = Length of the arrows.
3902// opacity = The opacity of the arrows. 0.0 is invisible, 1.0 is opaque. Default: 1.0
3903// Examples:
3904// frame_ref(25);
3905// frame_ref(30, opacity=0.5);
3906module frame_ref(s=15, opacity=1) {
3907 cube(0.01, center=true) {
3908 attach([1,0,0]) anchor_arrow(s=s, flag=false, color=[1.0, 0.3, 0.3, opacity]);
3909 attach([0,1,0]) anchor_arrow(s=s, flag=false, color=[0.3, 1.0, 0.3, opacity]);
3910 attach([0,0,1]) anchor_arrow(s=s, flag=false, color=[0.3, 0.3, 1.0, opacity]);
3911 children();
3912 }
3913}
3914
3915
3916////////////////////////////////////////////////////////////////////////////////////////////////////
3917////////////////////////////////////////////////////////////////////////////////////////////////////
3918////////////////////////////////////////////////////////////////////////////////////////////////////
3919////////////////////////////////////////////////////////////////////////////////////////////////////
3920///
3921/// Code after this is internal code for managing edge and corner sets and for displaying
3922/// edge and corners in the docs
3923///
3924
3925module _edges_text3d(txt,size=3) {
3926 if (is_list(txt)) {
3927 for (i=idx(txt)) {
3928 down((i-len(txt)/2+0.5)*size*1.5) {
3929 _edges_text3d(txt[i], size=size);
3930 }
3931 }
3932 } else {
3933 xrot(90) color("#000")
3934 linear_extrude(height=0.1) {
3935 text(text=txt, size=size, halign="center", valign="center");
3936 }
3937 }
3938}
3939
3940
3941function _edges_vec_txt(x) = is_string(x)? str("\"", x, "\"") :
3942 assert(is_string(x) || is_vector(x,3), str(x))
3943 let(
3944 lst = concat(
3945 x.z>0? ["TOP"] : x.z<0? ["BOT"] : [],
3946 x.y>0? ["BACK"] : x.y<0? ["FWD"] : [],
3947 x.x>0? ["RIGHT"] : x.x<0? ["LEFT"] : []
3948 ),
3949 out = [
3950 for (i = idx(lst))
3951 i>0? str("+",lst[i]) : lst[i]
3952 ]
3953 ) out;
3954
3955
3956function _edges_text(edges) =
3957 is_string(edges) ? [str("\"",edges,"\"")] :
3958 edges==EDGES_NONE ? ["EDGES_NONE"] :
3959 edges==EDGES_ALL ? ["EDGES_ALL"] :
3960 _is_edge_array(edges) ? [""] :
3961 is_vector(edges,3) ? _edges_vec_txt(edges) :
3962 is_list(edges) ? let(
3963 lst = [for (x=edges) each _edges_text(x)],
3964 out = [
3965 for (i=idx(lst))
3966 str(
3967 (i==0? "[" : ""),
3968 lst[i],
3969 (i<len(lst)-1? "," : ""),
3970 (i==len(lst)-1? "]" : "")
3971 )
3972 ]
3973 ) out :
3974 [""];
3975
3976
3977
3978/// Internal Constant: EDGES_NONE
3979/// Topics: Edges
3980/// See Also: EDGES_ALL, edges()
3981/// Description:
3982/// The set of no edges.
3983/// Figure(3D):
3984/// _show_edges(edges="NONE");
3985EDGES_NONE = [[0,0,0,0], [0,0,0,0], [0,0,0,0]];
3986
3987
3988/// Internal Constant: EDGES_ALL
3989/// Topics: Edges
3990/// See Also: EDGES_NONE, edges()
3991/// Description:
3992/// The set of all edges.
3993/// Figure(3D):
3994/// _show_edges(edges="ALL");
3995EDGES_ALL = [[1,1,1,1], [1,1,1,1], [1,1,1,1]];
3996
3997
3998/// Internal Constant: EDGES_OFFSETS
3999/// Topics: Edges
4000/// See Also: EDGES_NONE, EDGES_ALL, edges()
4001/// Description:
4002/// The vectors pointing to the center of each edge of a unit sized cube.
4003/// Each item in an edge array will have a corresponding vector in this array.
4004EDGE_OFFSETS = [
4005 [
4006 [ 0,-1,-1],
4007 [ 0, 1,-1],
4008 [ 0,-1, 1],
4009 [ 0, 1, 1]
4010 ], [
4011 [-1, 0,-1],
4012 [ 1, 0,-1],
4013 [-1, 0, 1],
4014 [ 1, 0, 1]
4015 ], [
4016 [-1,-1, 0],
4017 [ 1,-1, 0],
4018 [-1, 1, 0],
4019 [ 1, 1, 0]
4020 ]
4021];
4022
4023
4024
4025/// Internal Function: _is_edge_array()
4026/// Topics: Edges, Type Checking
4027/// Usage:
4028/// bool = _is_edge_array(x);
4029/// Description:
4030/// Returns true if the given value has the form of an edge array.
4031/// Arguments:
4032/// x = The item to check the type of.
4033/// See Also: edges(), EDGES_NONE, EDGES_ALL
4034function _is_edge_array(x) = is_list(x) && is_vector(x[0]) && len(x)==3 && len(x[0])==4;
4035
4036
4037function _edge_set(v) =
4038 _is_edge_array(v)? v : [
4039 for (ax=[0:2]) [
4040 for (b=[-1,1], a=[-1,1]) let(
4041 v2=[[0,a,b],[a,0,b],[a,b,0]][ax]
4042 ) (
4043 is_string(v)? (
4044 v=="X"? (ax==0) : // Return all X axis aligned edges.
4045 v=="Y"? (ax==1) : // Return all Y axis aligned edges.
4046 v=="Z"? (ax==2) : // Return all Z axis aligned edges.
4047 v=="ALL"? true : // Return all edges.
4048 v=="NONE"? false : // Return no edges.
4049 let(valid_values = ["X", "Y", "Z", "ALL", "NONE"])
4050 assert(
4051 in_list(v, valid_values),
4052 str(v, " must be a vector, edge array, or one of ", valid_values)
4053 ) v
4054 ) :
4055 let(nonz = sum(v_abs(v)))
4056 nonz==2? (v==v2) : // Edge: return matching edge.
4057 let(
4058 matches = num_true([
4059 for (i=[0:2]) v[i] && (v[i]==v2[i])
4060 ])
4061 )
4062 nonz==1? (matches==1) : // Face: return surrounding edges.
4063 (matches==2) // Corner: return touching edges.
4064 )? 1 : 0
4065 ]
4066];
4067
4068
4069/// Internal Function: _normalize_edges()
4070/// Topics: Edges
4071/// Usage:
4072/// edges = _normalize_edges(v);
4073/// Description:
4074/// Normalizes all values in an edge array to be `1`, if it was originally greater than `0`,
4075/// or `0`, if it was originally less than or equal to `0`.
4076/// See Also: edges(), EDGES_NONE, EDGES_ALL
4077function _normalize_edges(v) = [for (ax=v) [for (edge=ax) edge>0? 1 : 0]];
4078
4079
4080
4081
4082/// Internal Function: _edges()
4083/// Topics: Edges
4084/// Usage:
4085/// edgs = _edges(v);
4086/// edgs = _edges(v, except);
4087///
4088/// Description:
4089/// Takes a list of edge set descriptors, and returns a normalized edges array
4090/// that represents all those given edges.
4091/// Arguments:
4092/// v = The edge set to include.
4093/// except = The edge set to specifically exclude, even if they are in `v`.
4094///
4095/// See Also: EDGES_NONE, EDGES_ALL
4096///
4097function _edges(v, except=[]) =
4098 v==[] ? EDGES_NONE :
4099 (is_string(v) || is_vector(v) || _is_edge_array(v))? _edges([v], except=except) :
4100 (is_string(except) || is_vector(except) || _is_edge_array(except))? _edges(v, except=[except]) :
4101 except==[]? _normalize_edges(sum([for (x=v) _edge_set(x)])) :
4102 _normalize_edges(
4103 _normalize_edges(sum([for (x=v) _edge_set(x)])) -
4104 sum([for (x=except) _edge_set(x)])
4105 );
4106
4107
4108/// Internal Module: _show_edges()
4109/// Topics: Edges, Debugging
4110/// Usage:
4111/// _show_edges(edges, [size=], [text=], [txtsize=]);
4112/// Description:
4113/// Draws a semi-transparent cube with the given edges highlighted in red.
4114/// Arguments:
4115/// edges = The edges to highlight.
4116/// size = The scalar size of the cube.
4117/// text = The text to show on the front of the cube.
4118/// txtsize = The size of the text.
4119/// See Also: _edges(), EDGES_NONE, EDGES_ALL
4120/// Example:
4121/// _show_edges(size=30, edges=["X","Y"]);
4122module _show_edges(edges="ALL", size=20, text, txtsize=3,toplabel) {
4123 edge_set = _edges(edges);
4124 text = !is_undef(text) ? text : _edges_text(edges);
4125 color("red") {
4126 for (axis=[0:2], i=[0:3]) {
4127 if (edge_set[axis][i] > 0) {
4128 translate(EDGE_OFFSETS[axis][i]*size/2) {
4129 if (axis==0) xcyl(h=size, d=2);
4130 if (axis==1) ycyl(h=size, d=2);
4131 if (axis==2) zcyl(h=size, d=2);
4132 }
4133 }
4134 }
4135 }
4136 fwd(size/2) _edges_text3d(text, size=txtsize);
4137 color("yellow",0.7) cuboid(size=size);
4138 vpr = [55,0,25];
4139 color("black")
4140 if (is_def(toplabel))
4141 for(h=idx(toplabel)) up(21+6*h)rot(vpr) text3d(select(toplabel,-h-1),size=3.3,h=0.1,orient=UP,anchor=FRONT);
4142}
4143
4144
4145
4146
4147/// Internal Constant: CORNERS_NONE
4148/// Topics: Corners
4149/// Description:
4150/// The set of no corners.
4151/// Figure(3D):
4152/// _show_corners(corners="NONE");
4153/// See Also: CORNERS_ALL, corners()
4154CORNERS_NONE = [0,0,0,0,0,0,0,0]; // No corners.
4155
4156
4157/// Internal Constant: CORNERS_ALL
4158/// Topics: Corners
4159/// Description:
4160/// The set of all corners.
4161/// Figure(3D):
4162/// _show_corners(corners="ALL");
4163/// See Also: CORNERS_NONE, _corners()
4164CORNERS_ALL = [1,1,1,1,1,1,1,1];
4165
4166
4167/// Internal Constant: CORNER_OFFSETS
4168/// Topics: Corners
4169/// Description:
4170/// The vectors pointing to each corner of a unit sized cube.
4171/// Each item in a corner array will have a corresponding vector in this array.
4172/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4173CORNER_OFFSETS = [
4174 [-1,-1,-1], [ 1,-1,-1], [-1, 1,-1], [ 1, 1,-1],
4175 [-1,-1, 1], [ 1,-1, 1], [-1, 1, 1], [ 1, 1, 1]
4176];
4177
4178
4179
4180
4181/// Internal Function: _is_corner_array()
4182/// Topics: Corners, Type Checking
4183/// Usage:
4184/// bool = _is_corner_array(x)
4185/// Description:
4186/// Returns true if the given value has the form of a corner array.
4187/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4188function _is_corner_array(x) = is_vector(x) && len(x)==8 && all([for (xx=x) xx==1||xx==0]);
4189
4190
4191/// Internal Function: _normalize_corners()
4192/// Topics: Corners
4193/// Usage:
4194/// corns = _normalize_corners(v);
4195/// Description:
4196/// Normalizes all values in a corner array to be `1`, if it was originally greater than `0`,
4197/// or `0`, if it was originally less than or equal to `0`.
4198/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4199function _normalize_corners(v) = [for (x=v) x>0? 1 : 0];
4200
4201
4202function _corner_set(v) =
4203 _is_corner_array(v)? v : [
4204 for (i=[0:7]) let(
4205 v2 = CORNER_OFFSETS[i]
4206 ) (
4207 is_string(v)? (
4208 v=="ALL"? true : // Return all corners.
4209 v=="NONE"? false : // Return no corners.
4210 let(valid_values = ["ALL", "NONE"])
4211 assert(
4212 in_list(v, valid_values),
4213 str(v, " must be a vector, corner array, or one of ", valid_values)
4214 ) v
4215 ) :
4216 all([for (i=[0:2]) !v[i] || (v[i]==v2[i])])
4217 )? 1 : 0
4218];
4219
4220
4221/// Function: _corners()
4222/// Topics: Corners
4223/// Usage:
4224/// corns = _corners(v);
4225/// corns = _corners(v, except);
4226/// Description:
4227/// Takes a list of corner set descriptors, and returns a normalized corners array
4228/// that represents all those given corners. If the `except` argument is given
4229/// a list of corner set descriptors, then all those corners will be removed
4230/// from the returned corners array. If either argument only has a single corner
4231/// set descriptor, you do not have to pass it in a list.
4232function _corners(v, except=[]) =
4233 v==[] ? CORNERS_NONE :
4234 (is_string(v) || is_vector(v) || _is_corner_array(v))? _corners([v], except=except) :
4235 (is_string(except) || is_vector(except) || _is_corner_array(except))? _corners(v, except=[except]) :
4236 except==[]? _normalize_corners(sum([for (x=v) _corner_set(x)])) :
4237 let(
4238 a = _normalize_corners(sum([for (x=v) _corner_set(x)])),
4239 b = _normalize_corners(sum([for (x=except) _corner_set(x)]))
4240 ) _normalize_corners(a - b);
4241
4242
4243/// Internal Function: _corner_edges()
4244/// Topics: Corners
4245/// Description:
4246/// Returns [XCOUNT,YCOUNT,ZCOUNT] where each is the count of edges aligned with that
4247/// axis that are in the edge set and touch the given corner.
4248/// Arguments:
4249/// edges = Standard edges array.
4250/// v = Vector pointing to the corner to count edge intersections at.
4251/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4252function _corner_edges(edges, v) =
4253 let(u = (v+[1,1,1])/2) [edges[0][u.y+u.z*2], edges[1][u.x+u.z*2], edges[2][u.x+u.y*2]];
4254
4255
4256/// InternalFunction: _corner_edge_count()
4257/// Topics: Corners
4258/// Description:
4259/// Counts how many given edges intersect at a specific corner.
4260/// Arguments:
4261/// edges = Standard edges array.
4262/// v = Vector pointing to the corner to count edge intersections at.
4263/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4264function _corner_edge_count(edges, v) =
4265 let(u = (v+[1,1,1])/2) edges[0][u.y+u.z*2] + edges[1][u.x+u.z*2] + edges[2][u.x+u.y*2];
4266
4267
4268function _corners_text(corners) =
4269 is_string(corners) ? [str("\"",corners,"\"")] :
4270 corners==CORNERS_NONE ? ["CORNERS_NONE"] :
4271 corners==CORNERS_ALL ? ["CORNERS_ALL"] :
4272 _is_corner_array(corners) ? [""] :
4273 is_vector(corners,3) ? _edges_vec_txt(corners) :
4274 is_list(corners) ? let(
4275 lst = [for (x=corners) each _corners_text(x)],
4276 out = [
4277 for (i=idx(lst))
4278 str(
4279 (i==0? "[" : ""),
4280 lst[i],
4281 (i<len(lst)-1? "," : ""),
4282 (i==len(lst)-1? "]" : "")
4283 )
4284 ]
4285 ) out :
4286 [""];
4287
4288
4289/// Internal Module: _show_corners()
4290/// Topics: Corners, Debugging
4291/// Usage:
4292/// _show_corners(corners, [size=], [text=], [txtsize=]);
4293/// Description:
4294/// Draws a semi-transparent cube with the given corners highlighted in red.
4295/// Arguments:
4296/// corners = The corners to highlight.
4297/// size = The scalar size of the cube.
4298/// text = If given, overrides the text to be shown on the front of the cube.
4299/// txtsize = The size of the text.
4300/// See Also: CORNERS_NONE, CORNERS_ALL, corners()
4301/// Example:
4302/// _show_corners(corners=FWD+RIGHT, size=30);
4303module _show_corners(corners="ALL", size=20, text, txtsize=3,toplabel) {
4304 corner_set = _corners(corners);
4305 text = !is_undef(text) ? text : _corners_text(corners);
4306 for (i=[0:7]) if (corner_set[i]>0)
4307 translate(CORNER_OFFSETS[i]*size/2)
4308 color("red") sphere(d=2, $fn=16);
4309 fwd(size/2) _edges_text3d(text, size=txtsize);
4310 color("yellow",0.7) cuboid(size=size);
4311 vpr = [55,0,25];
4312 color("black")
4313 if (is_def(toplabel))
4314 for(h=idx(toplabel)) up(21+6*h)rot(vpr) text3d(select(toplabel,-h-1),size=3.3,h=.1,orient=UP,anchor=FRONT);
4315}
4316
4317module _show_cube_faces(faces, size=20, toplabel,botlabel) {
4318 color("red")
4319 for(f=faces){
4320 move(f*size/2) rot(from=UP,to=f)
4321 cuboid([size,size,.1]);
4322 }
4323 vpr = [55,0,25];
4324 color("black"){
4325 if (is_def(toplabel))
4326 for(h=idx(toplabel)) up(21+6*h)rot(vpr) text3d(select(toplabel,-h-1),size=3.3,h=.1,orient=UP,anchor=FRONT);
4327 if (is_def(botlabel))
4328 for(h=idx(botlabel)) down(26+6*h)rot(vpr) text3d(botlabel[h],size=3.3,h=.1,orient=UP,anchor=FRONT);
4329 }
4330 color("yellow",0.7) cuboid(size=size);
4331}
4332
4333// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap