diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..6ca207e
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,122 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4483c8b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,94 @@
+# OpenSCAD ClosePoints Library
+
+

+
+This is a general purpose OpenSCAD library for easily creating diverse shapes
+by simply creating lists of points which trace out layers in an outline of the
+desired shape. The library consists of modules for creating polyhedrons from
+these lists of points, as well as functions to assist in specifying the points
+using transformations.
+
+The file names starting with "demo" provide various examples of usage.
+
+# closepoints.scad API
+
+ClosePoints and CloseLoop are the two modules for creating a polyhedron.
+ClosePoints is for creating a polyhedron with no holes (e.g., a ball, or a
+cup), while CloseLoop is for creating a polyhedron which topologically contains
+one hole (e.g., a donut). To achieve this difference, ClosePoints auto-closes
+the top and bottom of the provided layer loops, while CloseLoop connects the
+last layer loop to the first layer loop to close the polyhedron.
+
+Following these are a number of functions for working with affine
+transformations, which can help significantly in tracing out the surface layers
+of a desired polyhedron.
+
+The API is as follows:
+
+
+```
+// This generates a closed polyhedron from an array of arrays of points,
+// with each inner array tracing out one loop outlining the polyhedron.
+// pointarrays should contain an array of N arrays each of size P outlining
+// a closed manifold. The points must obey the right-hand rule. Point your
+// right-hand thumb in the direction the N point arrays travel, and then the
+// P points in the inner arrays must loop in the direction the fingers curl.
+// For example, looking down, the P points in the inner arrays are
+// counter-clockwise in a loop, while the N point arrays increase in height.
+// Points in each inner array do not need to be equal height, but they
+// usually should not meet or cross the line segments from the adjacent
+// points in the other arrays.
+// (N>=2, P>=3)
+// Core triangles:
+// [j][i], [j+1][i], [j+1][(i+1)%P]
+// [j][i], [j+1][(i+1)%P], [j][(i+1)%P]
+// Then triangles are formed in a loop with the middle point of the first
+// and last array. To override this middle closure point, specify a
+// coordinate position for close_top_pt and/or close_bot_pt.
+module ClosePoints(pointarrays, close_top_pt=undef, close_bot_pt=undef)
+
+// This generates a looped polyhedron from an array of arrays of points, with
+// each inner array tracing out one layer loop outlining the polyhedron.
+// pointarrays should contain an array of N arrays each of size P outlining a
+// closed manifold. The points must obey the right-hand rule. For example,
+// looking down, the P points in the inner arrays are counter-clockwise in a
+// loop, while the N point arrays increase in height. Points in each inner
+// array do not need to be equal height, but they usually should not meet or
+// cross the line segments from the adjacent points in the other arrays. The
+// last layer loop should geometrically lead into the first when it is closed.
+// (N>=2, P>=3)
+// Core triangles:
+// [j][i], [j+1][i], [j+1][(i+1)%P]
+// [j][i], [j+1][(i+1)%P], [j][(i+1)%P]
+module CloseLoop(pointarrays)
+
+// Perform an affine transformation of matrix M on coordinate v.
+//
+// [Scale X] [Shear X along Y] [Shear X along Z] [Translate X]
+// [Shear Y along X] [Scale Y] [Shear Y along Z] [Translate Y]
+// [Shear Z along X] [Shear Z along Y] [Scale Z] [Translate Z]
+// or rotation matrix [[cos,-sin],[sin,cos]] in the 2 axes for a plane.
+function Affine(M, v)
+
+// Combine a list of affine transformation matrices into one.
+function AffMerge(Mlist)
+
+// Prepare a matrix to rotate around the x-axis.
+function RotX(a)
+
+// Prepare a matrix to rotate around the y-axis.
+function RotY(a)
+
+// Prepare a matrix to rotate around the z-axis.
+function RotZ(a)
+
+// Prepare a matrix to rotate around x, then y, then z.
+function Rotate(rotvec)
+
+// Prepare a matrix to translate by vector v.
+function Translate(v)
+
+// Prepare a matrix to scale by vector v.
+function Scale(v)
+```
+
diff --git a/closepoints.scad b/closepoints.scad
new file mode 100644
index 0000000..fb9419d
--- /dev/null
+++ b/closepoints.scad
@@ -0,0 +1,161 @@
+// Created 2021 by Ryan A. Colyer.
+// This work is released with CC0 into the public domain.
+// https://creativecommons.org/publicdomain/zero/1.0/
+
+
+// This generates a closed polyhedron from an array of arrays of points,
+// with each inner array tracing out one loop outlining the polyhedron.
+// pointarrays should contain an array of N arrays each of size P outlining
+// a closed manifold. The points must obey the right-hand rule. Point your
+// right-hand thumb in the direction the N point arrays travel, and then the
+// P points in the inner arrays must loop in the direction the fingers curl.
+// For example, looking down, the P points in the inner arrays are
+// counter-clockwise in a loop, while the N point arrays increase in height.
+// Points in each inner array do not need to be equal height, but they
+// usually should not meet or cross the line segments from the adjacent
+// points in the other arrays.
+// (N>=2, P>=3)
+// Core triangles:
+// [j][i], [j+1][i], [j+1][(i+1)%P]
+// [j][i], [j+1][(i+1)%P], [j][(i+1)%P]
+// Then triangles are formed in a loop with the middle point of the first
+// and last array. To override this middle closure point, specify a
+// coordinate position for close_top_pt and/or close_bot_pt.
+module ClosePoints(pointarrays, close_top_pt=undef, close_bot_pt=undef) {
+ function recurse_avg(arr, n=0, p=[0,0,0]) = (n>=len(arr)) ? p :
+ recurse_avg(arr, n+1, p+(arr[n]-p)/(n+1));
+
+ N = len(pointarrays);
+ P = len(pointarrays[0]);
+ NP = N*P;
+ midbot = is_undef(close_bot_pt) ?
+ recurse_avg(pointarrays[0]) :
+ close_bot_pt;
+ midtop = is_undef(close_top_pt) ?
+ recurse_avg(pointarrays[N-1]) :
+ close_top_pt;
+
+ faces_bot = [
+ for (i=[0:P-1])
+ [0,i+1,1+(i+1)%len(pointarrays[0])]
+ ];
+
+ loop_offset = 1;
+
+ faces_loop = [
+ for (j=[0:N-2], i=[0:P-1], t=[0:1])
+ [loop_offset, loop_offset, loop_offset] + (t==0 ?
+ [j*P+i, (j+1)*P+i, (j+1)*P+(i+1)%P] :
+ [j*P+i, (j+1)*P+(i+1)%P, j*P+(i+1)%P])
+ ];
+
+ top_offset = loop_offset + NP - P;
+ midtop_offset = top_offset + P;
+
+ faces_top = [
+ for (i=[0:P-1])
+ [midtop_offset,top_offset+(i+1)%P,top_offset+i]
+ ];
+
+ points = [
+ for (i=[-1:NP])
+ (i<0) ? midbot :
+ ((i==NP) ? midtop :
+ pointarrays[floor(i/P)][i%P])
+ ];
+ faces = concat(faces_bot, faces_loop, faces_top);
+
+ polyhedron(points=points, faces=faces, convexity=8);
+}
+
+
+// This generates a looped polyhedron from an array of arrays of points, with
+// each inner array tracing out one layer loop outlining the polyhedron.
+// pointarrays should contain an array of N arrays each of size P outlining a
+// closed manifold. The points must obey the right-hand rule. For example,
+// looking down, the P points in the inner arrays are counter-clockwise in a
+// loop, while the N point arrays increase in height. Points in each inner
+// array do not need to be equal height, but they usually should not meet or
+// cross the line segments from the adjacent points in the other arrays. The
+// last layer loop should geometrically lead into the first when it is closed.
+// (N>=2, P>=3)
+// Core triangles:
+// [j][i], [j+1][i], [j+1][(i+1)%P]
+// [j][i], [j+1][(i+1)%P], [j][(i+1)%P]
+module CloseLoop(pointarrays) {
+ function recurse_avg(arr, n=0, p=[0,0,0]) = (n>=len(arr)) ? p :
+ recurse_avg(arr, n+1, p+(arr[n]-p)/(n+1));
+
+ N = len(pointarrays);
+ P = len(pointarrays[0]);
+ NP = N*P;
+
+ faces_loop = [
+ for (j=[0:N-1], i=[0:P-1], t=[0:1])
+ t==0 ?
+ [j*P+i, ((j+1)%N)*P+i, ((j+1)%N)*P+(i+1)%P] :
+ [j*P+i, ((j+1)%N)*P+(i+1)%P, j*P+(i+1)%P]
+ ];
+
+ points = [
+ for (i=[0:NP-1])
+ pointarrays[floor(i/P)][i%P]
+ ];
+
+ polyhedron(points=points, faces=faces_loop, convexity=8);
+}
+
+
+// Perform an affine transformation of matrix M on coordinate v.
+//
+// [Scale X] [Shear X along Y] [Shear X along Z] [Translate X]
+// [Shear Y along X] [Scale Y] [Shear Y along Z] [Translate Y]
+// [Shear Z along X] [Shear Z along Y] [Scale Z] [Translate Z]
+// or rotation matrix [[cos,-sin],[sin,cos]] in the 2 axes for a plane.
+function Affine(M, v) = M * [v[0], v[1], v[2], 1];
+
+
+// Combine a list of affine transformation matrices into one.
+function AffMerge(Mlist, i=0) = i >= len(Mlist) ?
+ [[1,0,0,0],[0,1,0,0],[0,0,1,0]] :
+ let (
+ rest = AffMerge(Mlist, i+1),
+ prod = Mlist[i] * [rest[0], rest[1], rest[2], [0,0,0,1]]
+ )
+ [prod[0], prod[1], prod[2]];
+
+
+// Prepare a matrix to rotate around the x-axis.
+function RotX(a) =
+ [[ 1, 0, 0, 0],
+ [ 0, cos(a), -sin(a), 0],
+ [ 0, sin(a), cos(a), 0]];
+
+// Prepare a matrix to rotate around the y-axis.
+function RotY(a) =
+ [[ cos(a), 0, sin(a), 0],
+ [ 0, 1, 0, 0],
+ [-sin(a), 0, cos(a), 0]];
+
+// Prepare a matrix to rotate around the z-axis.
+function RotZ(a) =
+ [[cos(a), -sin(a), 0, 0],
+ [sin(a), cos(a), 0, 0],
+ [ 0, 0, 1, 0]];
+
+// Prepare a matrix to rotate around x, then y, then z.
+function Rotate(rotvec) =
+ AffMerge([RotZ(rotvec[0]), RotY(rotvec[1]), RotX(rotvec[2])]);
+
+// Prepare a matrix to translate by vector v.
+function Translate(v) =
+ [[1, 0, 0, v[0]],
+ [0, 1, 0, v[1]],
+ [0, 0, 1, v[2]]];
+
+// Prepare a matrix to scale by vector v.
+function Scale(v) =
+ [[v[0], 0, 0, 0],
+ [ 0, v[1], 0, 0],
+ [ 0, 0, v[2], 0]];
+
diff --git a/demo_3D_art.scad b/demo_3D_art.scad
new file mode 100644
index 0000000..263ae32
--- /dev/null
+++ b/demo_3D_art.scad
@@ -0,0 +1,57 @@
+// Created in 2018 by Ryan A. Colyer.
+// This work is released with CC0 into the public domain.
+// https://creativecommons.org/publicdomain/zero/1.0/
+
+use
+
+layer_step = 0.2;
+
+pedestal_top = 60;
+pedestal_ripple = 1;
+pedestal_ripple_rad = 4;
+pedestal_base_rad = 20;
+pedestal_base_thickness = 10;
+pedestal_connect_rad = 1.5;
+
+art_top = 30;
+art_radial_ripple = 1;
+art_height_ripple = 0.2;
+art_ztilt_freq = 3;
+art_ztilt_fact = 1;
+art_radtilt_freq = 1;
+art_radtilt_fact = 1;
+
+function OneLayer(rad, ztilt, h_off) =
+ [
+ for (a=[0:360])
+ [ rad*cos(a),
+ rad*sin(a),
+ art_radtilt_fact*rad*sin(art_radtilt_freq*a)
+ + art_ztilt_fact*ztilt*(cos(art_ztilt_freq*a)+1) + h_off
+ ]
+ ];
+
+artpointarrays =
+ [for (h=[0:layer_step:art_top])
+ OneLayer(
+ h*abs(sin(art_radial_ripple*360/art_top)+1.1) + pedestal_connect_rad,
+ h,
+ pedestal_top+pedestal_connect_rad
+ )
+ ];
+
+
+function Pedestal(h) =
+ let(
+ r = pedestal_base_rad*exp(-h*h/pedestal_base_thickness) +
+ pedestal_ripple_rad*pow(sin(h*pedestal_ripple*180/pedestal_top),2) +
+ pedestal_connect_rad
+ )
+ [ for (a=[0:360])
+ [ r*cos(a), r*sin(a), h ]
+ ];
+
+basepointarrays = [for (h=[0:layer_step:pedestal_top]) Pedestal(h)];
+
+ClosePoints(concat(basepointarrays, artpointarrays));
+
diff --git a/demo_gear_thing.scad b/demo_gear_thing.scad
new file mode 100644
index 0000000..5346333
--- /dev/null
+++ b/demo_gear_thing.scad
@@ -0,0 +1,28 @@
+// Created in 2021 by Ryan A. Colyer.
+// This work is released with CC0 into the public domain.
+// https://creativecommons.org/publicdomain/zero/1.0/
+
+
+use
+
+
+function GearRotXY(t) = RotZ(360*t);
+function GearRotXZ(t) = RotY(8*360*t);
+function Ellipse(t) = Translate([50*cos(360*t), 40*sin(360*t), 0]);
+
+function PathMatrix(t) = AffMerge([Ellipse(t), GearRotXY(t), GearRotXZ(t)]);
+
+function ThePolygon(t) =
+ [for (a=[0:2:359.99])
+ ((a < 6 || (a >= 180 && a < 186)) ? 7 : 5)*[cos(a), 0, -sin(a)]
+ ];
+
+pointarrays =
+ [for (t=[0:0.002:0.99999])
+ [for (p=ThePolygon(t))
+ Affine(PathMatrix(t), p)
+ ]
+ ];
+
+CloseLoop(pointarrays);
+
diff --git a/demo_roller_coaster.scad b/demo_roller_coaster.scad
new file mode 100644
index 0000000..433a03d
--- /dev/null
+++ b/demo_roller_coaster.scad
@@ -0,0 +1,30 @@
+// Created in 2021 by Ryan A. Colyer.
+// This work is released with CC0 into the public domain.
+// https://creativecommons.org/publicdomain/zero/1.0/
+
+use
+
+
+function RotZt(t) = RotZ(360*t);
+
+function hillf(t) = sin(-2*t*360-115)+sin(-3*t*360-57)+2;
+function Hills(t) = Translate([0, 0, 20*hillf(t)]);
+
+function RotXZ(t) = let(a = -45*(4 - hillf(t))/4) RotY(a);
+
+function ShiftX(t) = Translate([60, 0, 0]);
+
+function PathMatrix(t) = AffMerge([RotZt(t), Hills(t), ShiftX(t), RotXZ(t)]);
+
+function ThePolygon(t) =
+ [[-5,0,0], [-5,0,3], [-3,0,3], [-3,0,1], [3,0,1], [3,0,3], [5,0,3], [5,0,0]];
+
+pointarrays =
+ [for (t=[0:0.002:0.99999])
+ [for (p=ThePolygon(t))
+ Affine(PathMatrix(t), p)
+ ]
+ ];
+
+CloseLoop(pointarrays);
+
diff --git a/demo_roller_coaster2.scad b/demo_roller_coaster2.scad
new file mode 100644
index 0000000..4c4f14f
--- /dev/null
+++ b/demo_roller_coaster2.scad
@@ -0,0 +1,91 @@
+// Created in 2021 by Ryan A. Colyer.
+// This work is released with CC0 into the public domain.
+// https://creativecommons.org/publicdomain/zero/1.0/
+
+use
+
+
+function RotZt(t) = RotZ(360*t);
+
+function hillf(t) = sin(-2*t*360-115)+sin(-3*t*360-57)+2;
+function Hills(t) = Translate([0, 0, 20*hillf(t)]);
+
+function RotXZ(t) = let(a = -45*(4 - hillf(t))/4) RotY(a);
+
+function ShiftX(t) = Translate([60, 0, 0]);
+
+function PathMatrix(t) = AffMerge([RotZt(t), Hills(t), ShiftX(t), RotXZ(t)]);
+
+function ThePolygon(t) =
+ [[-5,0,0], [-5,0,3], [-3,0,3], [-3,0,1], [3,0,1], [3,0,3], [5,0,3], [5,0,0]];
+
+pointarrays =
+ [for (t=[0:0.002:0.99999])
+ [for (p=ThePolygon(t))
+ Affine(PathMatrix(t), p)
+ ]
+ ];
+
+CloseLoop(pointarrays);
+
+$fa = 4; $fs = 0.4;
+
+module wheel() {
+ color("DimGray")
+ rotate_extrude()
+ translate([15, 0])
+ offset(2)
+ square([3, 12], center = true);
+ color("Silver")
+ for (a = [0:20:179])
+ rotate([90, 0, a])
+ scale([0.2, 1, 1])
+ cylinder(r = 4, h = 30, center = true);
+}
+
+module train() {
+ color("Black") linear_extrude(10, center = true) offset(5) square([40, 200], center = true);
+ for (x = [-80, -40, 40, 80]) translate([30, x, 0]) rotate([90, 0, 90]) wheel();
+ for (x = [-80, -40, 40, 80]) translate([-30, x, 0]) rotate([90, 0, 90]) wheel();
+ translate([0, 0, 22]) {
+ color("Red") {
+ hull() {
+ scale([1, 0.3, 1]) sphere(20);
+ translate([0, -100, 0]) scale([1, 0.3, 1]) sphere(20);
+ }
+ translate([0, -50, 0]) cylinder(r = 6, h = 25);
+ translate([0, -50, 25]) sphere(6);
+ translate([0, -80, 0]) cylinder(r = 6, h = 40);
+ translate([0, -80, 50]) difference() {
+ sphere(r = 13);
+ translate([0, 0, 25]) cube(50, center = true);
+ }
+ difference() {
+ translate([0, 40, 30]) cube([42, 70, 100], center = true);
+ translate([0, 40, 50]) cube([44, 40, 40], center = true);
+ translate([0, 55, 25]) cube([36, 90, 100], center = true);
+ }
+ translate([0, 50, 80]) linear_extrude(8, scale = 1.1) square([42, 90], center = true);
+ }
+ color("Black") for(x = [-92, -62, -32, -2])
+ translate([0, x, 0]) rotate([90, 0, 0]) cylinder(r = 20.2, h = 5);
+ }
+}
+
+ztilt = atan2(20*(hillf($t+0.002)-hillf($t-0.002)), 60*0.004*6.28);
+multmatrix(AffMerge([RotZt($t), Hills($t), ShiftX($t)]))
+ translate([0,0,3])
+ rotate([ztilt, 0, 0])
+ multmatrix(RotXZ($t))
+ scale(0.06) mirror([0,1,0]) translate([0,0,18]) train();
+
+// Train written in 2019 by Torsten Paul
+//
+// To the extent possible under law, the author(s) have dedicated all
+// copyright and related and neighboring rights to this software to the
+// public domain worldwide. This software is distributed without any
+// warranty.
+//
+// You should have received a copy of the CC0 Public Domain
+// Dedication along with this software.
+// If not, see .
diff --git a/demo_wavy_donut.scad b/demo_wavy_donut.scad
new file mode 100644
index 0000000..472fbf2
--- /dev/null
+++ b/demo_wavy_donut.scad
@@ -0,0 +1,31 @@
+// Created in 2021 by Ryan A. Colyer.
+// This work is released with CC0 into the public domain.
+// https://creativecommons.org/publicdomain/zero/1.0/
+
+use
+
+
+// [Scale X] [Shear X along Y] [Shear X along Z] [Translate X]
+// [Shear Y along X] [Scale Y] [Shear Y along Z] [Translate Y]
+// [Shear Z along X] [Shear Z along Y] [Scale Z] [Translate Z]
+// or rotation matrix [[cos,-sin],[sin,cos]] in the 2 axes for a plane.
+function PathMatrix(t) =
+ [[cos(t*360), -sin(t*360), 0, 50*cos(t*360)],
+ [sin(t*360), cos(t*360), 0, 40*sin(t*360)],
+ [ 0, 0, 1, 0]];
+
+function ThePolygon(t) =
+ [for (a=[0:2:359.99])
+ (5+5*(1+cos(8*t*360))/2)*[cos(a), 0, -sin(a)]
+ ];
+
+
+pointarrays =
+ [for (t=[0:0.002:0.99999])
+ [for (p=ThePolygon(t))
+ Affine(PathMatrix(t), p)
+ ]
+ ];
+
+CloseLoop(pointarrays);
+
diff --git a/images/demo_images.gif b/images/demo_images.gif
new file mode 100644
index 0000000..16f5897
Binary files /dev/null and b/images/demo_images.gif differ