From ef5d197014c9fa1e04ae9ffb4811ef738fe6cc40 Mon Sep 17 00:00:00 2001
From: Revar Desmera <revarbat@gmail.com>
Date: Sat, 14 Jan 2023 23:00:55 -0800
Subject: [PATCH 1/6] Added $idx to various attachent modules.

---
 attachments.scad | 384 ++++++++++++++++++++++++++++-------------------
 1 file changed, 230 insertions(+), 154 deletions(-)

diff --git a/attachments.scad b/attachments.scad
index e3a15ca..d7ed7e8 100644
--- a/attachments.scad
+++ b/attachments.scad
@@ -18,7 +18,7 @@ $tags=undef;      // for backward compatibility
 $tag = "";
 $tag_prefix = "";
 $overlap = 0;
-$color = "default";          
+$color = "default";
 $save_color = undef;         // Saved color to revert back for children
 
 $attach_to = undef;
@@ -52,7 +52,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   For describing directions, faces, edges, and corners the library provides a set of shortcuts
 //   all based on combinations of unit direction vectors.  You can use these for anchoring and orienting
 //   attachable objects.  You can also them to specify edge sets for rounding or chamfering cuboids,
-//   or for placing edge, face and corner masks.  
+//   or for placing edge, face and corner masks.
 // Subsection: Anchor
 //   Anchoring is specified with the `anchor` argument in most shape modules.  Specifying `anchor`
 //   when creating an object will translate the object so that the anchor point is at the origin
@@ -62,10 +62,10 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   .
 //   When given as a vector, it points, in a general way, towards the face, edge, or corner of the
 //   object that you want the anchor for, relative to the center of the object.  You can simply
-//   specify a vector like `[0,0,1]` to anchor an object at the Z+ end, but you can also use 
+//   specify a vector like `[0,0,1]` to anchor an object at the Z+ end, but you can also use
 //   directional constants with names like `TOP`, `BOTTOM`, `LEFT`, `RIGHT` and `BACK` that you can add together
-//   to specify anchor points.  See [specifying directions](attachments.scad#subsection-specifying-directions) 
-//   below for the full list of pre-defined directional constants. 
+//   to specify anchor points.  See [specifying directions](attachments.scad#subsection-specifying-directions)
+//   below for the full list of pre-defined directional constants.
 //   .
 //   For example:
 //   - `[0,0,1]` is the same as `TOP` and refers to the center of the top face.
@@ -77,7 +77,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   The direction of a face anchor will be perpendicular to the face, pointing outward.  The direction of a edge anchor
 //   will be the average of the anchor directions of the two faces the edge is between.  The direction
 //   of a corner anchor will be the average of the anchor directions of the three faces the corner is
-//   on. 
+//   on.
 //   .
 //   When the object is cylindrical, conical, or spherical in nature, the anchors will be located
 //   around the surface of the cylinder, cone, or sphere, relative to the center.
@@ -90,7 +90,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   of the shape.  To support this, if you give an anchor in 2D that has anchor.y=0 then the Z component
 //   will be mapped to the Y direction.  This  means you can use TOP and BOTTOM for anchors of 2D objects.
 //   But remember that TOP and BOTTOM are three dimensional vectors and this is a special interpretation
-//   for 2d anchoring.  
+//   for 2d anchoring.
 //   .
 //   Some more complex objects, like screws and stepper motors, have named anchors to refer to places
 //   on the object that are not at one of the standard faces, edges or corners.  For example, stepper
@@ -103,7 +103,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   number of degrees.  If given as a 3D vector, the object will be rotated around each of the X, Y, Z
 //   axes by the number of degrees in each component of the vector.  Spin is always applied after
 //   anchoring, and before orientation.  Since spin is applied after anchoring it is not what
-//   you might think of intuitively as spinning the shape.  To do that, apply `zrot()` to the shape before anchoring.  
+//   you might think of intuitively as spinning the shape.  To do that, apply `zrot()` to the shape before anchoring.
 // Subsection: Orient
 //   Orientation is specified with the `orient` argument in most shape modules.  Specifying `orient`
 //   when creating an object will rotate the object such that the top of the object will be pointed
@@ -115,7 +115,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   corners of cubes.  You can simply specify these direction vectors numerically, but another
 //   option is to use named constants for direction vectors.  These constants define unit vectors
 //   for the six axis directions as shown below.
-// Figure(3D,Big,VPD=6): Named constants for direction vectors.  Some directions have more than one name.  
+// Figure(3D,Big,VPD=6): Named constants for direction vectors.  Some directions have more than one name.
 //   $fn=12;
 //   stroke([[0,0,0],RIGHT], endcap2="arrow2", width=.05);
 //   color("black")right(.05)up(.05)move(RIGHT) text3d("RIGHT",size=.1,h=.01,anchor=LEFT,orient=FRONT);
@@ -143,7 +143,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   up(.12)move(TOP) text3d("TOP",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
 //   move(TOP) text3d("UP",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
 //   }
-// 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.  
+// 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.
 //   $fn=12;
 //   stroke(path2d([[0,0,0],RIGHT]), endcap2="arrow2", width=.05);
 //   color("black")fwd(.22)left(.05)move(RIGHT) text("RIGHT",size=.1,anchor=RIGHT);
@@ -160,34 +160,34 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //       text("FWD",size=.1,anchor=RIGHT);
 //   fwd(.14) text("FORWARD",size=.1,anchor=RIGHT);
 //   fwd(.28) text("(BOTTOM)",size=.1,anchor=RIGHT);
-//   fwd(.14*3) text("(BOT)",size=.1,anchor=RIGHT);   
+//   fwd(.14*3) text("(BOT)",size=.1,anchor=RIGHT);
 //   }
 //   stroke(path2d([[0,0,0],BACK]), endcap2="arrow2", width=.05);
 // Subsection: Specifying Faces
 //   Modules operating on faces accept a list of faces to describe the faces to operate on.  Each
 //   face is given by a vector that points to that face.  Attachments of cuboid objects onto their faces also
-//   work by choosing an attachment face with a single vector in the same manner.  
-// Figure(3D,Big,NoScales,VPD=275): The six faces of the cube.  Some have faces have more than one name.  
+//   work by choosing an attachment face with a single vector in the same manner.
+// Figure(3D,Big,NoScales,VPD=275): The six faces of the cube.  Some have faces have more than one name.
 //   ydistribute(50) {
 //      xdistribute(35){
 //        _show_cube_faces([BACK], botlabel=["BACK"]);
 //        _show_cube_faces([UP],botlabel=["TOP","UP"]);
-//        _show_cube_faces([RIGHT],botlabel=["RIGHT"]);  
+//        _show_cube_faces([RIGHT],botlabel=["RIGHT"]);
 //      }
 //      xdistribute(35){
 //        _show_cube_faces([FRONT],toplabel=["FRONT","FWD", "FORWARD"]);
 //        _show_cube_faces([DOWN],toplabel=["BOTTOM","BOT","DOWN"]);
-//        _show_cube_faces([LEFT],toplabel=["LEFT"]);  
-//      }  
+//        _show_cube_faces([LEFT],toplabel=["LEFT"]);
+//      }
 //   }
 // Subsection: Specifying Edges
 //   Modules operating on edges use two arguments to describe the edge set they will use: The `edges` argument
 //   is a list of edge set descriptors to include in the edge set, and the `except` argument is a list of
 //   edge set descriptors to remove from the edge set.
 //   The default value for `edges` is `"ALL"`, the set of all edges.
-//   The default value for `except` is the    empty set, meaning no edges are removed. 
+//   The default value for `except` is the    empty set, meaning no edges are removed.
 //   If either argument is just a single edge set
-//   descriptor it can be passed directly rather than in a singleton list.  
+//   descriptor it can be passed directly rather than in a singleton list.
 //   Each edge set descriptor must be one of:
 //   - A vector pointing towards an edge, indicating that single edge.
 //   - A vector pointing towards a face, indicating all edges surrounding that face.
@@ -208,7 +208,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   You can specify edge descriptors directly by giving a vector, or you can use sums of the
 //   named direction vectors described above.  Below we show all of the edge sets you can
 //   describe with sums of the direction vectors, and then we show some examples of combining
-//   edge set descriptors.  
+//   edge set descriptors.
 // Figure(3D,Big,VPD=300,NoScales): Vectors pointing toward an edge select that single edge
 //   ydistribute(50) {
 //       xdistribute(30) {
@@ -270,33 +270,33 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //           _show_edges(edges="NONE");
 //       }
 //   }
-// 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 or top faces.  Adding `except` removes an edge.  
+// 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 or top faces.  Adding `except` removes an edge.
 //   xdistribute(43){
 //     _show_edges(_edges([0,-1,1]),toplabel=["edges=[0,-1,1]"]);
 //     _show_edges(_edges(TOP+FRONT),toplabel=["edges=TOP+FRONT"]);
 //     _show_edges(_edges([TOP,FRONT]),toplabel=["edges=[TOP,FRONT]"]);
-//     _show_edges(_edges([TOP,FRONT],TOP+FRONT),toplabel=["edges=[TOP,FRONT]","except=TOP+FRONT"]);      
+//     _show_edges(_edges([TOP,FRONT],TOP+FRONT),toplabel=["edges=[TOP,FRONT]","except=TOP+FRONT"]);
 //   }
-// 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.  
+// 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.
 //   xdistribute(43){
 //     _show_edges(_edges(BOT,BACK), toplabel=["edges=BOT","except=BACK"]);
 //     _show_edges(_edges("Z",BACK), toplabel=["edges=\"Z\"", "except=BACK"]);
 //     _show_edges(_edges("ALL",BACK), toplabel=["(edges=\"ALL\")", "except=BACK"]);
-//     _show_edges(_edges("Y",BACK), toplabel=["edges=\"Y\"","except=BACK"]);   
+//     _show_edges(_edges("Y",BACK), toplabel=["edges=\"Y\"","except=BACK"]);
 //   }
-// 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.  
+// 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.
 //   xdistribute(52){
 //    _show_edges(_edges("ALL",[FRONT+RIGHT,FRONT+LEFT]),
 //               toplabel=["except=[FRONT+RIGHT,","       FRONT+LEFT]"]);
-//    _show_edges(_edges([1,-1,1]),toplabel=["edges=[1,-1,1]"]);             
-//    _show_edges(_edges([TOP,BOT], TOP+RIGHT+FRONT),toplabel=["edges=[TOP,BOT]","except=TOP+RIGHT+FRONT"]); 
-//   }             
+//    _show_edges(_edges([1,-1,1]),toplabel=["edges=[1,-1,1]"]);
+//    _show_edges(_edges([TOP,BOT], TOP+RIGHT+FRONT),toplabel=["edges=[TOP,BOT]","except=TOP+RIGHT+FRONT"]);
+//   }
 // Subsection: Specifying Corners
 //   Modules operating on corners use two arguments to describe the corner set they will use: The `corners` argument
 //   is a list of corner set descriptors to include in the corner set, and the `except` argument is a list of
 //   corner set descriptors to remove from the corner set.
 //   The default value for `corners` is `"ALL"`, the set of all corners.
-//   The default value for `except` is the   empty set, meaning no corners are removed.  
+//   The default value for `except` is the   empty set, meaning no corners are removed.
 //   If either argument is just a single corner set
 //   descriptor it can be passed directly rather than in a singleton list.
 //   Each corner set descriptor must be one of:
@@ -312,7 +312,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   You can specify corner descriptors directly by giving a vector, or you can use sums of the
 //   named direction vectors described above.  Below we show all of the corner sets you can
 //   describe with sums of the direction vectors and then we show some examples of combining
-//   corner set descriptors.  
+//   corner set descriptors.
 // Figure(3D,Big,NoScales,VPD=300): Vectors pointing toward a corner select that corner.
 //   ydistribute(55) {
 //       xdistribute(35) {
@@ -367,7 +367,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //       _show_corners(corners="ALL");
 //       _show_corners(corners="NONE");
 //   }
-// 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.  
+// 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.
 //   xdistribute(52){
 //     _show_corners(_corners([1,-1,-1]),toplabel=["corners=[1,-1,-1]"]);
 //     _show_corners(_corners(BOT+RIGHT+FRONT),toplabel=["corners=BOT+RIGHT+FRONT"]);
@@ -379,7 +379,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //       _show_corners(_corners([FRONT+TOP,BOT+BACK]), toplabel=["corners=[FRONT+TOP,","        BOT+BACK]"]);
 //       _show_corners(_corners("ALL",FRONT+TOP), toplabel=["(corners=\"ALL\")","except=FRONT+TOP"]);
 //    }
-// 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.  
+// 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.
 //    xdistribute(58){
 //       _show_corners(_corners(TOP,[1,1,1]), toplabel=["corners=TOP","except=[1,1,1]"]);
 //       _show_corners(_corners("ALL",[FRONT+RIGHT+TOP,FRONT+LEFT+BOT]),
@@ -392,7 +392,7 @@ _ANCHOR_TYPES = ["intersect","hull"];
 
 // Module: position()
 // Usage:
-//   position(from) CHILDREN;
+//   PARENT() position(from) CHILDREN;
 //
 // Topics: Attachments
 // See Also: attachable(), attach(), orient()
@@ -402,6 +402,10 @@ _ANCHOR_TYPES = ["intersect","hull"];
 //   of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   from = The vector, or name of the parent anchor point to attach to.
+// Side Effects:
+//   `$attach_anchor` for each `from=` anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
+//   `$attach_to` is set to `undef`.
+//   `$attach_norot` is set to `true`.
 // Example:
 //   spheroid(d=20) {
 //       position(TOP) cyl(l=10, d1=10, d2=5, anchor=BOTTOM);
@@ -426,7 +430,7 @@ module position(from)
 // Module: orient()
 // Usage:
 //   orient(dir, [spin=]) CHILDREN;
-//   orient(anchor=, [spin=]) CHILDREN;
+//   PARENT() orient(anchor=, [spin=]) CHILDREN;
 // Topics: Attachments
 // Description:
 //   Orients children such that their top is tilted towards the given direction, or towards the
@@ -437,6 +441,10 @@ module position(from)
 //   ---
 //   anchor = The anchor on the parent which you want to match the orientation of.  Use instead of `dir`.
 //   spin = The spin to add to the children.  (Overrides anchor spin.)
+// Side Effects:
+//   `$attach_anchor` is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for the `anchor=`, if given.
+//   `$attach_to` is set to `undef`.
+//   `$attach_norot` is set to `true`.
 // See Also: attachable(), attach(), orient()
 // Example: Orienting by Vector
 //   prismoid([50,50],[30,30],h=40) {
@@ -466,7 +474,7 @@ module orient(dir, anchor, spin) {
     req_children($children);
     if (!is_undef(dir)) {
         spin = default(spin, 0);
-        check = 
+        check =
           assert(anchor==undef, "Only one of dir= or anchor= may be given to orient()")
           assert(is_vector(dir))
           assert(is_finite(spin));
@@ -494,8 +502,8 @@ module orient(dir, anchor, spin) {
 
 // Module: attach()
 // Usage:
-//   attach(from, [overlap=], [norot=]) CHILDREN;
-//   attach(from, to, [overlap=], [norot=]) CHILDREN;
+//   PARENT() attach(from, [overlap=], [norot=]) CHILDREN;
+//   PARENT() attach(from, to, [overlap=], [norot=]) CHILDREN;
 // Topics: Attachments
 // See Also: attachable(), position(), face_profile(), edge_profile(), corner_profile()
 // Description:
@@ -511,6 +519,11 @@ module orient(dir, anchor, spin) {
 //   ---
 //   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.
 //   norot = If true, don't rotate children when attaching to the anchor point.  Only translate to the anchor point.
+// Side Effects:
+//   `$idx` is set to the index number of each anchor if a list of anchors is given.  Otherwise is set to `0`.
+//   `$attach_anchor` for each `from=` anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
+//   `$attach_to` is set to the value of the `to=` argument, if given.  Otherwise, `undef`
+//   `$attach_norot` is set to the value of the `norot=` argument.
 // Example:
 //   spheroid(d=20) {
 //       attach(TOP) down(1.5) cyl(l=11.5, d1=10, d2=5, anchor=BOTTOM);
@@ -523,7 +536,8 @@ module attach(from, to, overlap, norot=false)
     assert($parent_geom != undef, "No object to attach to!");
     overlap = (overlap!=undef)? overlap : $overlap;
     anchors = (is_vector(from)||is_string(from))? [from] : from;
-    for (anchr = anchors) {
+    for ($idx = idx(anchors)) {
+        anchr = anchors[$idx];
         anch = _find_anchor(anchr, $parent_geom);
         two_d = _attach_geom_2d($parent_geom);
         $attach_to = to;
@@ -543,21 +557,21 @@ module attach(from, to, overlap, norot=false)
 
 // Module: tag()
 // Usage:
-//   tag(tag) CHILDREN;
+//   PARENT() tag(tag) CHILDREN;
 // Topics: Attachments
 // See Also: force_tag(), recolor(), hide(), show_only(), diff(), intersect()
 // Description:
 //   Assigns the specified tag to all of the children. Note that if you want
 //   to apply a tag to non-tag-aware objects you need to use {{force_tag()}} instead.
 //   This works by setting the `$tag` variable, but it provides extra error checking and
-//   handling of scopes.  You may set `$tag` directly yourself, but this is not recommended. 
+//   handling of scopes.  You may set `$tag` directly yourself, but this is not recommended.
 //   .
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   tag = tag string, which must not contain any spaces.
 // Side Effects:
-//   Sets `$tag` to the tag you specify, possibly with a scope prefix. 
-// Example(3D):  Applies the tag to both cuboids instead of having to repeat `$tag="remove"` for each one. 
+//   Sets `$tag` to the tag you specify, possibly with a scope prefix.
+// Example(3D):  Applies the tag to both cuboids instead of having to repeat `$tag="remove"` for each one.
 //   diff("remove")
 //     cuboid(10){
 //       position(TOP) cuboid(3);
@@ -566,7 +580,7 @@ module attach(from, to, overlap, norot=false)
 //         position(FRONT) cuboid(3);
 //         position(RIGHT) cuboid(3);
 //       }
-//     }  
+//     }
 module tag(tag)
 {
     req_children($children);
@@ -580,7 +594,7 @@ module tag(tag)
 
 // Module: force_tag()
 // Usage:
-//   force_tag([tag]) CHILDREN;
+//   PARENT() force_tag([tag]) CHILDREN;
 // Topics: Attachments
 // See Also: tag(), recolor(), hide(), show_only(), diff(), intersect()
 // Description:
@@ -589,7 +603,7 @@ module tag(tag)
 //   making a final determination about whether to show or hide the children.
 //   This means that tagging in children's children will be ignored.
 //   This module is specifically provided for operating on children that are not tag aware such as modules
-//   that don't use {{attachable()}} or built in modules such as 
+//   that don't use {{attachable()}} or built in modules such as
 //   - `polygon()`
 //   - `projection()`
 //   - `polyhedron()`  (or use [`vnf_polyhedron()`](vnf.scad#vnf_polyhedron))
@@ -612,14 +626,14 @@ module tag(tag)
 // Arguments:
 //   tag = tag string, which must not contain any spaces
 // Side Effects:
-//   Sets `$tag` to the tag you specify, possibly with a scope prefix. 
+//   Sets `$tag` to the tag you specify, possibly with a scope prefix.
 // Example(2D): This example produces the full square without subtracting the "remove" item.  When you use non-attachable modules with tags, results are unpredictable.
 //   diff()
 //   {
 //     polygon(square(10));
 //     move(-[.01,.01])polygon(square(5),$tag="remove");
 //   }
-// 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.  
+// 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.
 //   diff()
 //   {
 //     force_tag()
@@ -642,7 +656,7 @@ module force_tag(tag)
 
 // Module: default_tag()
 // Usage:
-//   default_tag(tag) CHILDREN;
+//   PARENT() default_tag(tag) CHILDREN;
 // Topics: Attachments
 // See Also: force_tag(), recolor(), hide(), show_only(), diff(), intersect()
 // Description:
@@ -651,14 +665,14 @@ module force_tag(tag)
 //   The default_tag() module sets the `$tag` variable only if it is not already
 //   set so you can have a module set a default tag of "remove" but that tag can be overridden by a {{tag()}}
 //   in force from a parent.  If you use {{tag()}} it will override any previously
-//   specified tag from a parent, which can be very confusing to a user trying to change the tag on a module.  
+//   specified tag from a parent, which can be very confusing to a user trying to change the tag on a module.
 //   .
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   tag = tag string, which must not contain any spaces.
 // Side Effects:
-//   Sets `$tag` to the tag you specify, possibly with a scope prefix. 
-// Example(3D):  The module thing() is defined with {{tag()}} and the user applied tag of "keep_it" is ignored, leaving the user puzzled.  
+//   Sets `$tag` to the tag you specify, possibly with a scope prefix.
+// Example(3D):  The module thing() is defined with {{tag()}} and the user applied tag of "keep_it" is ignored, leaving the user puzzled.
 //   module thing() { tag("remove") cuboid(10);}
 //   diff()
 //     cuboid(20){
@@ -685,8 +699,10 @@ module default_tag(tag)
 // Description:
 //   Creates a tag scope with locally altered tag names to avoid tag name conflict with other code.
 //   This is necessary when writing modules because the module's caller might happen to use the same tags.
-//   Note that if you directly set the `$tag` variable then tag scoping will not work correctly.  
-// Example: In this example the ring module uses "remove" tags which will conflict with use of the same tags by the parent.  
+//   Note that if you directly set the `$tag` variable then tag scoping will not work correctly.
+// Side Effects:
+//   `$tag_prefix` is set to the value of `scope=` if given, otherwise is set to a random string.
+// Example: In this example the ring module uses "remove" tags which will conflict with use of the same tags by the parent.
 //   module ring(r,h,w=1,anchor,spin,orient)
 //   {
 //     tag_scope("ringscope")
@@ -716,29 +732,29 @@ module tag_scope(scope){
   assert(undef==str_find(scope," "),str("Scope string \"",scope,"\" contains a space, which is not allowed"));
   $tag_prefix=scope;
   children();
-}  
+}
 
 
-// Section: Attachment Modifiers 
+// Section: Attachment Modifiers
 
 // Module: diff()
 // Usage:
-//   diff([remove], [keep]) CHILDREN;
+//   diff([remove], [keep]) PARENT() CHILDREN;
 // Topics: Attachments
 // See Also: tag(), force_tag(), recolor(), show_only(), hide(), tag_diff(), intersect(), tag_intersect()
 // Description:
 //   Perform a differencing operation using tags to control what happens.  This is specifically intended to
 //   address the situation where you want differences between a parent and child object, something
-//   that is impossible with the native difference() module.  
+//   that is impossible with the native difference() module.
 //   The children to diff are grouped into three categories, regardless of nesting level.
 //   The `remove` argument is a space delimited list of tags specifying objects to
 //   subtract.  The `keep` argument is a similar list of tags giving objects to be kept.
 //   Objects not matching either the `remove` or `keep` lists form the third category of base objects.
-//   To produce its output, diff() forms the union of all the base objects and then 
+//   To produce its output, diff() forms the union of all the base objects and then
 //   subtracts all the objects with tags in `remove`.  Finally it adds in objects listed in `keep`.
 //   Attachable objects should be tagged using {{tag()}}
-//   and non-attachable objects with {{force_tag()}}.  
-//   . 
+//   and non-attachable objects with {{force_tag()}}.
+//   .
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   remove = String containing space delimited set of tag names of children to difference away.  Default: `"remove"`
@@ -749,7 +765,7 @@ module tag_scope(scope){
 //       tag("remove") attach(TOP) sphere(d=40);
 //       tag("keep") attach(CTR) cylinder(h=40, d=10);
 //   }
-// Example: The "hole" items are subtracted from everything else.  The other tags can be anything you find convenient.  
+// Example: The "hole" items are subtracted from everything else.  The other tags can be anything you find convenient.
 //   diff("hole")
 //     tag("body")sphere(d=100) {
 //       tag("pole") zcyl(d=55, h=100);  // attach() not needed for center-to-center.
@@ -771,7 +787,7 @@ module tag_scope(scope){
 //       edge_mask(FWD)
 //           rounding_edge_mask(l=max($parent_size)*1.01, r=25);
 //   }
-// 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. 
+// 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.
 //  diff()
 //     tag("remove")cuboid(10)
 //       tag("")position(RIGHT+BACK)cyl(r=8,h=9);
@@ -794,7 +810,7 @@ module tag_scope(scope){
 //     // The orange bar has its center removed
 //     color("orange") down(1) xcyl(h=8, d=1);
 //     // "keep" prevents interior of the blue bar intact
-//     tag("keep") recolor("blue") up(1) xcyl(h=8, d=1);  
+//     tag("keep") recolor("blue") up(1) xcyl(h=8, d=1);
 //   }
 //   // Objects outside the diff don't have pipe interiors removed
 //   color("purple") down(2.2) ycyl(h=8, d=0.3);
@@ -825,7 +841,7 @@ module tag_scope(scope){
 //                      tag("remA")diff("remB")
 //                        right(.2)position(RIGHT)cyl(r=4,h=10,anchor=RIGHT)
 //                          tag("remB")left(.2)position(LEFT)cyl(r=3,h=11,anchor=LEFT);
-// Example(3D,NoAxes,NoScales): When working with Non-Attachables like rotate_extrude() you must apply {{force_tag()}} to every non-attachable object.  
+// Example(3D,NoAxes,NoScales): When working with Non-Attachables like rotate_extrude() you must apply {{force_tag()}} to every non-attachable object.
 //   back_half()
 //     diff("remove")
 //       cuboid(40) {
@@ -839,7 +855,7 @@ module tag_scope(scope){
 //                 right(20)
 //                   circle(5);
 //       }
-// 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.  
+// 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.
 //  $fn=32;
 //  diff()
 //    cuboid(10){
@@ -858,7 +874,7 @@ module tag_scope(scope){
 //         tag("keep")cyl(r=3,h=17)
 //           tag("remove")position(RIGHT)cyl(r=2,h=18);
 //     }
-// 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.  
+// 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.
 //   $fn=32;
 //   intersect("keep","fullkeep")
 //     diff(keep="fullkeep keep")
@@ -890,21 +906,21 @@ module diff(remove="remove", keep="keep")
 {
     req_children($children);
     assert(is_string(remove),"remove must be a string of tags");
-    assert(is_string(keep),"keep must be a string of tags"); 
+    assert(is_string(keep),"keep must be a string of tags");
     if (_is_shown())
-    {      
+    {
         difference() {
             hide(str(remove," ",keep)) children();
             show_only(remove) children();
         }
     }
-    show_int(keep)children();        
+    show_int(keep)children();
 }
 
 
 // Module: tag_diff()
 // Usage:
-//   tag_diff(tag, [remove], [keep]) CHILDREN;
+//   tag_diff(tag, [remove], [keep]) PARENT() CHILDREN;
 // Topics: Attachments
 // See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), intersect(), tag_intersect()
 // Description:
@@ -912,15 +928,15 @@ module diff(remove="remove", keep="keep")
 //   and then tag the resulting difference object with the specified tag.  This forces the specified
 //   tag to be resolved at the level of the difference operation.  In most cases, this is not necessary,
 //   but if you have kept objects and want to operate on this difference object as a whole object using
-//   more tag operations, you will probably not get the results you want if you simply use {{tag()}}.  
-//   . 
+//   more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
+//   .
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   tag = Tag string to apply to this difference object
 //   remove = String containing space delimited set of tag names of children to difference away.  Default: `"remove"`
 //   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"`
 // Side Effects:
-//   Sets `$tag` to the tag you specify, possibly with a scope prefix. 
+//   Sets `$tag` to the tag you specify, possibly with a scope prefix.
 // 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:
 //   diff("rem"){
 //     cuboid([20,10,30],anchor=FRONT);
@@ -939,7 +955,7 @@ module diff(remove="remove", keep="keep")
 //         tag("keep")cuboid([2,2,20]);
 //       }
 //   }
-// 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.  
+// 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.
 //   diff("rem","nothing")
 //     cyl(r=8,h=6)
 //       tag("rem")diff()
@@ -953,14 +969,13 @@ module diff(remove="remove", keep="keep")
 //         cyl(r=7,h=7)
 //           tag("remove")cyl(r=6,h=8)
 //           tag("keep")cyl(r=5,h=9);
-// 
 module tag_diff(tag,remove="remove", keep="keep")
 {
     req_children($children);
     assert(is_string(remove),"remove must be a string of tags");
     assert(is_string(keep),"keep must be a string of tags");
     assert(is_string(tag),"tag must be a string");
-    assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));   
+    assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
     $tag=str($tag_prefix,tag);
     if (_is_shown())
       show_all(){
@@ -968,14 +983,14 @@ module tag_diff(tag,remove="remove", keep="keep")
             hide(str(remove," ",keep)) children();
             show_only(remove) children();
          }
-         show_only(keep)children();        
+         show_only(keep)children();
       }
 }
 
 
 // Module: intersect()
 // Usage:
-//   intersect([intersect], [keep]) CHILDREN;
+//   intersect([intersect], [keep]) PARENT() CHILDREN;
 // Topics: Attachments
 // See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), tag_diff(), tag_intersect()
 // Description:
@@ -990,7 +1005,7 @@ module tag_diff(tag,remove="remove", keep="keep")
 //   between the union of the `intersect` tagged objects and union of the objects that don't
 //   match any of the listed tags.  Finally the objects listed in `keep` are
 //   unioned with the result.  Attachable objects should be tagged using {{tag()}}
-//   and non-attachable objects with {{force_tag()}}.  
+//   and non-attachable objects with {{force_tag()}}.
 //   .
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
@@ -1001,8 +1016,8 @@ module tag_diff(tag,remove="remove", keep="keep")
 //     sphere(d=100) {
 //         tag("mask")cuboid([40,100,100]);
 //         tag("axle")xcyl(d=40, l=100);
-//     } 
-// 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.  
+//     }
+// 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.
 //   $fn=32;
 //   intersect("keep","fullkeep")
 //     diff(keep="fullkeep keep")
@@ -1039,12 +1054,12 @@ module intersect(intersect="intersect",keep="keep")
       hide(str(intersect," ",keep)) children();
    }
    show_int(keep) children();
-}   
+}
 
 
 // Module: tag_intersect()
 // Usage:
-//   tag_intersect(tag, [intersect], [keep]) CHILDREN;
+//   tag_intersect(tag, [intersect], [keep]) PARENT() CHILDREN;
 // Topics: Attachments
 // See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), tag_diff(), intersect()
 // Description:
@@ -1052,7 +1067,7 @@ module intersect(intersect="intersect",keep="keep")
 //   and then tag the resulting difference object with the specified tag.  This forces the specified
 //   tag to be resolved at the level of the intersect operation.  In most cases, this is not necessary,
 //   but if you have kept objects and want to operate on this difference object as a whole object using
-//   more tag operations, you will probably not get the results you want if you simply use {{tag()}}.  
+//   more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
 //   .
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
@@ -1060,8 +1075,8 @@ module intersect(intersect="intersect",keep="keep")
 //   intersect = String containing space delimited set of tag names of children to intersect.  Default: "intersect"
 //   keep = String containing space delimited set of tag names of children to keep whole.  Default: "keep"
 // Side Effects:
-//   Sets `$tag` to the tag you specify, possibly with a scope prefix. 
-// Example:  Without `tag_intersect()` the kept object is not included in the difference.  
+//   Sets `$tag` to the tag you specify, possibly with a scope prefix.
+// Example:  Without `tag_intersect()` the kept object is not included in the difference.
 //   $fn=32;
 //   diff()
 //     cuboid([20,15,9])
@@ -1070,7 +1085,7 @@ module intersect(intersect="intersect",keep="keep")
 //         tag("intersect")position(RIGHT) cyl(r=7,h=10);
 //         tag("keep")position(LEFT)cyl(r=4,h=10);
 //       }
-// Example: Using tag_intersect corrects the problem. 
+// Example: Using tag_intersect corrects the problem.
 //   $fn=32;
 //   diff()
 //     cuboid([20,15,9])
@@ -1078,13 +1093,13 @@ module intersect(intersect="intersect",keep="keep")
 //       cuboid(10){
 //         tag("intersect")position(RIGHT) cyl(r=7,h=10);
 //         tag("keep")position(LEFT)cyl(r=4,h=10);
-//       } 
+//       }
 module tag_intersect(tag,intersect="intersect",keep="keep")
 {
    assert(is_string(intersect),"intersect must be a string of tags");
    assert(is_string(keep),"keep must be a string of tags");
    assert(is_string(tag),"tag must be a string");
-   assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));   
+   assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
    $tag=str($tag_prefix,tag);
    if (_is_shown())
      show_all(){
@@ -1094,7 +1109,7 @@ module tag_intersect(tag,intersect="intersect",keep="keep")
        }
        show_only(keep) children();
    }
-}   
+}
 
 
 // Module: conv_hull()
@@ -1105,15 +1120,15 @@ module tag_intersect(tag,intersect="intersect",keep="keep")
 // Description:
 //   Performs a hull operation on the children using tags to determine what happens.  The items
 //   not tagged with the `keep` tags are combined into a convex hull, and the children tagged with the keep tags
-//   are unioned with the result.  
+//   are unioned with the result.
 //   .
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   keep = String containing space delimited set of tag names of children to keep out of the hull.  Default: "keep"
-// Example:  
+// Example:
 //   conv_hull("keep")
 //      sphere(d=100, $fn=64) {
-//        cuboid([40,90,90]); 
+//        cuboid([40,90,90]);
 //        tag("keep")xcyl(d=40, l=120);
 //      }
 // Example: difference combined with hull where all objects are relative to each other.
@@ -1124,7 +1139,7 @@ module tag_intersect(tag,intersect="intersect",keep="keep")
 //         position(RIGHT+BACK)cyl(r=4,h=10)
 //           tag("remove")cyl(r=2,h=12);
 module conv_hull(keep="keep")
-{  
+{
     req_children($children);
     assert(is_string(keep),"keep must be a string of tags");
     if (_is_shown())
@@ -1143,29 +1158,29 @@ module conv_hull(keep="keep")
 //   and then tag the resulting hull object with the specified tag.  This forces the specified
 //   tag to be resolved at the level of the hull operation.  In most cases, this is not necessary,
 //   but if you have kept objects and want to operate on the hull object as a whole object using
-//   more tag operations, you will probably not get the results you want if you simply use {{tag()}}.  
+//   more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
 //   .
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   keep = String containing space delimited set of tag names of children to keep out of the hull.  Default: "keep"
 // Side Effects:
-//   Sets `$tag` to the tag you specify, possibly with a scope prefix. 
+//   Sets `$tag` to the tag you specify, possibly with a scope prefix.
 // Example: With a regular tag, the kept object is not handled as desired:
 //   diff(){
 //      cuboid([30,30,9])
-//        tag("remove")conv_hull("remove")       
+//        tag("remove")conv_hull("remove")
 //          cuboid(10,anchor=LEFT+FRONT){
 //            position(RIGHT+BACK)cyl(r=4,h=10);
-//            tag("keep")position(FRONT+LEFT)cyl(r=4,h=10);         
+//            tag("keep")position(FRONT+LEFT)cyl(r=4,h=10);
 //          }
 //   }
 // Example: Using `tag_conv_hull()` fixes the problem:
 //   diff(){
 //      cuboid([30,30,9])
-//        tag_conv_hull("remove")       
+//        tag_conv_hull("remove")
 //          cuboid(10,anchor=LEFT+FRONT){
 //            position(RIGHT+BACK)cyl(r=4,h=10);
-//            tag("keep")position(FRONT+LEFT)cyl(r=4,h=10);         
+//            tag("keep")position(FRONT+LEFT)cyl(r=4,h=10);
 //          }
 //   }
 module tag_conv_hull(tag,keep="keep")
@@ -1173,14 +1188,14 @@ module tag_conv_hull(tag,keep="keep")
     req_children($children);
     assert(is_string(keep),"keep must be a string of tags");
     assert(is_string(tag),"tag must be a string");
-    assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));   
+    assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
     $tag=str($tag_prefix,tag);
     if (_is_shown())
       show_all(){
         hull() hide(keep) children();
         show_only(keep) children();
       }
-}    
+}
 
 
 // Module: hide()
@@ -1189,11 +1204,11 @@ module tag_conv_hull(tag,keep="keep")
 // Topics: Attachments
 // See Also: tag(), recolor(), show_only(), diff(), intersect()
 // Description:
-//   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()`.  
+//   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()`.
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Side Effects:
-//   Sets `$tags_hidden` to include the tags you specify. 
-// Example:  Hides part of the model.  
+//   Sets `$tags_hidden` to include the tags you specify.
+// Example:  Hides part of the model.
 //   hide("A")
 //     tag("main") cube(50, anchor=CENTER, $tag="Main") {
 //       tag("A")attach(LEFT, BOTTOM) cylinder(d=30, l=30);
@@ -1206,7 +1221,7 @@ module tag_conv_hull(tag,keep="keep")
 //       tag("visible") {
 //         position(RIGHT) cyl(r=1,h=12);
 //         position(LEFT) cyl(r=1,h=12);
-//       }  
+//       }
 module hide(tags)
 {
     req_children($children);
@@ -1223,13 +1238,13 @@ module hide(tags)
 // Topics: Attachments
 // See Also: tag(), recolor(), hide(), diff(), intersect()
 // Description:
-//   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.  
+//   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.
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Side Effects:
-//   Sets `$tags_shown` to the tag you specify. 
+//   Sets `$tags_shown` to the tag you specify.
 // Example:  Display the attachments but not the parent
 //   show_only("visible")
-//     cube(50, anchor=CENTER) 
+//     cube(50, anchor=CENTER)
 //       tag("visible"){
 //         attach(LEFT, BOTTOM) cylinder(d=30, l=30);
 //         attach(RIGHT, BOTTOM) cylinder(d=30, l=30);
@@ -1239,7 +1254,7 @@ module show_only(tags)
     req_children($children);
     dummy=assert(is_string(tags), str("tags must be a string",tags));
     taglist = [for(s=str_split(tags," ",keep_nulls=false)) str($tag_prefix,s)];
-    $tags_shown = taglist;    
+    $tags_shown = taglist;
     children();
 }
 
@@ -1258,7 +1273,7 @@ module show_all()
    $tags_shown="ALL";
    $tags_hidden=[];
    children();
-}   
+}
 
 
 // Module: show_int()
@@ -1284,16 +1299,58 @@ module show_int(tags)
 // Section: Attachable Masks
 
 
+// Module: face_mask()
+// Usage:
+//   PARENT() face_mask(faces) CHILDREN;
+// Topics: Attachments, Masking
+// Description:
+//   Takes a 3D mask shape, and attaches it to the given faces, with the appropriate orientation to be
+//   differenced away.  The mask shape should be vertically oriented (Z-aligned) with the bottom half
+//   (Z-) shaped to be diffed away from the face of parent attachable shape.  If no tag is set then
+//   `face_mask()` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
+//   For details on specifying the faces to mask see [Specifying Faces](attachments.scad#subsection-specifying-faces).
+//   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
+// Arguments:
+//   edges = Faces to mask.  See  [Specifying Faces](attachments.scad#subsection-specifying-faces) for information on specifying faces.  Default: All faces
+// Side Effects:
+//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
+//   `$idx` is set to the index number of each face in the list of faces given.
+//   `$attach_anchor` is set for each face given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
+// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_mask(), edge_mask()
+// Example:
+//   diff()
+//   cylinder(r=30, h=60)
+//       face_mask(TOP) {
+//           rounding_cylinder_mask(r=30,rounding=5);
+//           cuboid([5,61,10]);
+//       }
+// Example: Using `$idx`
+//   diff()
+//   cylinder(r=30, h=60)
+//       face_mask([TOP, BOT])
+//           zrot(45*$idx) zrot_copies([0,90]) cuboid([5,61,10]);
+module face_mask(faces=[LEFT,RIGHT,FRONT,BACK,BOT,TOP]) {
+    req_children($children);
+    faces = is_vector(faces)? [faces] : faces;
+    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.");
+    assert($parent_geom != undef, "No object to attach to!");
+    attach(faces) {
+       if ($tag=="") tag("remove") children();
+       else children();
+    }
+}
+
+
 // Module: edge_mask()
 // Usage:
-//   edge_mask([edges], [except]) CHILDREN;
-// Topics: Attachments
-// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_mask()
+//   PARENT() edge_mask([edges], [except]) CHILDREN;
+// Topics: Attachments, Masking
+// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_mask(), face_mask()
 // Description:
 //   Takes a 3D mask shape, and attaches it to the given edges, with the appropriate orientation to be
 //   differenced away.  The mask shape should be vertically oriented (Z-aligned) with the back-right
 //   quadrant (X+Y+) shaped to be diffed away from the edge of parent attachable shape.  If no tag is set
-//   then `edge_mask` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.  
+//   then `edge_mask` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
 //   For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Figure: A Typical Edge Rounding Mask
@@ -1308,8 +1365,12 @@ module show_int(tags)
 //   edges = Edges to mask.  See [Specifying Edges](attachments.scad#subsection-specifying-edges).  Default: All edges.
 //   except = Edges to explicitly NOT mask.  See [Specifying Edges](attachments.scad#subsection-specifying-edges).  Default: No edges.
 // Side Effects:
-//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set. 
-// Example:  
+//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
+// Side Effects:
+//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
+//   `$idx` is set to the index number of each edge.
+//   `$attach_anchor` is set for each edge given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
+// Example:
 //   diff()
 //   cube([50,60,70],center=true)
 //       edge_mask([TOP,"Z"],except=[BACK,TOP+LEFT])
@@ -1323,7 +1384,8 @@ module edge_mask(edges=EDGES_ALL, except=[]) {
         if (edges[axis][i]>0)
         EDGE_OFFSETS[axis][i]
     ];
-    for (vec = vecs) {
+    for ($idx = idx(vecs)) {
+        vec = vecs[$idx];
         vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0);
         dummy=assert(vcount == 2, "Not an edge vector!");
         anch = _find_anchor(vec, $parent_geom);
@@ -1344,20 +1406,22 @@ module edge_mask(edges=EDGES_ALL, except=[]) {
 
 // Module: corner_mask()
 // Usage:
-//   corner_mask([corners], [except]) CHILDREN;
-// Topics: Attachments
+//   PARENT() corner_mask([corners], [except]) CHILDREN;
+// Topics: Attachments, Masking
 // See Also: attachable(), position(), attach(), face_profile(), edge_profile(), edge_mask()
 // Description:
 //   Takes a 3D mask shape, and attaches it to the specified corners, with the appropriate orientation to
 //   be differenced away.  The 3D corner mask shape should be designed to mask away the X+Y+Z+ octant.  If no tag is set
-//   then `corner_mask` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.  
-//   See [Specifying Corners](attachments.scad#subsection-specifying-corners) for information on how to specify corner sets.  
+//   then `corner_mask` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
+//   See [Specifying Corners](attachments.scad#subsection-specifying-corners) for information on how to specify corner sets.
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   corners = Corners to mask.  See [Specifying Corners](attachments.scad#subsection-specifying-corners).  Default: All corners.
 //   except = Corners to explicitly NOT mask.  See [Specifying Corners](attachments.scad#subsection-specifying-corners).  Default: No corners.
 // Side Effects:
-//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set. 
+//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
+//   `$idx` is set to the index number of each corner.
+//   `$attach_anchor` is set for each corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
 // Example:
 //   diff()
 //   cube(100, center=true)
@@ -1371,7 +1435,8 @@ module corner_mask(corners=CORNERS_ALL, except=[]) {
     assert($parent_geom != undef, "No object to attach to!");
     corners = _corners(corners, except=except);
     vecs = [for (i = [0:7]) if (corners[i]>0) CORNER_OFFSETS[i]];
-    for (vec = vecs) {
+    for ($idx = idx(vecs)) {
+        vec = vecs[$idx];
         vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0);
         dummy=assert(vcount == 3, "Not an edge vector!");
         anch = _find_anchor(vec, $parent_geom);
@@ -1390,13 +1455,13 @@ module corner_mask(corners=CORNERS_ALL, except=[]) {
 
 // Module: face_profile()
 // Usage:
-//   face_profile(faces, r|d=, [convexity=]) CHILDREN;
-// Topics: Attachments
+//   PARENT() face_profile(faces, r|d=, [convexity=]) CHILDREN;
+// Topics: Attachments, Masking
 // See Also: attachable(), position(), attach(), edge_profile(), corner_profile()
 // Description:
 //   Given a 2D edge profile, extrudes it into a mask for all edges and corners bounding each given face. If no tag is set
-//   then `face_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.  
-//   See  [Specifying Faces](attachments.scad#subsection-specifying-faces) for information on specifying faces.  
+//   then `face_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
+//   See  [Specifying Faces](attachments.scad#subsection-specifying-faces) for information on specifying faces.
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   faces = Faces to mask edges and corners of.
@@ -1405,7 +1470,10 @@ module corner_mask(corners=CORNERS_ALL, except=[]) {
 //   d = Diameter of corner mask.
 //   convexity = Max number of times a line could intersect the perimeter of the mask shape.  Default: 10
 // Side Effects:
-//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set. 
+//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
+//   `$idx` is set to the index number of each face.
+//   `$attach_anchor` is set for each edge or corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
+//   `$profile_type` is set to `"edge"` or `"corner"`, depending on what is being masked.
 // Example:
 //   diff()
 //   cube([50,60,70],center=true)
@@ -1424,13 +1492,13 @@ module face_profile(faces=[], r, d, convexity=10) {
 
 // Module: edge_profile()
 // Usage:
-//   edge_profile([edges], [except], [convexity]) CHILDREN;
-// Topics: Attachments
+//   PARENT() edge_profile([edges], [except], [convexity]) CHILDREN;
+// Topics: Attachments, Masking
 // See Also: attachable(), position(), attach(), face_profile(), corner_profile()
 // Description:
 //   Takes a 2D mask shape and attaches it to the selected edges, with the appropriate orientation and
 //   extruded length to be `diff()`ed away, to give the edge a matching profile.  If no tag is set
-//   then `edge_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.  
+//   then `edge_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
 //   For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
 //   For a step-by-step
 //   explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
@@ -1439,7 +1507,10 @@ module face_profile(faces=[], r, d, convexity=10) {
 //   except = Edges to explicitly NOT mask.  See [Specifying Edges](attachments.scad#subsection-specifying-edges).  Default: No edges.
 //   convexity = Max number of times a line could intersect the perimeter of the mask shape.  Default: 10
 // Side Effects:
-//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set. 
+//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
+//   `$idx` is set to the index number of each edge.
+//   `$attach_anchor` is set for each edge given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
+//   `$profile_type` is set to `"edge"`.
 // Example:
 //   diff()
 //   cube([50,60,70],center=true)
@@ -1454,13 +1525,15 @@ module edge_profile(edges=EDGES_ALL, except=[], convexity=10) {
         if (edges[axis][i]>0)
         EDGE_OFFSETS[axis][i]
     ];
-    for (vec = vecs) {
+    for ($idx = idx(vecs)) {
+        vec = vecs[$idx];
         vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0);
         dummy=assert(vcount == 2, "Not an edge vector!");
         anch = _find_anchor(vec, $parent_geom);
         $attach_to = undef;
         $attach_anchor = anch;
         $attach_norot = true;
+        $profile_type = "edge";
         psize = point3d($parent_size);
         length = [for (i=[0:2]) if(!vec[i]) psize[i]][0]+0.1;
         rotang =
@@ -1481,14 +1554,14 @@ module edge_profile(edges=EDGES_ALL, except=[], convexity=10) {
 
 // Module: corner_profile()
 // Usage:
-//   corner_profile([corners], [except], [r=|d=], [convexity=]) CHILDREN;
-// Topics: Attachments
+//   PARENT() corner_profile([corners], [except], [r=|d=], [convexity=]) CHILDREN;
+// Topics: Attachments, Masking
 // See Also: attachable(), position(), attach(), face_profile(), edge_profile()
 // Description:
 //   Takes a 2D mask shape, rotationally extrudes and converts it into a corner mask, and attaches it
 //   to the selected corners with the appropriate orientation. If no tag is set
-//   then `corner_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.  
-//   See [Specifying Corners](attachments.scad#subsection-specifying-corners) for information on how to specify corner sets.  
+//   then `corner_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
+//   See [Specifying Corners](attachments.scad#subsection-specifying-corners) for information on how to specify corner sets.
 //   For a step-by-step explanation of attachments, see the [[Attachments Tutorial|Tutorial-Attachments]].
 // Arguments:
 //   corners = Corners to mask.  See [Specifying Corners](attachments.scad#subsection-specifying-corners).  Default: All corners.
@@ -1498,7 +1571,10 @@ module edge_profile(edges=EDGES_ALL, except=[], convexity=10) {
 //   d = Diameter of corner mask.
 //   convexity = Max number of times a line could intersect the perimeter of the mask shape.  Default: 10
 // Side Effects:
-//   Tags the children with "remove" (and hence sets $tag) if no tag is already set. 
+//   Tags the children with "remove" (and hence sets $tag) if no tag is already set.
+//   `$idx` is set to the index number of each corner.
+//   `$attach_anchor` is set for each corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
+//   `$profile_type` is set to `"corner"`.
 // Example:
 //   diff()
 //   cuboid([50,60,70],rounding=10,edges="Z",anchor=CENTER) {
@@ -1511,13 +1587,15 @@ module corner_profile(corners=CORNERS_ALL, except=[], r, d, convexity=10) {
     assert(is_num(r));
     corners = _corners(corners, except=except);
     vecs = [for (i = [0:7]) if (corners[i]>0) CORNER_OFFSETS[i]];
-    for (vec = vecs) {
+    for ($idx = idx(vecs)) {
+        vec = vecs[$idx];
         vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0);
         dummy=assert(vcount == 3, "Not an edge vector!");
         anch = _find_anchor(vec, $parent_geom);
         $attach_to = undef;
         $attach_anchor = anch;
         $attach_norot = true;
+        $profile_type = "corner";
         rotang = vec.z<0?
             [  0,0,180+v_theta(vec)-45] :
             [180,0,-90+v_theta(vec)-45];
@@ -1772,7 +1850,7 @@ module corner_profile(corners=CORNERS_ALL, except=[], r, d, convexity=10) {
 //       children();
 //   }
 //
-// 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 need to call attachable() twice, because tags inside the attachable() call don't work as expected.  This example shows how you can call attachable twice to 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. 
+// 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 need to call attachable() twice, because tags inside the attachable() call don't work as expected.  This example shows how you can call attachable twice to 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.
 //   module thing(anchor,spin,orient) {
 //      tag("remove") attachable(size=[15,15,15],anchor=anchor,spin=spin,orient=orient){
 //        cuboid([10,10,16]);
@@ -1782,19 +1860,19 @@ module corner_profile(corners=CORNERS_ALL, except=[], r, d, convexity=10) {
 //        cuboid([15,15,15]);
 //        children();
 //      }
-//   }   
+//   }
 //   diff()
 //     cube([19,10,19])
 //       attach([FRONT],overlap=-4)
 //         thing(anchor=TOP)
 //           # attach(TOP) cuboid(2,anchor=TOP);
-// 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.  
+// 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.
 //   module thing(anchor = CENTER, spin = 0, orient = UP) {
 //      tag("remove") attachable(anchor, spin, orient, d1=0,d2=95,h=33) {
 //          cylinder(h = 33.1, d1 = 0, d2 = 95, anchor=CENTER);
 //          union(){}  // dummy children
 //      }
-//      tag("keep") attachable(anchor, spin, orient,d1=0,d2=95,h=33) {      
+//      tag("keep") attachable(anchor, spin, orient,d1=0,d2=95,h=33) {
 //            cylinder(h = 33, d = 10,anchor=CENTER);
 //            children();
 //        }
@@ -1957,7 +2035,7 @@ function reorient(
     axis=UP,
     geom,
     p=undef
-) = 
+) =
     assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor))
     assert(is_undef(spin)   || is_vector(spin,3) || is_num(spin), str("Got: ",spin))
     assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient))
@@ -2371,7 +2449,6 @@ function _attach_transform(anchor, spin, orient, geom, p) =
 
 
 function _get_cp(geom) =
-    
     let(cp=select(geom,-3))
     is_vector(cp) ? cp
   : let(
@@ -2410,7 +2487,7 @@ function _force_anchor_2d(anchor) =
 //   anchor = Vector or named anchor string.
 //   geom = The geometry description of the shape.
 function _find_anchor(anchor, geom) =
-    is_string(anchor)? (  
+    is_string(anchor)? (
           anchor=="origin"? [anchor, CENTER, UP, 0]
         : let(
               anchors = last(geom),
@@ -2419,7 +2496,7 @@ function _find_anchor(anchor, geom) =
           assert(found!=[], str("Unknown anchor: ",anchor))
           anchors[found]
     ) :
-    let( 
+    let(
         cp = _get_cp(geom),
         offset_raw = select(geom,-2),
         offset = [for (i=[0:2]) anchor[i]==0? 0 : offset_raw[i]],  // prevents bad centering.
@@ -2617,7 +2694,7 @@ function _find_anchor(anchor, geom) =
             rgn = force_region(geom[1]),
             rpts = rot(from=anchor, to=RIGHT, p=flatten(rgn)),
             maxx = max(column(rpts,0)),
-            ys = [for (pt=rpts) if (approx(pt.x, maxx)) pt.y],            
+            ys = [for (pt=rpts) if (approx(pt.x, maxx)) pt.y],
             midy = (min(ys)+max(ys))/2,
             pos = rot(from=RIGHT, to=anchor, p=[maxx,midy])
         ) [anchor, pos, unit(anchor,BACK), 0]
@@ -2656,7 +2733,7 @@ function _find_anchor(anchor, geom) =
 /// Topics: Attachments
 /// See Also: reorient(), attachable()
 // Description:
-//   Returns true if objects should currently be shown based on the tag settings.  
+//   Returns true if objects should currently be shown based on the tag settings.
 function _is_shown() =
     assert(is_list($tags_shown) || $tags_shown=="ALL")
     assert(is_list($tags_hidden))
@@ -2705,7 +2782,7 @@ function _standard_anchors(two_d=false) = [
 //   s = Length of anchor arrows.
 //   ---
 //   std = If true show standard anchors.  Default: true
-//   custom = If true show named anchors.  Default: true 
+//   custom = If true show named anchors.  Default: true
 // Example(FlatSpin,VPD=333):
 //   cube(50, center=true) show_anchors();
 module show_anchors(s=10, std=true, custom=true) {
@@ -2812,7 +2889,7 @@ module anchor_arrow2d(s=15, color=[0.333,0.333,1], $tag="anchor-arrow") {
 //   expose_anchors(opacity) {child1() show_anchors(); child2() show_anchors(); ...}
 // Description:
 //   Used in combination with show_anchors() to display an object in transparent gray with its anchors in solid color.
-//   Children will appear transparent and any anchor arrows drawn with will appear in solid color.  
+//   Children will appear transparent and any anchor arrows drawn with will appear in solid color.
 // Arguments:
 //   opacity = The opacity of the children.  0.0 is invisible, 1.0 is opaque.  Default: 0.2
 // Example(FlatSpin,VPD=333):
@@ -2822,7 +2899,7 @@ module expose_anchors(opacity=0.2) {
         children();
     hide("anchor-arrow")
         color(is_undef($color) || $color=="default" ? [0,0,0] :
-              is_string($color) ? $color 
+              is_string($color) ? $color
                                 : point3d($color),
               opacity)
             children();
@@ -3025,7 +3102,7 @@ function _normalize_edges(v) = [for (ax=v) [for (edge=ax) edge>0? 1 : 0]];
 ///
 /// Description:
 ///   Takes a list of edge set descriptors, and returns a normalized edges array
-///   that represents all those given edges.  
+///   that represents all those given edges.
 /// Arguments:
 ///   v = The edge set to include.
 ///   except = The edge set to specifically exclude, even if they are in `v`.
@@ -3258,7 +3335,6 @@ module _show_cube_faces(faces, size=20, toplabel,botlabel) {
           move(f*size/2) rot(from=UP,to=f)
              cuboid([size,size,.1]);
      }
-    
    vpr = [55,0,25];
    color("black"){
    if (is_def(toplabel))

From 98bb93768cc6fc74b8d34d9557877f2c823f5077 Mon Sep 17 00:00:00 2001
From: Revar Desmera <revarbat@gmail.com>
Date: Wed, 18 Jan 2023 01:04:44 -0800
Subject: [PATCH 2/6] Fix for #1018: distributors.scad matrix functions

---
 distributors.scad | 711 +++++++++++++++++++++++++++++++++++++++-------
 transforms.scad   |   2 +-
 2 files changed, 609 insertions(+), 104 deletions(-)

diff --git a/distributors.scad b/distributors.scad
index cdbe179..02a8bb3 100644
--- a/distributors.scad
+++ b/distributors.scad
@@ -16,21 +16,33 @@
 //////////////////////////////////////////////////////////////////////
 
 
-// Module: move_copies()
-//
-// Description:
-//   Translates copies of all children to each given translation offset.
+// Function&Module: move_copies()
 //
 // Usage:
 //   move_copies(a) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = move_copies(a, p=);
+// Usage: Get Translation Matrices
+//   mats = move_copies(a);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, translates copies of all children to each given translation offset.
+//   When called as a function, with no `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   a = Array of XYZ offset vectors. Default `[[0,0,0]]`
+//   ---
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually.
 //   `$idx` is set to the index number of each child being copied.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   #sphere(r=10);
 //   move_copies([[-25,-25,0], [25,-25,0], [0,0,50], [0,25,0]]) sphere(r=10);
@@ -45,27 +57,54 @@ module move_copies(a=[[0,0,0]])
     }
 }
 
+function move_copies(a=[[0,0,0]],p=_NO_ARG) =
+    assert(is_list(a))
+    let(
+        mats = [
+            for (pos = a)
+            assert(is_vector(pos),"move_copies offsets should be a 2d or 3d vector.")
+            translate(pos)
+        ]
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
 
-// Module: xcopies()
-//
-// Description:
-//   Places out `n` copies of the children along a line on the X axis.
+
+// Function&Module: xcopies()
 //
 // Usage:
 //   xcopies(spacing, [n], [sp]) CHILDREN;
 //   xcopies(l, [n], [sp]) CHILDREN;
 //   xcopies(LIST) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = xcopies(spacing, [n], [sp], p=);
+//   copies = xcopies(l, [n], [sp], p=);
+//   copies = xcopies(LIST, p=);
+// Usage: Get Translation Matrices
+//   mats = xcopies(spacing, [n], [sp]);
+//   mats = xcopies(l, [n], [sp]);
+//   mats = xcopies(LIST);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, places `n` copies of the children along a line on the X axis.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
+//   ---
 //   spacing = Given a scalar, specifies a uniform spacing between copies. Given a list of scalars, each one gives a specific position along the line. (Default: 1.0)
 //   n = Number of copies to place. (Default: 2)
 //   l = Length to place copies over.
 //   sp = If given as a point, copies will be placed on a line to the right of starting position `sp`.  If given as a scalar, copies will be placed on a line to the right of starting position `[sp,0,0]`.  If not given, copies will be placed along a line that is centered at [0,0,0].
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually.
 //   `$idx` is set to the index number of each child being copied.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Examples:
 //   xcopies(20) sphere(3);
 //   xcopies(20, n=3) sphere(3);
@@ -101,26 +140,53 @@ module xcopies(spacing, n, l, sp)
 }
 
 
-// Module: ycopies()
-//
-// Description:
-//   Places `n` copies of the children along a line on the Y axis.
+function xcopies(spacing, n, l, sp, p=_NO_ARG) =
+    let(
+        dir = RIGHT,
+        sp = is_finite(sp)? (sp*dir) : sp,
+        mats = is_vector(spacing)
+          ? let(sp = default(sp,[0,0,0])) [for (n = spacing) translate(sp + n*dir)]
+          : line_copies(l=u_mul(l,dir), spacing=u_mul(spacing,dir), n=n, p1=sp)
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
+
+
+// Function&Module: ycopies()
 //
 // Usage:
 //   ycopies(spacing, [n], [sp]) CHILDREN;
 //   ycopies(l, [n], [sp]) CHILDREN;
 //   ycopies(LIST) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = ycopies(spacing, [n], [sp], p=);
+//   copies = ycopies(l, [n], [sp], p=);
+//   copies = ycopies(LIST, p=);
+// Usage: Get Translation Matrices
+//   mats = ycopies(spacing, [n], [sp]);
+//   mats = ycopies(l, [n], [sp]);
+//   mats = ycopies(LIST);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, places `n` copies of the children along a line on the Y axis.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
+//   ---
 //   spacing = Given a scalar, specifies a uniform spacing between copies. Given a list of scalars, each one gives a specific position along the line. (Default: 1.0)
 //   n = Number of copies to place on the line. (Default: 2)
 //   l = Length to place copies over.
 //   sp = If given as a point, copies will be place on a line back from starting position `sp`.  If given as a scalar, copies will be placed on a line back from starting position `[0,sp,0]`.  If not given, copies will be placed along a line that is centered at [0,0,0].
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually.
 //   `$idx` is set to the index number of each child being copied.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Examples:
 //   ycopies(20) sphere(3);
 //   ycopies(20, n=3) sphere(3);
@@ -156,26 +222,53 @@ module ycopies(spacing, n, l, sp)
 }
 
 
-// Module: zcopies()
-//
-// Description:
-//   Places `n` copies of the children along a line on the Z axis.
+function ycopies(spacing, n, l, sp, p=_NO_ARG) =
+    let(
+        dir = BACK,
+        sp = is_finite(sp)? (sp*dir) : sp,
+        mats = is_vector(spacing)
+          ? let(sp = default(sp,[0,0,0])) [for (n = spacing) translate(sp + n*dir)]
+          : line_copies(l=u_mul(l,dir), spacing=u_mul(spacing,dir), n=n, p1=sp)
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
+
+
+// Function&Module: zcopies()
 //
 // Usage:
 //   zcopies(spacing, [n], [sp]) CHILDREN;
 //   zcopies(l, [n], [sp]) CHILDREN;
 //   zcopies(LIST) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = zcopies(spacing, [n], [sp], p=);
+//   copies = zcopies(l, [n], [sp], p=);
+//   copies = zcopies(LIST, p=);
+// Usage: Get Translation Matrices
+//   mats = zcopies(spacing, [n], [sp]);
+//   mats = zcopies(l, [n], [sp]);
+//   mats = zcopies(LIST);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, places `n` copies of the children along a line on the Z axis.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
+//   ---
 //   spacing = Given a scalar, specifies a uniform spacing between copies. Given a list of scalars, each one gives a specific position along the line. (Default: 1.0)
 //   n = Number of copies to place. (Default: 2)
 //   l = Length to place copies over.
 //   sp = If given as a point, copies will be placed on a line up from starting position `sp`.  If given as a scalar, copies will be placed on a line up from starting position `[0,0,sp]`.  If not given, copies will be placed on a line that is centered at [0,0,0].
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually.
 //   `$idx` is set to the index number of each child being copied.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Examples:
 //   zcopies(20) sphere(3);
 //   zcopies(20, n=3) sphere(3);
@@ -225,6 +318,17 @@ module zcopies(spacing, n, l, sp)
 }
 
 
+function zcopies(spacing, n, l, sp, p=_NO_ARG) =
+    let(
+        dir = UP,
+        sp = is_finite(sp)? (sp*dir) : sp,
+        mats = is_vector(spacing)
+          ? let(sp = default(sp,[0,0,0])) [for (n = spacing) translate(sp + n*dir)]
+          : line_copies(l=u_mul(l,dir), spacing=u_mul(spacing,dir), n=n, p1=sp)
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
+
+
 
 // Function&Module: line_copies()
 //
@@ -238,14 +342,24 @@ module zcopies(spacing, n, l, sp)
 //   line_copies([n=], [p1=], [p2=]) CHILDREN;
 // Usage: Place copies at the given spacing, centered along the line from `p1` to `p2`
 //   line_copies([spacing], [p1=], [p2=]) CHILDREN;
-// Usage: As a function
-//   pts = line_copies([spacing], [n], [p1=]);
-//   pts = line_copies([spacing], [l=], [p1=]);
-//   pts = line_copies([n=], [l=], [p1=]);
-//   pts = line_copies([n=], [p1=], [p2=]);
-//   pts = line_copies([spacing], [p1=], [p2=]);
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = line_copies([spacing], [n], [p1=], p=);
+//   copies = line_copies([spacing], [l=], [p1=], p=);
+//   copies = line_copies([n=], [l=], [p1=], p=);
+//   copies = line_copies([n=], [p1=], [p2=], p=);
+//   copies = line_copies([spacing], [p1=], [p2=], p=);
+// Usage: Get Translation Matrices
+//   mats = line_copies([spacing], [n], [p1=]);
+//   mats = line_copies([spacing], [l=], [p1=]);
+//   mats = line_copies([n=], [l=], [p1=]);
+//   mats = line_copies([n=], [p1=], [p2=]);
+//   mats = line_copies([spacing], [p1=], [p2=]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
 // Description:
-//   When called as a function, returns a list of points at evenly spaced positions along a line.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //   When called as a module, copies `children()` at one or more evenly spaced positions along a line.
 //   By default, the line will be centered at the origin, unless the starting point `p1` is given.
 //   The line will be pointed towards `RIGHT` (X+) unless otherwise given as a vector in `l`,
@@ -270,11 +384,14 @@ module zcopies(spacing, n, l, sp)
 //   l = Either the scalar length of the line, or a vector giving both the direction and length of the line.
 //   p1 = If given, specifies the starting point of the line.
 //   p2 = If given with `p1`, specifies the ending point of line, and indirectly calculates the line length.
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually.
 //   `$idx` is set to the index number of each child being copied.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Examples:
 //   line_copies(10) sphere(d=1);
 //   line_copies(10, n=5) sphere(d=1);
@@ -315,7 +432,7 @@ module line_copies(spacing, n, l, p1, p2)
     }
 }
 
-function line_copies(spacing, n, l, p1, p2) =
+function line_copies(spacing, n, l, p1, p2, p=_NO_ARG) =
     assert(is_undef(spacing) || is_finite(spacing) || is_vector(spacing))
     assert(is_undef(n) || is_finite(n))
     assert(is_undef(l) || is_finite(l) || is_vector(l))
@@ -336,23 +453,34 @@ function line_copies(spacing, n, l, p1, p2) =
     )
     assert(!is_undef(cnt), "Need two of `spacing`, 'l', 'n', or `p1`/`p2` arguments in `line_copies()`.")
     let( spos = !is_undef(p1)? point3d(p1) : -(cnt-1)/2 * spc )
-    [for (i=[0:1:cnt-1]) i * spc + spos];
+    [for (i=[0:1:cnt-1]) translate(i * spc + spos, p=p)];
 
 
 
-
-
-
-// Module: grid_copies()
-//
-// Description:
-//   Makes a square or hexagonal grid of copies of children, with an optional masking polygon or region.  
+// Function&Module: grid_copies()
 //
 // Usage:
 //   grid_copies(spacing, size=, [stagger=], [scale=], [inside=]) CHILDREN;
 //   grid_copies(n=, size=, [stagger=], [scale=], [inside=]) CHILDREN;
 //   grid_copies(spacing, [n], [stagger=], [scale=], [inside=]) CHILDREN;
 //   grid_copies(n=, inside=, [stagger], [scale]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = grid_copies(spacing, size=, [stagger=], [scale=], [inside=], p=);
+//   copies = grid_copies(n=, size=, [stagger=], [scale=], [inside=], p=);
+//   copies = grid_copies(spacing, [n], [stagger=], [scale=], [inside=], p=);
+//   copies = grid_copies(n=, inside=, [stagger], [scale], p=);
+// Usage: Get Translation Matrices
+//   mats = grid_copies(spacing, size=, [stagger=], [scale=], [inside=]);
+//   mats = grid_copies(n=, size=, [stagger=], [scale=], [inside=]);
+//   mats = grid_copies(spacing, [n], [stagger=], [scale=], [inside=]);
+//   mats = grid_copies(n=, inside=, [stagger], [scale]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, makes a square or hexagonal grid of copies of children, with an optional masking polygon or region.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   spacing = Distance between copies in [X,Y] or scalar distance.
@@ -362,12 +490,15 @@ function line_copies(spacing, n, l, p1, p2) =
 //   stagger = If true, make a staggered (hexagonal) grid.  If false, make square grid.  If `"alt"`, makes alternate staggered pattern.  Default: false
 //   inside = If given a list of polygon points, or a region, only creates copies whose center would be inside the polygon or region.  Polygon can be concave and/or self crossing.
 //   nonzero = If inside is set to a polygon with self-crossings then use the nonzero method for deciding if points are in the polygon.  Default: false
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually.
 //   `$col` is set to the integer column number for each child.
 //   `$row` is set to the integer row number for each child.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Examples:
 //   grid_copies(size=50, spacing=10) cylinder(d=10, h=1);
 //   grid_copies(size=50, spacing=[10,15]) cylinder(d=10, h=1);
@@ -399,7 +530,6 @@ function line_copies(spacing, n, l, p1, p2) =
 //               cylinder(h=20, d=10/cos(180/6)+0.01, $fn=6);
 //   }
 
-function grid_copies(spacing, n, size, stagger=false, inside=undef, nonzero) = no_function("grid_copies");
 module grid2d(spacing, n, size, stagger=false, inside=undef, nonzero)
 {
    deprecate("grid_copies");
@@ -482,27 +612,108 @@ module grid_copies(spacing, n, size, stagger=false, inside=undef, nonzero)
 }
 
 
+function grid_copies(spacing, n, size, stagger=false, inside=undef, nonzero, p=_NO_ARG) =
+    let(
+        dummy = assert(in_list(stagger, [false, true, "alt"])),
+        bounds = is_undef(inside)? undef :
+            is_path(inside)? pointlist_bounds(inside) :
+            assert(is_region(inside))
+            pointlist_bounds(flatten(inside)),
+        nonzero = is_path(inside) ? default(nonzero,false)
+                : assert(is_undef(nonzero), "nonzero only allowed if inside is a polygon")
+                  false,
+        size = is_num(size)? [size, size] :
+            is_vector(size)? assert(len(size)==2) size :
+            bounds!=undef? [
+                for (i=[0:1]) 2*max(abs(bounds[0][i]),bounds[1][i])
+            ] : undef,
+        spacing = is_num(spacing)? (
+                stagger!=false? polar_to_xy(spacing,60) :
+                [spacing,spacing]
+            ) :
+            is_vector(spacing)? assert(len(spacing)==2) spacing :
+            size!=undef? (
+                is_num(n)? v_div(size,(n-1)*[1,1]) :
+                is_vector(n)? assert(len(n)==2) v_div(size,n-[1,1]) :
+                v_div(size,(stagger==false? [1,1] : [2,2]))
+            ) :
+            undef,
+        n = is_num(n)? [n,n] :
+            is_vector(n)? assert(len(n)==2) n :
+            size!=undef && spacing!=undef? v_floor(v_div(size,spacing))+[1,1] :
+            [2,2],
+        offset = v_mul(spacing, n-[1,1])/2,
+        mats = stagger == false
+          ? [
+                for (row = [0:1:n.y-1], col = [0:1:n.x-1])
+                let( pos = v_mul([col,row],spacing) - offset )
+                if (
+                    is_undef(inside) ||
+                    (is_path(inside) && point_in_polygon(pos, inside, nonzero=nonzero)>=0) ||
+                    (is_region(inside) && point_in_region(pos, inside)>=0)
+                )
+                translate(pos)
+            ]
+          : // stagger == true or stagger == "alt"
+            let(
+                staggermod = (stagger == "alt")? 1 : 0,
+                cols1 = ceil(n.x/2),
+                cols2 = n.x - cols1
+            )
+            [
+                for (row = [0:1:n.y-1])
+                let( rowcols = ((row%2) == staggermod)? cols1 : cols2 )
+                if (rowcols > 0)
+                for (col = [0:1:rowcols-1])
+                let(
+                    rowdx = (row%2 != staggermod)? spacing.x : 0,
+                    pos = v_mul([2*col,row],spacing) + [rowdx,0] - offset
+                )
+                if (
+                    is_undef(inside) ||
+                    (is_path(inside) && point_in_polygon(pos, inside, nonzero=nonzero)>=0) ||
+                    (is_region(inside) && point_in_region(pos, inside)>=0)
+                )
+                translate(pos)
+            ]
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
+
+
 //////////////////////////////////////////////////////////////////////
 // Section: Rotating copies of all children
 //////////////////////////////////////////////////////////////////////
 
 
-// Module: rot_copies()
-//
-// Description:
-//   Given a list of [X,Y,Z] rotation angles in `rots`, rotates copies of the children to each of those angles, regardless of axis of rotation.
-//   Given a list of scalar angles in `rots`, rotates copies of the children to each of those angles around the axis of rotation.
-//   If given a vector `v`, that becomes the axis of rotation.  Default axis of rotation is UP.
-//   If given a count `n`, makes that many copies, rotated evenly around the axis.
-//   If given an offset `delta`, translates each child by that amount before rotating them into place.  This makes rings.
-//   If given a centerpoint `cp`, centers the ring around that centerpoint.
-//   If `subrot` is true, each child will be rotated in place to keep the same size towards the center when making rings.
-//   The first (unrotated) copy will be placed at the relative starting angle `sa`.
+// Function&Module: rot_copies()
 //
 // Usage:
 //   rot_copies(rots, [cp=], [sa=], [delta=], [subrot=]) CHILDREN;
 //   rot_copies(rots, v, [cp=], [sa=], [delta=], [subrot=]) CHILDREN;
 //   rot_copies(n=, [v=], [cp=], [sa=], [delta=], [subrot=]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = rot_copies(rots, [cp=], [sa=], [delta=], [subrot=], p=);
+//   copies = rot_copies(rots, v, [cp=], [sa=], [delta=], [subrot=], p=);
+//   copies = rot_copies(n=, [v=], [cp=], [sa=], [delta=], [subrot=], p=);
+// Usage: Get Translation Matrices
+//   mats = rot_copies(rots, [cp=], [sa=], [delta=], [subrot=]);
+//   mats = rot_copies(rots, v, [cp=], [sa=], [delta=], [subrot=]);
+//   mats = rot_copies(n=, [v=], [cp=], [sa=], [delta=], [subrot=]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module:
+//   - Given a list of [X,Y,Z] rotation angles in `rots`, rotates copies of the children to each of those angles, regardless of axis of rotation.
+//   - Given a list of scalar angles in `rots`, rotates copies of the children to each of those angles around the axis of rotation.
+//   - If given a vector `v`, that becomes the axis of rotation.  Default axis of rotation is UP.
+//   - If given a count `n`, makes that many copies, rotated evenly around the axis.
+//   - If given an offset `delta`, translates each child by that amount before rotating them into place.  This makes rings.
+//   - If given a centerpoint `cp`, centers the ring around that centerpoint.
+//   - If `subrot` is true, each child will be rotated in place to keep the same size towards the center when making rings.
+//   - The first (unrotated) copy will be placed at the relative starting angle `sa`.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   rots = A list of [X,Y,Z] rotation angles in degrees.  If `v` is given, this will be a list of scalar angles in degrees to rotate around `v`.
@@ -513,12 +724,15 @@ module grid_copies(spacing, n, size, stagger=false, inside=undef, nonzero)
 //   sa = Starting angle, in degrees.  For use with `n`.  Angle is in degrees counter-clockwise.  Default: 0
 //   delta = [X,Y,Z] amount to move away from cp before rotating.  Makes rings of copies.  Default: `[0,0,0]`
 //   subrot = If false, don't sub-rotate children as they are copied around the ring.  Only makes sense when used with `delta`.  Default: `true`
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$ang` is set to the rotation angle (or XYZ rotation triplet) of each child copy, and can be used to modify each child individually.
 //   `$idx` is set to the index value of each child copy.
 //   `$axis` is set to the axis to rotate around, if `rots` was given as a list of angles instead of a list of [X,Y,Z] rotation angles.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   #cylinder(h=20, r1=5, r2=0);
 //   rot_copies([[45,0,0],[0,45,90],[90,-45,270]]) cylinder(h=20, r1=5, r2=0);
@@ -575,19 +789,51 @@ module rot_copies(rots=[], v=undef, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0]
 }
 
 
-// Module: xrot_copies()
+function rot_copies(rots=[], v, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0], subrot=true, p=_NO_ARG) =
+    let(
+        sang = sa + offset,
+        angs = !is_undef(n)?
+            (n<=0? [] : [for (i=[0:1:n-1]) i/n*360+sang]) :
+            rots==[]? [] :
+            assert(!is_string(rots), "Argument rots must be an angle, a list of angles, or a range of angles.")
+            assert(!is_undef(rots[0]), "Argument rots must be an angle, a list of angles, or a range of angles.")
+            [for (a=rots) a],
+        mats = [
+            for (ang = angs)
+            translate(cp) *
+            rot(a=ang, v=v) *
+            translate(delta) *
+            rot(a=(subrot? sang : ang), v=v, reverse=true) *
+            translate(-cp)
+        ]
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
+
+
+// Function&Module: xrot_copies()
 //
 // Usage:
 //   xrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=]) CHILDREN;
 //   xrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = xrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=], p=);
+//   copies = xrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=], p=);
+// Usage: Get Translation Matrices
+//   mats = xrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=]);
+//   mats = xrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=]);
+//
+// Topics: Transformations, Distributors, Copiers
 //
 // Description:
-//   Given an array of angles, rotates copies of the children to each of those angles around the X axis.
-//   If given a count `n`, makes that many copies, rotated evenly around the X axis.
-//   If given a radius `r` (or diameter `d`), distributes children around a ring of that size around the X axis.
-//   If given a centerpoint `cp`, centers the rotation around that centerpoint.
-//   If `subrot` is true, each child will be rotated in place to keep the same size towards the center when making rings.
-//   The first (unrotated) copy will be placed at the relative starting angle `sa`.
+//   When called as a module:
+//   - Given an array of angles, rotates copies of the children to each of those angles around the X axis.
+//   - If given a count `n`, makes that many copies, rotated evenly around the X axis.
+//   - If given a radius `r` (or diameter `d`), distributes children around a ring of that size around the X axis.
+//   - If given a centerpoint `cp`, centers the rotation around that centerpoint.
+//   - If `subrot` is true, each child will be rotated in place to keep the same size towards the center when making rings.
+//   - The first (unrotated) copy will be placed at the relative starting angle `sa`.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   rots = Optional array of rotation angles, in degrees, to make copies at.
@@ -598,12 +844,15 @@ module rot_copies(rots=[], v=undef, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0]
 //   r = If given, makes a ring of child copies around the X axis, at the given radius.  Default: 0
 //   d = If given, makes a ring of child copies around the X axis, at the given diameter.
 //   subrot = If false, don't sub-rotate children as they are copied around the ring.
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$idx` is set to the index value of each child copy.
 //   `$ang` is set to the rotation angle of each child copy, and can be used to modify each child individually.
 //   `$axis` is set to the axis vector rotated around.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   xrot_copies([180, 270, 315])
 //       cylinder(h=20, r1=5, r2=0);
@@ -636,19 +885,35 @@ module xrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
 }
 
 
-// Module: yrot_copies()
+function xrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG) =
+    let( r = get_radius(r=r, d=d, dflt=0) )
+    rot_copies(rots=rots, v=RIGHT, cp=cp, n=n, sa=sa, delta=[0, r, 0], subrot=subrot, p=p);
+
+
+// Function&Module: yrot_copies()
 //
 // Usage:
 //   yrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=]) CHILDREN;
 //   yrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = yrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=], p=);
+//   copies = yrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=], p=);
+// Usage: Get Translation Matrices
+//   mats = yrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=]);
+//   mats = yrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=]);
+//
+// Topics: Transformations, Distributors, Copiers
 //
 // Description:
-//   Given an array of angles, rotates copies of the children to each of those angles around the Y axis.
-//   If given a count `n`, makes that many copies, rotated evenly around the Y axis.
-//   If given a radius `r` (or diameter `d`), distributes children around a ring of that size around the Y axis.
-//   If given a centerpoint `cp`, centers the rotation around that centerpoint.
-//   If `subrot` is true, each child will be rotated in place to keep the same size towards the center when making rings.
-//   The first (unrotated) copy will be placed at the relative starting angle `sa`.
+//   When called as a module:
+//   - Given an array of angles, rotates copies of the children to each of those angles around the Y axis.
+//   - If given a count `n`, makes that many copies, rotated evenly around the Y axis.
+//   - If given a radius `r` (or diameter `d`), distributes children around a ring of that size around the Y axis.
+//   - If given a centerpoint `cp`, centers the rotation around that centerpoint.
+//   - If `subrot` is true, each child will be rotated in place to keep the same size towards the center when making rings.
+//   - The first (unrotated) copy will be placed at the relative starting angle `sa`.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   rots = Optional array of rotation angles, in degrees, to make copies at.
@@ -659,12 +924,15 @@ module xrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
 //   r = If given, makes a ring of child copies around the Y axis, at the given radius.  Default: 0
 //   d = If given, makes a ring of child copies around the Y axis, at the given diameter.
 //   subrot = If false, don't sub-rotate children as they are copied around the ring.
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$idx` is set to the index value of each child copy.
 //   `$ang` is set to the rotation angle of each child copy, and can be used to modify each child individually.
 //   `$axis` is set to the axis vector rotated around.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   yrot_copies([180, 270, 315])
 //       cylinder(h=20, r1=5, r2=0);
@@ -697,19 +965,35 @@ module yrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
 }
 
 
-// Module: zrot_copies()
+function yrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG) =
+    let( r = get_radius(r=r, d=d, dflt=0) )
+    rot_copies(rots=rots, v=BACK, cp=cp, n=n, sa=sa, delta=[-r, 0, 0], subrot=subrot, p=p);
+
+
+// Function&Module: zrot_copies()
 //
 // Usage:
 //   zrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=]) CHILDREN;
 //   zrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = zrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=], p=);
+//   copies = zrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=], p=);
+// Usage: Get Translation Matrices
+//   mats = zrot_copies(rots, [cp], [r=|d=], [sa=], [subrot=]);
+//   mats = zrot_copies(n=, [cp=], [r=|d=], [sa=], [subrot=]);
+//
+// Topics: Transformations, Distributors, Copiers
 //
 // Description:
-//   Given an array of angles, rotates copies of the children to each of those angles around the Z axis.
-//   If given a count `n`, makes that many copies, rotated evenly around the Z axis.
-//   If given a radius `r` (or diameter `d`), distributes children around a ring of that size around the Z axis.
-//   If given a centerpoint `cp`, centers the rotation around that centerpoint.
-//   If `subrot` is true, each child will be rotated in place to keep the same size towards the center when making rings.
-//   The first (unrotated) copy will be placed at the relative starting angle `sa`.
+//   When called as a module:
+//   - Given an array of angles, rotates copies of the children to each of those angles around the Z axis.
+//   - If given a count `n`, makes that many copies, rotated evenly around the Z axis.
+//   - If given a radius `r` (or diameter `d`), distributes children around a ring of that size around the Z axis.
+//   - If given a centerpoint `cp`, centers the rotation around that centerpoint.
+//   - If `subrot` is true, each child will be rotated in place to keep the same size towards the center when making rings.
+//   - The first (unrotated) copy will be placed at the relative starting angle `sa`.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   rots = Optional array of rotation angles, in degrees, to make copies at.
@@ -720,12 +1004,16 @@ module yrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
 //   r = If given, makes a ring of child copies around the Z axis, at the given radius.  Default: 0
 //   d = If given, makes a ring of child copies around the Z axis, at the given diameter.
 //   subrot = If false, don't sub-rotate children as they are copied around the ring.  Default: true
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Side Effects:
 //   `$idx` is set to the index value of each child copy.
 //   `$ang` is set to the rotation angle of each child copy, and can be used to modify each child individually.
 //   `$axis` is set to the axis vector rotated around.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   zrot_copies([180, 270, 315])
 //       yrot(90) cylinder(h=20, r1=5, r2=0);
@@ -757,14 +1045,29 @@ module zrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
 }
 
 
-// Module: arc_copies()
-//
-// Description:
-//   Evenly distributes n duplicate children around an ovoid arc on the XY plane.
+function zrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG) =
+    let( r = get_radius(r=r, d=d, dflt=0) )
+    rot_copies(rots=rots, v=UP, cp=cp, n=n, sa=sa, delta=[r, 0, 0], subrot=subrot, p=p);
+
+
+// Function&Module: arc_copies()
 //
 // Usage:
 //   arc_copies(n, r|d=, [sa=], [ea=], [rot=]) CHILDREN;
 //   arc_copies(n, rx=|dx=, ry=|dy=, [sa=], [ea=], [rot=]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = arc_copies(n, r|d=, [sa=], [ea=], [rot=], p=);
+//   copies = arc_copies(n, rx=|dx=, ry=|dy=, [sa=], [ea=], [rot=], p=);
+// Usage: Get Translation Matrices
+//   mats = arc_copies(n, r|d=, [sa=], [ea=], [rot=]);
+//   mats = arc_copies(n, rx=|dx=, ry=|dy=, [sa=], [ea=], [rot=]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, evenly distributes n duplicate children around an ovoid arc on the XY plane.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   n = number of copies to distribute around the circle. (Default: 6)
@@ -778,12 +1081,15 @@ module zrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
 //   rot = whether to rotate the copied children.  (Default: true)
 //   sa = starting angle. (Default: 0.0)
 //   ea = ending angle. Will distribute copies CCW from sa to ea. (Default: 360.0)
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$ang` is set to the rotation angle of each child copy, and can be used to modify each child individually.
 //   `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually.
 //   `$idx` is set to the index value of each child copy.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   #cube(size=[10,3,3],center=true);
 //   arc_copies(d=40, n=5) cube(size=[10,3,3],center=true);
@@ -804,11 +1110,12 @@ module zrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
 //       if ($idx % 2 == 0) rect(6);
 //       else circle(d=6);
 
-function arc_copies(n=6,r,rx,ry,d,dx,dy,sa=0,ea=360,rot=true) = no_function("arc_copies");
 module arc_of(n=6,r,rx,ry,d,dx,dy,sa=0,ea=360,rot=true){
     deprecate("arc_copies");
     arc_copies(n,r,rx,ry,d,dx,dy,sa,ea,rot);
 }    
+
+
 module arc_copies(
     n=6,
     r=undef,
@@ -836,14 +1143,51 @@ module arc_copies(
 }
 
 
+function arc_copies(
+    n=6,
+    r=undef,
+    rx=undef, ry=undef,
+    d=undef, dx=undef, dy=undef,
+    sa=0, ea=360,
+    rot=true,
+    p=_NO_ARG
+) =
+    let(
+        rx = get_radius(r1=rx, r=r, d1=dx, d=d, dflt=1),
+        ry = get_radius(r1=ry, r=r, d1=dy, d=d, dflt=1),
+        sa = posmod(sa, 360),
+        ea = posmod(ea, 360),
+        n = (abs(ea-sa)<0.01)?(n+1):n,
+        delt = (((ea<=sa)?360.0:0)+ea-sa)/(n-1),
+        mats = [
+            for (i = [0:1:n-1])
+            let(
+                ang = sa + (i * delt),
+                pos =[rx*cos(ang), ry*sin(ang), 0],
+                ang2 = rot? atan2(ry*sin(ang), rx*cos(ang)) : 0
+            )
+            translate(pos) * zrot(ang2)
+        ]
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
 
-// Module: sphere_copies()
-//
-// Description:
-//   Spreads children semi-evenly over the surface of a sphere or ellipsoid.
+
+
+// Function&Module: sphere_copies()
 //
 // Usage:
 //   sphere_copies(n, r|d=, [cone_ang=], [scale=], [perp=]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = sphere_copies(n, r|d=, [cone_ang=], [scale=], [perp=], p=);
+// Usage: Get Translation Matrices
+//   mats = sphere_copies(n, r|d=, [cone_ang=], [scale=], [perp=]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, spreads children semi-evenly over the surface of a sphere or ellipsoid.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   n = How many copies to evenly spread over the surface.
@@ -853,6 +1197,7 @@ module arc_copies(
 //   cone_ang = Angle of the cone, in degrees, to limit how much of the sphere gets covered.  For full sphere coverage, use 180.  Measured pre-scaling.  Default: 180
 //   scale = The [X,Y,Z] scaling factors to reshape the sphere being covered.
 //   perp = If true, rotate children to be perpendicular to the sphere surface.  Default: true
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$pos` is set to the relative post-scaled centerpoint of each child copy, and can be used to modify each child individually.
@@ -861,6 +1206,8 @@ module arc_copies(
 //   `$rad` is set to the pre-scaled radial distance of the child from the center of the sphere.
 //   `$idx` is set to the index number of each child being copied.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   sphere_copies(n=250, d=100, cone_ang=45, scale=[3,3,1])
 //       cylinder(d=10, h=10, center=false);
@@ -869,13 +1216,14 @@ module arc_copies(
 //   sphere_copies(n=500, d=100, cone_ang=180)
 //       color(unit(point3d(v_abs($pos))))
 //           cylinder(d=8, h=10, center=false);
-function sphere_copies(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=true) = no_function("sphere_copies");
+
 module ovoid_spread(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=true)
 {
   deprecate("sphere_copies");
   sphere_copies(n,r,d,cone_ang,scale,perp);
 }  
 
+
 module sphere_copies(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=true)
 {
     req_children($children);  
@@ -904,23 +1252,59 @@ module sphere_copies(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=t
     }
 }
 
+
+function sphere_copies(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=true, p=_NO_ARG) =
+    let(
+        r = get_radius(r=r, d=d, dflt=50),
+        cnt = ceil(n / (cone_ang/180)),
+
+        // Calculate an array of [theta,phi] angles for `n` number of
+        // points, almost evenly spaced across the surface of a sphere.
+        // This approximation is based on the golden spiral method.
+        theta_phis = [for (x=[0:1:n-1]) [180*(1+sqrt(5))*(x+0.5)%360, acos(1-2*(x+0.5)/cnt)]],
+
+        mats = [
+            for (tp = theta_phis)
+            let(
+                xyz = spherical_to_xyz(r, tp[0], tp[1]),
+                pos = v_mul(xyz,point3d(scale,1))
+            )
+            translate(pos) *
+            (perp? rot(from=UP, to=xyz) : ident(4))
+        ]
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
+
+
+
 // Section: Placing copies of all children on a path
 
 
-// Module: path_copies()
-//
-// Description:
-//   Place copies all of the children at points along the path based on path length.  You can specify `dist` as
-//   a scalar or distance list and the children will be placed at the specified distances from the start of the path.  Otherwise the children are
-//   placed at uniformly spaced points along the path.  If you specify `n` but not `spacing` then `n` copies will be placed
-//   with one at path[0] if `closed` is true, or spanning the entire path from start to end if `closed` is false.
-//   If you specify `spacing` but not `n` then copies will spread out starting from one set at path[0] for `closed=true` or at the path center for open paths.
-//   If you specify `sp` then the copies will start at distance `sp` from the start of the path.  
+// Function&Module: path_copies()
 //
 // Usage: Uniformly distribute copies 
 //   path_copies(path, [n], [spacing], [sp], [rotate_children], [closed=]) CHILDREN;
 // Usage: Place copies at specified locations
 //   path_copies(path, dist=, [rotate_children=], [closed=]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = path_copies(path, [n], [spacing], [sp], [rotate_children], [closed=], p=);
+//   copies = path_copies(path, dist=, [rotate_children=], [closed=], p=);
+// Usage: Get Translation Matrices
+//   mats = path_copies(path, [n], [spacing], [sp], [rotate_children], [closed=]);
+//   mats = path_copies(path, dist=, [rotate_children=], [closed=]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module:
+//   - Place copies all of the children at points along the path based on path length.  You can specify `dist` as
+//   - a scalar or distance list and the children will be placed at the specified distances from the start of the path.  Otherwise the children are
+//   - placed at uniformly spaced points along the path.  If you specify `n` but not `spacing` then `n` copies will be placed
+//   - with one at path[0] if `closed` is true, or spanning the entire path from start to end if `closed` is false.
+//   - If you specify `spacing` but not `n` then copies will spread out starting from one set at path[0] for `closed=true` or at the path center for open paths.
+//   - If you specify `sp` then the copies will start at distance `sp` from the start of the path.  
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   path = path or 1-region where children are placed
@@ -931,6 +1315,7 @@ module sphere_copies(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=t
 //   ---
 //   dist = Specify a list of distances to determine placement of children.  
 //   closed = If true treat path as a closed curve.  Default: false
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$pos` is set to the center of each copy
@@ -938,6 +1323,8 @@ module sphere_copies(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=t
 //   `$dir` is set to the direction vector of the path at the point where the copy is placed.
 //   `$normal` is set to the direction of the normal vector to the path direction that is coplanar with the path at this point
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example(2D):
 //   spiral = [for(theta=[0:360*8]) theta * [cos(theta), sin(theta)]]/100;
 //   stroke(spiral,width=.25);
@@ -1002,13 +1389,13 @@ module sphere_copies(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=t
 //      color("blue") cyl(h=3,r=.2, anchor=BOTTOM);       // z-aligned cylinder
 //      color("red") xcyl(h=10,r=.2, anchor=FRONT+LEFT);  // x-aligned cylinder
 //   }
-function path_copies(path, n, spacing, sp=undef, rotate_children=true, dist, closed) = no_function("path_copies");
 
 module path_spread(path, n, spacing, sp=undef, rotate_children=true, dist, closed){
   deprecate("path_copes");
   path_copies(path,n,spacing,sp,dist,rotate_children,dist, closed) children();
 }  
 
+
 module path_copies(path, n, spacing, sp=undef, dist, rotate_children=true, dist, closed)
 {
     req_children($children);  
@@ -1060,28 +1447,81 @@ module path_copies(path, n, spacing, sp=undef, dist, rotate_children=true, dist,
 }
 
 
+function path_copies(path, n, spacing, sp=undef, dist, rotate_children=true, dist, closed, p=_NO_ARG) =
+    let(
+        is_1reg = is_1region(path),
+        path = is_1reg ? path[0] : path,
+        closed = default(closed, is_1reg),
+        length = path_length(path,closed),
+        distind = is_def(dist) ? sortidx(dist) : undef,
+        distances =
+            is_def(dist) ? assert(is_undef(n) && is_undef(spacing) && is_undef(sp), "Can't use n, spacing or undef with dist")
+                           select(dist,distind)
+          : is_def(sp)? (   // Start point given
+                is_def(n) && is_def(spacing)? count(n,sp,spacing) :
+                is_def(n)? lerpn(sp, length, n) :
+                list([sp:spacing:length])
+            )
+          : is_def(n) && is_undef(spacing)? lerpn(0,length,n,!closed) // N alone given
+          : (      // No start point and spacing is given, N maybe given
+            let(
+                n = is_def(n)? n : floor(length/spacing)+(closed?0:1),
+                ptlist = count(n,0,spacing),
+                listcenter = mean(ptlist)
+            ) closed?
+                sort([for(entry=ptlist) posmod(entry-listcenter,length)]) :
+                [for(entry=ptlist) entry + length/2-listcenter ]
+          ),
+        distOK = min(distances)>=0 && max(distances)<=length,
+        dummy = assert(distOK,"Cannot fit all of the copies"),
+        cutlist = path_cut_points(path, distances, closed, direction=true),
+        planar = len(path[0])==2,
+        mats = [
+            for(i=[0:1:len(cutlist)-1])
+            translate(cutlist[i][0]) * (
+                !rotate_children? ident(4) :
+                planar? rot(from=[0,1],to=cutlist[i][3]) :
+                frame_map(x=cutlist[i][2], z=cutlist[i][3])
+            )
+        ]
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
+
+
 
 //////////////////////////////////////////////////////////////////////
 // Section: Making a copy of all children with reflection
 //////////////////////////////////////////////////////////////////////
 
 
-// Module: xflip_copy()
-//
-// Description:
-//   Makes a copy of the children, mirrored across the X axis.
+// Function&Module: xflip_copy()
 //
 // Usage:
 //   xflip_copy([offset], [x]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = xflip_copy([offset], [x], p=);
+// Usage: Get Translation Matrices
+//   mats = xflip_copy([offset], [x]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, makes a copy of the children, mirrored across the X axis.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   offset = Distance to offset children right, before copying.
 //   x = The X coordinate of the mirroring plane.  Default: 0
+//   ---
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$orig` is true for the original instance of children.  False for the copy.
 //   `$idx` is set to the index value of each copy.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   xflip_copy() yrot(90) cylinder(h=20, r1=4, r2=0);
 //   color("blue",0.25) cube([0.01,15,15], center=true);
@@ -1100,22 +1540,38 @@ module xflip_copy(offset=0, x=0)
 }
 
 
-// Module: yflip_copy()
-//
-// Description:
-//   Makes a copy of the children, mirrored across the Y axis.
+function xflip_copy(offset=0, x=0, p=_NO_ARG) =
+    mirror_copy(v=[1,0,0], offset=offset, cp=[x,0,0], p=p);
+
+
+// Function&Module: yflip_copy()
 //
 // Usage:
 //   yflip_copy([offset], [y]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = yflip_copy([offset], [y], p=);
+// Usage: Get Translation Matrices
+//   mats = yflip_copy([offset], [y]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, makes a copy of the children, mirrored across the Y axis.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   offset = Distance to offset children back, before copying.
 //   y = The Y coordinate of the mirroring plane.  Default: 0
+//   ---
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$orig` is true for the original instance of children.  False for the copy.
 //   `$idx` is set to the index value of each copy.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   yflip_copy() xrot(-90) cylinder(h=20, r1=4, r2=0);
 //   color("blue",0.25) cube([15,0.01,15], center=true);
@@ -1134,22 +1590,38 @@ module yflip_copy(offset=0, y=0)
 }
 
 
-// Module: zflip_copy()
-//
-// Description:
-//   Makes a copy of the children, mirrored across the Z axis.
+function yflip_copy(offset=0, y=0, p=_NO_ARG) =
+    mirror_copy(v=[0,1,0], offset=offset, cp=[0,y,0], p=p);
+
+
+// Function&Module: zflip_copy()
 //
 // Usage:
 //   zflip_copy([offset], [z]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = zflip_copy([offset], [z], p=);
+// Usage: Get Translation Matrices
+//   mats = zflip_copy([offset], [z]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, makes a copy of the children, mirrored across the Z axis.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   offset = Distance to offset children up, before copying.
 //   z = The Z coordinate of the mirroring plane.  Default: 0
+//   ---
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$orig` is true for the original instance of children.  False for the copy.
 //   `$idx` is set to the index value of each copy.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   zflip_copy() cylinder(h=20, r1=4, r2=0);
 //   color("blue",0.25) cube([15,15,0.01], center=true);
@@ -1168,23 +1640,39 @@ module zflip_copy(offset=0, z=0)
 }
 
 
-// Module: mirror_copy()
-//
-// Description:
-//   Makes a copy of the children, mirrored across the given plane.
+function zflip_copy(offset=0, z=0, p=_NO_ARG) =
+    mirror_copy(v=[0,0,1], offset=offset, cp=[0,0,z], p=p);
+
+
+// Function&Module: mirror_copy()
 //
 // Usage:
 //   mirror_copy(v, [cp], [offset]) CHILDREN;
+// Usage: As a function to translate points, VNF, or Bezier patches
+//   copies = mirror_copy(v, [cp], [offset], p=);
+// Usage: Get Translation Matrices
+//   mats = mirror_copy(v, [cp], [offset]);
+//
+// Topics: Transformations, Distributors, Copiers
+//
+// Description:
+//   When called as a module, makes a copy of the children, mirrored across the given plane.
+//   When called as a function, *without* a `p=` argument, returns a list of transformation matrices, one for each copy.
+//   When called as a function, *with* a `p=` argument, returns a list of transformed copies of `p=`.
 //
 // Arguments:
 //   v = The normal vector of the plane to mirror across.
 //   offset = distance to offset away from the plane.
 //   cp = A point that lies on the mirroring plane.
+//   ---
+//   p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
 //
 // Side Effects:
 //   `$orig` is true for the original instance of children.  False for the copy.
 //   `$idx` is set to the index value of each copy.
 //
+// See Also: move_copies(), xcopies(), ycopies(), zcopies(), line_copies(), rot_copies(), xrot_copies(), yrot_copies(), zrot_copies(), arc_copies(), sphere_copies()
+//
 // Example:
 //   mirror_copy([1,-1,0]) zrot(-45) yrot(90) cylinder(d1=10, d2=0, h=20);
 //   color("blue",0.25) zrot(-45) cube([0.01,15,15], center=true);
@@ -1223,6 +1711,25 @@ module mirror_copy(v=[0,0,1], offset=0, cp)
 }
 
 
+function mirror_copy(v=[0,0,1], offset=0, cp, p=_NO_ARG) =
+    let(
+        cp = is_vector(v,4)? plane_normal(v) * v[3] :
+            is_vector(cp)? cp :
+            is_num(cp)? cp*unit(v) :
+            [0,0,0],
+        nv = is_vector(v,4)? plane_normal(v) : unit(v),
+        off = nv*offset,
+        mats = [
+            translate(off),
+            translate(cp) *
+                mirror(nv) *
+                translate(-cp) *
+                translate(off)
+        ]
+    )
+    p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
+
+
 
 ////////////////////
 // Section: Distributing children individually along a line
@@ -1414,6 +1921,4 @@ module distribute(spacing=undef, sizes=undef, dir=RIGHT, l=undef)
 
 
 
-
-
 // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap
diff --git a/transforms.scad b/transforms.scad
index 91e5a73..cba5f97 100644
--- a/transforms.scad
+++ b/transforms.scad
@@ -76,7 +76,7 @@ _NO_ARG = [true,[123232345],false];
 //
 // Usage: As Module
 //   move(v) CHILDREN;
-// Usage: As a function to translate points, VNF, or Bezier patch
+// Usage: As a function to translate points, VNF, or Bezier patches
 //   pts = move(v, p);
 //   pts = move(STRING, p);
 // Usage: Get Translation Matrix

From a98b2a833f88d81ba3a9289274274352e4139973 Mon Sep 17 00:00:00 2001
From: Revar Desmera <revarbat@gmail.com>
Date: Wed, 18 Jan 2023 01:18:17 -0800
Subject: [PATCH 3/6] Fix for line_copies() test.

---
 tests/test_distributors.scad | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/tests/test_distributors.scad b/tests/test_distributors.scad
index 2c95740..d5f4537 100644
--- a/tests/test_distributors.scad
+++ b/tests/test_distributors.scad
@@ -2,15 +2,15 @@ include <../std.scad>
 
 
 module test_line_copies() {
-    assert_equal(line_copies(l=100,n=5), [[-50,0,0],[-25,0,0],[0,0,0],[25,0,0],[50,0,0]]);
-    assert_equal(line_copies(20,n=5), [[-40,0,0],[-20,0,0],[0,0,0],[20,0,0],[40,0,0]]);
-    assert_equal(line_copies(spacing=20,n=5), [[-40,0,0],[-20,0,0],[0,0,0],[20,0,0],[40,0,0]]);
-    assert_equal(line_copies(spacing=[0,20],n=5), [[0,-40,0],[0,-20,0],[0,0,0],[0,20,0],[0,40,0]]);
+    assert_equal(line_copies(l=100,n=5,p=[0,0,0]), [[-50,0,0],[-25,0,0],[0,0,0],[25,0,0],[50,0,0]]);
+    assert_equal(line_copies(20,n=5,p=[0,0,0]), [[-40,0,0],[-20,0,0],[0,0,0],[20,0,0],[40,0,0]]);
+    assert_equal(line_copies(spacing=20,n=5,p=[0,0,0]), [[-40,0,0],[-20,0,0],[0,0,0],[20,0,0],[40,0,0]]);
+    assert_equal(line_copies(spacing=[0,20],n=5,p=[0,0,0]), [[0,-40,0],[0,-20,0],[0,0,0],[0,20,0],[0,40,0]]);
 
-    assert_equal(line_copies(p1=[0,0],l=100,n=5), [[0,0,0],[25,0,0],[50,0,0],[75,0,0],[100,0,0]]);
-    assert_equal(line_copies(p1=[0,0],20,n=5), [[0,0,0],[20,0,0],[40,0,0],[60,0,0],[80,0,0]]);
-    assert_equal(line_copies(p1=[0,0],spacing=20,n=5), [[0,0,0],[20,0,0],[40,0,0],[60,0,0],[80,0,0]]);
-    assert_equal(line_copies(p1=[0,0],spacing=[0,20],n=5), [[0,0,0],[0,20,0],[0,40,0],[0,60,0],[0,80,0]]);
+    assert_equal(line_copies(p1=[0,0],l=100,n=5,p=[0,0,0]), [[0,0,0],[25,0,0],[50,0,0],[75,0,0],[100,0,0]]);
+    assert_equal(line_copies(p1=[0,0],20,n=5,p=[0,0,0]), [[0,0,0],[20,0,0],[40,0,0],[60,0,0],[80,0,0]]);
+    assert_equal(line_copies(p1=[0,0],spacing=20,n=5,p=[0,0,0]), [[0,0,0],[20,0,0],[40,0,0],[60,0,0],[80,0,0]]);
+    assert_equal(line_copies(p1=[0,0],spacing=[0,20],n=5,p=[0,0,0]), [[0,0,0],[0,20,0],[0,40,0],[0,60,0],[0,80,0]]);
 }
 test_line_copies();
 

From cab3eaf37fcab02bb1ead8e2280f21779eeca4c4 Mon Sep 17 00:00:00 2001
From: Revar Desmera <revarbat@gmail.com>
Date: Wed, 18 Jan 2023 02:33:10 -0800
Subject: [PATCH 4/6] Distributors bugfixes.

---
 distributors.scad | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/distributors.scad b/distributors.scad
index 02a8bb3..2a5e2aa 100644
--- a/distributors.scad
+++ b/distributors.scad
@@ -418,13 +418,13 @@ function zcopies(spacing, n, l, sp, p=_NO_ARG) =
 
 module line_of(spacing, n, l, p1, p2) {
     deprecate("line_copies");
-    line_copies(spacing, n, l, p1, p2);
+    line_copies(spacing, n, l, p1, p2) children();
 }
 
 module line_copies(spacing, n, l, p1, p2)
 {
     req_children($children);
-    pts = line_copies(spacing=spacing, n=n, l=l, p1=p1, p2=p2);
+    pts = line_copies(spacing=spacing, n=n, l=l, p1=p1, p2=p2, p=[0,0,0]);
     for (i=idx(pts)) {
         $idx = i;
         $pos = pts[i];
@@ -801,10 +801,10 @@ function rot_copies(rots=[], v, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0], su
         mats = [
             for (ang = angs)
             translate(cp) *
-            rot(a=ang, v=v) *
-            translate(delta) *
-            rot(a=(subrot? sang : ang), v=v, reverse=true) *
-            translate(-cp)
+                rot(a=ang, v=v) *
+                translate(delta) *
+                rot(a=(subrot? sang : ang), v=v, reverse=true) *
+                translate(-cp)
         ]
     )
     p==_NO_ARG? mats : [for (m = mats) apply(m, p)];
@@ -1112,7 +1112,7 @@ function zrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG)
 
 module arc_of(n=6,r,rx,ry,d,dx,dy,sa=0,ea=360,rot=true){
     deprecate("arc_copies");
-    arc_copies(n,r,rx,ry,d,dx,dy,sa,ea,rot);
+    arc_copies(n,r,rx,ry,d,dx,dy,sa,ea,rot) children();
 }    
 
 
@@ -1220,7 +1220,7 @@ function arc_copies(
 module ovoid_spread(n=100, r=undef, d=undef, cone_ang=90, scale=[1,1,1], perp=true)
 {
   deprecate("sphere_copies");
-  sphere_copies(n,r,d,cone_ang,scale,perp);
+  sphere_copies(n,r,d,cone_ang,scale,perp) children();
 }  
 
 

From 885a5fef9b11b62e3fa791f6b6f0a32c6ad9fb80 Mon Sep 17 00:00:00 2001
From: Revar Desmera <revarbat@gmail.com>
Date: Wed, 18 Jan 2023 03:14:27 -0800
Subject: [PATCH 5/6] line_copies() matrix return example fix.

---
 distributors.scad | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/distributors.scad b/distributors.scad
index 2a5e2aa..89693c7 100644
--- a/distributors.scad
+++ b/distributors.scad
@@ -412,8 +412,11 @@ function zcopies(spacing, n, l, sp, p=_NO_ARG) =
 //       cube(size=[1,3,1],center=true);
 //       cube(size=[3,1,1],center=true);
 //   }
-// Example(2D): The functional form of line_copies() returns a list of points.
-//   pts = line_copies([10,5],n=5);
+// Example(2D): The functional form of line_copies() returns a list of transform matrices.
+//   mats = line_copies([10,5],n=5);
+//   for (m = mats) multmatrix(m)  circle(d=2);
+// Example(2D): The functional form of line_copies() returns a list of points if given a point.
+//   pts = line_copies([10,5],n=5,p=[0,0,0]);
 //   move_copies(pts) circle(d=2);
 
 module line_of(spacing, n, l, p1, p2) {

From 6602ff3eee4eeef61abe1bbcf8fa26ad4e7cd2ff Mon Sep 17 00:00:00 2001
From: Revar Desmera <revarbat@gmail.com>
Date: Mon, 23 Jan 2023 15:01:14 -0800
Subject: [PATCH 6/6] Get Textures Figures to render.

---
 skin.scad | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/skin.scad b/skin.scad
index 8a9ef29..3e9a7da 100644
--- a/skin.scad
+++ b/skin.scad
@@ -2585,9 +2585,15 @@ function associate_vertices(polygons, split, curpoly=0) =
 //   Another serious limitation is more subtle.  In the 2D examples above, it is obvious how to connect the
 //   dots together.  But in 3D example we need to triangulate the points on a grid, and this triangulation is not unique.
 //   The `style` argument lets you specify how the points are triangulated using the styles supported by {{vnf_vertex_array()}}.
-//   In the example below we have expanded the 2D example into 3D: [[0,0,0,0],[0,1,1,0],[0,1,1,0],[0,0,0,0]], and we show the
-//   3D triangulations produced by the different styles:
-// Figure(Big, NoAxes, VPR = [39.2, 0, 13.3], VPT = [3.76242, -5.50969, 4.51854], VPD = 32.0275):
+//   In the example below we have expanded the 2D example into 3D:
+//   ```openscad
+//       [[0,0,0,0],
+//        [0,1,1,0],
+//        [0,1,1,0],
+//        [0,0,0,0]]
+//   ```
+//   and we show the 3D triangulations produced by the different styles:
+// Figure(3D,Big,NoAxes,VPR=[39.2,0,13.3],VPT=[3.76242,-5.50969,4.51854],VPD=32.0275):
 //   tex = [
 //          [0,0,0,0,0],
 //          [0,1,1,0,0],
@@ -2628,10 +2634,10 @@ function associate_vertices(polygons, split, curpoly=0) =
 //   to make a valid object is to have no points at all on the Y=0 line, and of course none on Y=1.  In this case, the resulting texture produces
 //   a collection of disconnected objects.  Note that the Z coordinates of your tile can be anything, but for the dimensional settings on textures
 //   to work intuitively, you should construct your tile so that Z ranges from 0 to 1.
-// Figure: This is the "hexgrid" VNF tile, which creates a hexagonal grid texture, something which doesn't work well with a height field because the edges of the hexagon don't align with the grid.  Note how the tile ranges between 0 and 1 in both X, Y and Z.
+// Figure(3D): This is the "hexgrid" VNF tile, which creates a hexagonal grid texture, something which doesn't work well with a height field because the edges of the hexagon don't align with the grid.  Note how the tile ranges between 0 and 1 in both X, Y and Z.
 //   tex = texture("hex_grid");
 //   vnf_polyhedron(tex);
-// Figure: This is an example of a tile that has no edges at the top or bottom, so it creates disconnected rings.  See below for examples showing this tile in use.
+// Figure(3D): This is an example of a tile that has no edges at the top or bottom, so it creates disconnected rings.  See below for examples showing this tile in use.
 //   shape = skin([
 //                 rect(2/5),
 //                 rect(2/3),