From d1fe73bb5ed542c1b3f5b3223ff52cd84e9ef6de Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Wed, 29 Jul 2020 06:50:14 +0100 Subject: [PATCH] Revert "restore polyhedra, shapes and shapes3d" This reverts commit ea604f967215a0de46881c90706cc65fc356338e. --- polyhedra.scad | 785 ++++++++++++++++++++ shapes.scad | 1755 +++++++++++++++++++++++++++++++++++++++++++++ shapes2d.scad | 1866 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 4406 insertions(+) create mode 100644 polyhedra.scad create mode 100644 shapes.scad create mode 100644 shapes2d.scad diff --git a/polyhedra.scad b/polyhedra.scad new file mode 100644 index 0000000..3902ecf --- /dev/null +++ b/polyhedra.scad @@ -0,0 +1,785 @@ +////////////////////////////////////////////////////////////////////// +// LibFile: polyhedra.scad +// Useful platonic, archimedian, and catalan polyhedra. +// To use, add the following lines to the beginning of your file: +// ``` +// include +// include +// ``` +////////////////////////////////////////////////////////////////////// + + +include + + +// CommonCode: +// $fn=96; + + +// Section: Polyhedra + + +// Groups entries in "arr" into groups of equal values and returns index lists of those groups + +function _unique_groups(m) = [ + for (i=[0:1:len(m)-1]) let( + s = search([m[i]], m, 0)[0] + ) if (s[0]==i) s +]; + + +// TODO +// +// Use volume info? +// Support choosing a face number down +// Support multiple inspheres/outspheres when appropriate? +// face order for children? +// orient faces so an edge is parallel to the x-axis +// + + +// Module: regular_polyhedron() +// +// Description: +// Creates a regular polyhedron with optional rounding. Children are placed on the polyhedron's faces. +// +// **Selecting the polyhedron:** +// You constrain the polyhedra list by specifying different characteristics, that must all be met +// * `name`: e.g. `"dodecahedron"` or `"pentagonal icositetrahedron"` +// * `type`: Options are `"platonic"`, `"archimedean"` and `"catalan"` +// * `faces`: The required number of faces +// * `facetype`: The required face type(s). List of vertex counts for the faces. Exactly the listed types of faces must appear: +// * `facetype = 3`: polyhedron with all triangular faces. +// * `facetype = [5,6]`: polyhedron with only pentagons and hexagons. (Must have both!) +// * hasfaces: The list of vertex counts for faces; at least one listed type must appear: +// * `hasfaces = 3`: polygon has at least one triangular face +// * `hasfaces = [5,6]`: polygon has a hexagonal or a pentagonal face +// +// The result is a list of selected polyhedra. You then specify `index` to choose which one of the +// remaining polyhedra you want. If you don't give `index` the first one on the list is created. +// Two examples: +// * `faces=12, index=2`: Creates the 3rd solid with 12 faces +// * `type="archimedean", faces=14`: Creates the first archimedean solid with 14 faces (there are 3) +// +// **Choosing the size of your polyhedron:** +// The default is to create a polyhedron whose smallest edge has length 1. You can specify the +// smallest edge length with the size option. Alternatively you can specify the size of the +// inscribed sphere, midscribed sphere, or circumscribed sphere using `ir`, `mr` and `cr` respectively. +// If you specify `cr=3` then the outermost points of the polyhedron will be 3 units from the center. +// If you specify `ir=3` then the innermost faces of the polyhedron will be 3 units from the center. +// For the platonic solids every face meets the inscribed sphere and every corner touches the +// circumscribed sphere. For the Archimedean solids the inscribed sphere will touch only some of +// the faces and for the Catalan solids the circumscribed sphere meets only some of the corners. +// +// **Orientation:** +// Orientation is controled by the facedown parameter. Set this to false to get the canonical orientation. +// Set it to true to get the largest face oriented down. If you set it to a number the module searches for +// a face with the specified number of vertices and orients that face down. +// +// **Rounding:** +// If you specify the rounding parameter the module makes a rounded polyhedron by first creating an +// undersized model and then expanding it with `minkowski()`. This only produces the correct result +// if the in-sphere contacts all of the faces of the polyhedron, which is true for the platonic, the +// catalan solids and the trapezohedra but false for the archimedean solids. +// +// **Children:** +// The module places children on the faces of the polyhedron. The child coordinate system is +// positioned so that the origin is the center of the face. If `rotate_children` is true (default) +// then the coordinate system is oriented so the z axis is normal to the face, which lies in the xy +// plane. If you give `repeat=true` (default) the children are cycled through to cover all faces. +// With `repeat=false` each child is used once. You can specify `draw=false` to suppress drawing of +// the polyhedron, e.g. to use for `difference()` operations. The module sets various parameters +// you can use in your children (see the side effects list below). +// +// **Stellation:** +// Technically stellation is an operation of shifting the polyhedron's faces to produce a new shape +// that may have self-intersecting faces. OpenSCAD cannot handle self-intersecting faces, so we +// instead erect a pyramid on each face, a process technically referred to as augmentation. The +// height of the pyramid is given by the `stellate` argument. If `stellate` is `false` or `0` then +// no stellation is performed. Otherwise stellate gives the pyramid height as a multiple of the +// edge length. A negative pyramid height can be used to perform excavation, where a pyramid is +// removed from each face. +// +// **Special Polyhedra:** +// These can be selected only by name and may require different parameters, or ignore some standard +// parameters. +// * Trapezohedron: a family of solids with an even number of kite shaped sides. +// One example of a trapezohedron is the d10 die, which is a 10 face trapezohedron. +// You must specify exactly two of `side`, `longside`, `h`, and `r` (or `d`). +// You cannot create trapezohedron shapes using `mr`, `ir`, or `or`. +// * `side`: Length of the short side. +// * `longside`: Length of the long side that extends to the apex. +// * `h`: Distance from the center to the apex. +// * `r`: Radius of the polygon that defines the equatorial vertices. +// * `d`: Diameter of the polygon that defines the equatorial vertices. +// +// * Named stellations: various polyhedra such as the Kepler-Poinsot solids are stellations with +// specific pyramid heights. To make them easier to generate you can specify them by name. +// This is equivalent to giving the name of the appropriate base solid and the magic stellate +// parameter needed to produce that shape. The supported solids are: +// * `"great dodecahedron"` +// * `"small stellated dodecahedron"` +// * `"great stellated dodecahedron"` +// +// Arguments: +// name = Name of polyhedron to create. +// index = Index to select from polyhedron list. Default: 0. +// type = Type of polyhedron: "platonic", "archimedean", "catalan". +// faces = Number of faces. +// facetype = Scalar or vector listing required type of faces as vertex count. Polyhedron must have faces of every type listed and no other types. +// hasfaces = Scalar of vector list face vertex counts. Polyhedron must have at least one of the listed types of face. +// side = Length of the smallest edge of the polyhedron. Default: 1. +// ir = inner radius. Polyhedron is scaled so it has the specified inner radius. Overrides side. +// mr = middle radius. Polyhedron is scaled so it has the specified middle radius. Overrides side. +// or = outer radius. Polyhedron is scaled so it has the specified outer radius. Overrides side. +// r = outer radius. Overrides or. +// d = outer diameter. Overrides or. +// anchor = Side of the origin to anchor to. The bounding box of the polyhedron is aligned as specified. Use directional constants from `constants.scad`. Default: `CENTER` +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=UP+BACK+RIGHT`. +// facedown = If false display the solid in native orientation. If true orient it with a largest face down. If set to a vertex count, orient it so a face with the specified number of vertices is down. Default: true. +// rounding = Specify a rounding radius for the shape. Note that depending on $fn the dimensions of the shape may have small dimensional errors. +// repeat = If true then repeat the children to fill all the faces. If false use only the available children and stop. Default: true. +// draw = If true then draw the polyhedron. If false, draw the children but not the polyhedron. Default: true. +// rotate_children = If true then orient children normal to their associated face. If false orient children to the parent coordinate system. Default: true. +// stellate = Set to a number to erect a pyramid on every face of your polyhedron with the specified height. The height is a multiple of the side length. Default: false. +// longside = Specify the long side length for a trapezohedron. Ignored for other shapes. +// h = Specify the height of the apex for a trapezohedron. Ignored for other shapes. +// +// Side Effects: +// `$faceindex` - Index number of the face +// `$face` - Coordinates of the face (2d if rotate_children==true, 3d if not) +// `$center` - Polyhedron center in the child coordinate system +// +// Examples: All of the available polyhedra by name in their native orientation +// regular_polyhedron("tetrahedron", facedown=false); +// regular_polyhedron("cube", facedown=false); +// regular_polyhedron("octahedron", facedown=false); +// regular_polyhedron("dodecahedron", facedown=false); +// regular_polyhedron("icosahedron", facedown=false); +// regular_polyhedron("truncated tetrahedron", facedown=false); +// regular_polyhedron("truncated octahedron", facedown=false); +// regular_polyhedron("truncated cube", facedown=false); +// regular_polyhedron("truncated icosahedron", facedown=false); +// regular_polyhedron("truncated dodecahedron", facedown=false); +// regular_polyhedron("cuboctahedron", facedown=false); +// regular_polyhedron("icosidodecahedron", facedown=false); +// regular_polyhedron("rhombicuboctahedron", facedown=false); +// regular_polyhedron("rhombicosidodecahedron", facedown=false); +// regular_polyhedron("truncated cuboctahedron", facedown=false); +// regular_polyhedron("truncated icosidodecahedron", facedown=false); +// regular_polyhedron("snub cube", facedown=false); +// regular_polyhedron("snub dodecahedron", facedown=false); +// regular_polyhedron("triakis tetrahedron", facedown=false); +// regular_polyhedron("tetrakis hexahedron", facedown=false); +// regular_polyhedron("triakis octahedron", facedown=false); +// regular_polyhedron("pentakis dodecahedron", facedown=false); +// regular_polyhedron("triakis icosahedron", facedown=false); +// regular_polyhedron("rhombic dodecahedron", facedown=false); +// regular_polyhedron("rhombic triacontahedron", facedown=false); +// regular_polyhedron("deltoidal icositetrahedron", facedown=false); +// regular_polyhedron("deltoidal hexecontahedron", facedown=false); +// regular_polyhedron("disdyakis dodecahedron", facedown=false); +// regular_polyhedron("disdyakis triacontahedron", facedown=false); +// regular_polyhedron("pentagonal icositetrahedron", facedown=false); +// regular_polyhedron("pentagonal hexecontahedron", facedown=false); +// regular_polyhedron("trapezohedron",faces=10, side=1, longside=2.25, facedown=false); +// regular_polyhedron("great dodecahedron"); +// regular_polyhedron("small stellated dodecahedron"); +// regular_polyhedron("great stellated dodecahedron"); +// Example: Third Archimedean solid +// regular_polyhedron(type="archimedean", index=2); +// Example(Med): Solids that have 8 or 10 vertex faces +// N = len(regular_polyhedron_info("index set", hasfaces=[8,10])); +// for(i=[0:N-1]) right(3*i) +// regular_polyhedron(hasfaces=[8,10], index=i, mr=1); +// Example(Big): Solids that include a quadrilateral face +// N = len(regular_polyhedron_info("index set", hasfaces=4)); +// for(i=[0:N-1]) right(3*i) +// regular_polyhedron(hasfaces=4, index=i, mr=1); +// Example(Med): Solids with only quadrilateral faces +// N = len(regular_polyhedron_info("index set", facetype=4)); +// for(i=[0:N-1]) right(3*i) +// regular_polyhedron(facetype=4, index=i, mr=1); +// Example: Solids that have both pentagons and hexagons and no other face types +// N = len(regular_polyhedron_info("index set", facetype=[5,6])); +// for(i=[0:N-1]) right(3*i) +// regular_polyhedron(facetype=[5,6], index=i, mr=1); +// Example: Rounded octahedron +// regular_polyhedron("octahedron", side=1, rounding=.2); +// Example: Rounded catalon solid +// regular_polyhedron("rhombic dodecahedron", side=1, rounding=0.2); +// Example(Med): Rounded Archimedean solid compared to unrounded version. The small faces are shifted back from their correct position. +// %regular_polyhedron(type="archimedean", mr=1, rounding=0); +// regular_polyhedron(type="archimedean", mr=1, rounding=0.3); +// Example: Two children are distributed arbitrarily over the faces +// regular_polyhedron(faces=12,index=2,repeat=true) { +// color("red") sphere(r=.1); +// color("green") sphere(r=.1); +// } +// Example(FlatSpin): Difference the children from the polyhedron; children depend on $faceindex +// difference(){ +// regular_polyhedron("tetrahedron", side=25); +// regular_polyhedron("tetrahedron", side=25,draw=false) +// down(.3) linear_extrude(height=1) +// text(str($faceindex),halign="center",valign="center"); +// } +// Example(Big): With `rotate_children` you can control direction of the children. +// regular_polyhedron(name="tetrahedron", anchor=UP, rotate_children=true) +// cylinder(r=.1, h=.5); +// right(2) regular_polyhedron(name="tetrahedron", anchor=UP, rotate_children=false) +// cylinder(r=.1, h=.5); +// Example(FlatSpin,Med): Using `$face` you can have full control of the construction of your children. This example constructs the Great Icosahedron. +// module makestar(pts) { // Make a star from a point list +// polygon( +// [ +// for(i=[0:len(pts)-1]) let( +// p0=select(pts,i), +// p1=select(pts,i+1), +// center=(p0+p1)/2, +// v=sqrt(7/4-PHI)*(p1-p0) +// ) each [p0, [v.y+center.x, -v.x+center.y]] +// ] +// ); +// } +// regular_polyhedron("dodecahedron", side=1, repeat=true) +// linear_extrude(scale=0, height=sqrt((5+2*sqrt(5))/5)) makestar($face); +// Example(Med): The spheres are all radius 1 and the octahedra are sized to match the in-sphere, mid-sphere and out-sphere. The sphere size is slightly adjusted for the in-sphere and out-sphere so you can see the relationship: the sphere is tangent to the faces for the former and the corners poke out for the latter. Note also the difference in the size of the three octahedra. +// sphere(r=1.005); +// %regular_polyhedron("octahedron", ir=1, facedown=false); +// right(3.5) { +// sphere(r=1); +// %regular_polyhedron("octahedron", mr=1, facedown=false); +// } +// right(6.5) { +// %sphere(r=.95); // Slightly undersized sphere means the points poke out a bit +// regular_polyhedron("octahedron", or=1,facedown=false); +// } +// Example(Med): For the Archimdean solids the in-sphere does not touch all of the faces, as shown by this example, but the circumscribed sphere meets every vertex. (This explains the problem for rounding over these solids because the rounding method uses the in-sphere.) +// sphere(r=1.005); +// %regular_polyhedron("snub dodecahedron", ir=1, facedown=false); +// right(3) { +// sphere(r=1); +// %regular_polyhedron("snub dodecahedron", mr=1, facedown=false); +// } +// right(6) { +// %sphere(r=.99); +// regular_polyhedron("snub dodecahedron", or=1,facedown=false); +// } +// Example(Med): For a Catalan solid the in-sphere touches every face but the circumscribed sphere only touches some vertices. +// sphere(r=1.002); +// %regular_polyhedron("pentagonal hexecontahedron", ir=1, facedown=false); +// right(3) { +// sphere(r=1); +// %regular_polyhedron("pentagonal hexecontahedron", mr=1, facedown=false); +// } +// right(6) { +// %sphere(r=.98); +// regular_polyhedron("pentagonal hexecontahedron", or=1,facedown=false); +// } +module regular_polyhedron( + name=undef, + index=undef, + type=undef, + faces=undef, + facetype=undef, + hasfaces=undef, + side=1, + ir=undef, + mr=undef, + or=undef, + r=undef, + d=undef, + anchor=[0,0,0], + center=undef, + rounding=0, + repeat=true, + facedown=true, + draw=true, + rotate_children=true, + stellate = false, + longside=undef, // special parameter for trapezohedron + h=undef // special parameter for trapezohedron +) { + assert(rounding>=0, "'rounding' must be nonnegative"); + entry = regular_polyhedron_info( + "fullentry", name=name, index=index, + type=type, faces=faces, facetype=facetype, + hasfaces=hasfaces, side=side, + ir=ir, mr=mr, or=or, + r=r, d=d, + anchor=anchor, center=center, + facedown=facedown, + stellate=stellate, + longside=longside, h=h + ); + scaled_points = entry[0]; + translation = entry[1]; + face_triangles = entry[2]; + faces = entry[3]; + face_normals = entry[4]; + in_radius = entry[5]; + if (draw){ + if (rounding==0) + polyhedron(move(translation, p=scaled_points), faces = face_triangles); + else { + fn = segs(rounding); + rounding = rounding/cos(180/fn); + adjusted_scale = 1 - rounding / in_radius; + minkowski(){ + sphere(r=rounding, $fn=fn); + polyhedron(move(translation,p=adjusted_scale*scaled_points), faces = face_triangles); + } + } + } + if ($children>0) { + maxrange = repeat ? len(faces)-1 : $children-1; + for(i=[0:1:maxrange]) { + // Would like to orient so an edge (longest edge?) is parallel to x axis + facepts = move(translation, p=select(scaled_points, faces[i])); + center = mean(facepts); + rotatedface = rot(from=face_normals[i], to=[0,0,1], p=move(-center, p=facepts)); + clockwise = sortidx([for(pt=rotatedface) -atan2(pt.y,pt.x)]); + $face = rotate_children? + path2d(select(rotatedface,clockwise)) : + select(move(-center,p=facepts), clockwise); + $faceindex = i; + $center = -translation-center; + translate(center) + if (rotate_children) { + rot(from=[0,0,1], to=face_normals[i]) + children(i % $children); + } else { + children(i % $children); + } + } + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// Some internal functions used to generate polyhedra data +// +// All permutations and even permutations of three items +// +function _even_perms(v) = [v, [v[2], v[0], v[1]], [v[1],v[2],v[0]]]; +function _all_perms(v) = [v, [v[2], v[0], v[1]], [v[1],v[2],v[0]], [v[1],v[0],v[2]],[v[2],v[1],v[0]],[v[0],v[2],v[1]]]; +// +// Point reflections across all planes. In the unconstrained case, this means one point becomes 8 points. +// +// sign=="even" means an even number of minus signs (odd number of plus signs) +// sign=="odd" means an odd number of minus signs (even number of plus signs) +// +function _point_ref(points, sign="both") = + unique([ + for(i=[-1,1],j=[-1,1],k=[-1,1]) + if (sign=="both" || sign=="even" && i*j*k>0 || sign=="odd" && i*j*k<0) + each [for(point=points) vmul(point,[i,j,k])] + ]); +// +_tribonacci=(1+4*cosh(acosh(2+3/8)/3))/3; +// +///////////////////////////////////////////////////////////////////////////// +// +// Polyhedra data table. +// The polyhedra information is from Wikipedia and http://dmccooey.com/polyhedra/ +// +_polyhedra_ = [ + // Platonic Solids + + ["tetrahedron", "platonic", 4,[3], 2*sqrt(2), sqrt(6)/12, sqrt(2)/4, sqrt(6)/4, 1/6/sqrt(2), + _point_ref([[1,1,1]], sign="even")], + ["cube", "platonic", 6, [4], 2, 1/2, 1/sqrt(2), sqrt(3)/2, 1, + _point_ref([[1,1,1]])], + ["octahedron", "platonic", 8, [3], sqrt(2), sqrt(6)/6, 1/2, sqrt(2)/2, sqrt(2)/3, + _point_ref(_even_perms([1,0,0]))], + ["dodecahedron", "platonic", 12, [5], 2/PHI, sqrt(5/2+11*sqrt(5)/10)/2, (3+sqrt(5))/4, sqrt(3)*PHI/2, (15+7*sqrt(5))/4, + _point_ref(concat([[1,1,1]],_even_perms([0,PHI,1/PHI])))], + ["icosahedron", "platonic", 20, [3], 2, PHI*PHI/2/sqrt(3), cos(36), sin(72), 5*(3+sqrt(5))/12, + _point_ref(_even_perms([0,1,PHI]))], + + // Archimedian Solids, listed in order by Wenniger number, W6-W18 + + ["truncated tetrahedron", "archimedean", 8,[6,3], sqrt(8), sqrt(6)/4, 3*sqrt(2)/4, sqrt(11/8), 23*sqrt(2)/12, + _point_ref(_all_perms([1,1,3]),sign="even")], + ["truncated octahedron", "archimedean", 14, [6,4], sqrt(2), sqrt(6)/2, 1.5, sqrt(10)/2, 8*sqrt(2), + _point_ref(_all_perms([0,1,2]))], + ["truncated cube", "archimedean", 14, [8,3], 2*(sqrt(2)-1), (1+sqrt(2))/2, 1+sqrt(2)/2, sqrt(7+4*sqrt(2))/2, 7+14*sqrt(2)/3, + _point_ref(_all_perms([1,1,sqrt(2)-1]))], + ["truncated icosahedron", "archimedean", 32, [6, 5], 2, (3*sqrt(3)+sqrt(15))/4, 3*PHI/2, sqrt(58+18*sqrt(5))/4, (125+43*sqrt(5))/4, + _point_ref(concat( + _even_perms([0,1,3*PHI]), + _even_perms([1,2+PHI,2*PHI]), + _even_perms([PHI,2,PHI*PHI*PHI]) + ))], + ["truncated dodecahedron", "archimedean", 32, [10, 3], 2*PHI-2, sqrt(7+11*PHI)/2, (3*PHI+1)/2,sqrt(11+PHI*15)/2, 5*(99+47*sqrt(5))/12, + _point_ref(concat( + _even_perms([0,1/PHI, 2+PHI]), + _even_perms([1/PHI,PHI,2*PHI]), + _even_perms([PHI,2,PHI+1]) + ))], + ["cuboctahedron", "archimedean", 14, [4,3], sqrt(2), sqrt(2)/2, sqrt(3)/2, 1, 5*sqrt(2)/3, + _point_ref(_all_perms([1,1,0]))], + ["icosidodecahedron", "archimedean", 32, [5,3], 1, sqrt(5*(5+2*sqrt(5)))/5,sqrt(5+2*sqrt(5))/2, PHI, (14+17*PHI)/3, + _point_ref(concat(_even_perms([0,0,PHI]),_even_perms([1/2,PHI/2,PHI*PHI/2])))], + ["rhombicuboctahedron", "archimedean", 26, [4, 3], 2, (1+sqrt(2))/2, sqrt(2*(2+sqrt(2)))/2, sqrt(5+2*sqrt(2))/2, 4+10*sqrt(2)/3, + _point_ref(_even_perms([1,1,1+sqrt(2)]))], + ["rhombicosidodecahedron", "archimedean", 62, [5,4,3], 2, 3/10*sqrt(15+20*PHI), sqrt(3/2+2*PHI), sqrt(8*PHI+7)/2, (31+58*PHI)/3, + _point_ref(concat( + _even_perms([1,1,PHI*PHI*PHI]), + _even_perms([PHI*PHI,PHI,2*PHI]), + _even_perms([2+PHI,0,PHI*PHI]) + ))], + ["truncated cuboctahedron", "archimedean", 26, [8, 6, 4], 2, (1+2*sqrt(2))/2, sqrt(6*(2+sqrt(2)))/2, sqrt(13+6*sqrt(2))/2, (22+14*sqrt(2)), + _point_ref(_all_perms([1,1+sqrt(2), 1+2*sqrt(2)]))], + ["truncated icosidodecahedron", "archimedean", 62, [10,6,4], 2*PHI - 2, sqrt(15/4+5*PHI),sqrt(9/2+6*PHI),sqrt(19/4+6*PHI), 95+50*sqrt(5), + _point_ref(concat( + _even_perms([1/PHI,1/PHI,3+PHI]), + _even_perms([2/PHI,PHI,1+2*PHI]), + _even_perms([1/PHI,PHI*PHI,3*PHI-1]), + _even_perms([2*PHI-1,2,2+PHI]), + _even_perms([PHI,3,2*PHI]) + ))], + ["snub cube", "archimedean", 38, [4,3], 1.60972,1.14261350892596209,1.24722316799364325, 1.34371337374460170, + sqrt((613*_tribonacci+203)/(9*(35*_tribonacci-62))), + concat( + _point_ref(_even_perms([1,1/_tribonacci,_tribonacci]), sign="odd"), + _point_ref(_even_perms([1,_tribonacci,1/_tribonacci]), sign="even") + )], + ["snub dodecahedron", "archimedean", 92, [5, 3], 1, 1.98091594728184,2.097053835252087,2.155837375115, 37.61664996273336, + concat( + _point_ref(_even_perms([0.374821658114562,0.330921024729844,2.097053835252088]), sign="odd"), + _point_ref(_even_perms([0.192893711352359,1.249503788463027,1.746186440985827]), sign="odd"), + _point_ref(_even_perms([1.103156835071754,0.847550046789061,1.646917940690374]), sign="odd"), + _point_ref(_even_perms([0.567715369466922,0.643029605914072,1.977838965420219]), sign="even"), + _point_ref(_even_perms([1.415265416255982,0.728335176957192,1.454024229338015]), sign="even") + )], + + // Catalan Solids, the duals to the Archimedean solids, listed in the corresponding order + + ["triakis tetrahedron","catalan", 12, [3], 9/5, 5*sqrt(22)/44, 5*sqrt(2)/12, 5*sqrt(6)/12, 25*sqrt(2)/36, + concat( + _point_ref([9*sqrt(2)/20*[1,1,1]],sign="even"), + _point_ref([3*sqrt(2)/4*[1,1,1]],sign="odd") + )], + ["tetrakis hexahedron", "catalan", 24, [3], 1, 2/sqrt(5), 2*sqrt(2)/3, 2/sqrt(3), 32/9, + _point_ref(concat([[2/3,2/3,2/3]],_even_perms([1,0,0])))], + ["triakis octahedron", "catalan", 24, [3], 2, sqrt(17*(23+16*sqrt(2)))/34, 1/2+sqrt(2)/4,(1+sqrt(2))/2,3/2+sqrt(2), + _point_ref(concat([[1,1,1]],_even_perms([1+sqrt(2),0,0])))], + ["pentakis dodecahedron", "catalan", 60, [3], 1,sqrt(477/436+97*sqrt(5)/218), sqrt(5)/4+11/12, sqrt(7/4+sqrt(5)/3), 125*sqrt(5)/36+205/36, + _point_ref(concat( + _even_perms([0,(5-PHI)/6, PHI/2+2/3]), + _even_perms([0,(PHI+1)/2,PHI/2]),[(4*PHI-1)/6 * [1,1,1]] + ))], + ["triakis icosahedron", "catalan", 60, [3], 1, sqrt((139+199*PHI)/244), (8*PHI+1)/10, sqrt(13/8+19/8/sqrt(5)), (13*PHI+3)/2, + _point_ref(concat( + _even_perms([(PHI+7)/10, 0, (8*PHI+1)/10]), + _even_perms([0, 1/2, (PHI+1)/2]),[PHI/2*[1,1,1]] + ))], + ["rhombic dodecahedron", "catalan", 12, [4], sqrt(3), sqrt(2/3), 2*sqrt(2)/3, 2/sqrt(3), 16*sqrt(3)/9, + _point_ref(concat([[1,1,1]], _even_perms([2,0,0])))], + ["rhombic triacontahedron", "catalan", 30,[4], 1, sqrt(1+2/sqrt(5)), 1+1/sqrt(5), (1+sqrt(5))/2, 4*sqrt(5+2*sqrt(5)), + concat( + _point_ref(_even_perms([0,sqrt(1+2/sqrt(5)), sqrt((5+sqrt(5))/10)])), + _point_ref(_even_perms([0,sqrt(2/(5+sqrt(5))), sqrt(1+2/sqrt(5))])), + _point_ref([sqrt((5+sqrt(5))/10)*[1,1,1]]) + )], + ["deltoidal icositetrahedron", "catalan", 24, [4], 2*sqrt(10-sqrt(2))/7, 7*sqrt((7+4*sqrt(2))/(34 * (10-sqrt(2)))), + 7*sqrt(2*(2+sqrt(2)))/sqrt(10-sqrt(2))/4, 7*sqrt(2)/sqrt(10-sqrt(2))/2, + (14+21*sqrt(2))/sqrt(10-sqrt(2)), + _point_ref(concat( + _even_perms([0,1,1]), _even_perms([sqrt(2),0,0]), + _even_perms((4+sqrt(2))/7*[1,1,1]) + ))], + ["deltoidal hexecontahedron", "catalan", 60, [4], sqrt(5*(85-31*sqrt(5)))/11, sqrt(571/164+1269/164/sqrt(5)), 5/4+13/4/sqrt(5), + sqrt(147+65*sqrt(5))/6, sqrt(29530+13204*sqrt(5))/3, + _point_ref(concat( + _even_perms([0,0,sqrt(5)]), + _even_perms([0,(15+sqrt(5))/22, (25+9*sqrt(5))/22]), + _even_perms([0,(5+3*sqrt(5))/6, (5+sqrt(5))/6]), + _even_perms([(5-sqrt(5))/4, sqrt(5)/2, (5+sqrt(5))/4]), + [(5+4*sqrt(5))/11*[1,1,1]] + ))], + ["disdyakis dodecahedron", "catalan", 48, [3], 1,sqrt(249/194+285/194/sqrt(2)) ,(2+3*sqrt(2))/4, sqrt(183/98+213/98/sqrt(2)), + sqrt(6582+4539*sqrt(2))/7, + _point_ref(concat( + _even_perms([sqrt(183/98+213/98/sqrt(2)),0,0]), + _even_perms(sqrt(3+3/sqrt(2))/2 * [1,1,0]),[7/sqrt(6*(10-sqrt(2)))*[1,1,1]] + ))], + ["disdyakis triacontahedron","catalan", 120, [3], sqrt(15*(85-31*sqrt(5)))/11, sqrt(3477/964+7707/964/sqrt(5)), 5/4+13/4/sqrt(5), + sqrt(441+195*sqrt(5))/10,sqrt(17718/5+39612/5/sqrt(5)), + _point_ref(concat( + _even_perms([0,0,3*(5+4*sqrt(5))/11]), + _even_perms([0,(5-sqrt(5))/2,(5+sqrt(5))/2]), + _even_perms([0,(15+9*sqrt(5))/10,3*(5+sqrt(5))/10]), + _even_perms([3*(15+sqrt(5))/44,3*(5+4*sqrt(5))/22, (75+27*sqrt(5))/44]), [sqrt(5)*[1,1,1]] + ))], + ["pentagonal icositetrahedron","catalan",24, [5], 0.593465355971, 1.950681331784, 2.1015938932963, 2.29400105368695, 35.6302020120713, + concat( + _point_ref(_even_perms([0.21879664300048044,0.740183741369857,1.0236561781126901]),sign="even"), + _point_ref(_even_perms([0.21879664300048044,1.0236561781126901,0.740183741369857]),sign="odd"), + _point_ref(_even_perms([1.3614101519264425,0,0])), + _point_ref([0.7401837413698572*[1,1,1]]) + )], + ["pentagonal hexecontahedron", "catalan", 60,[5], 0.58289953474498, 3.499527848905764,3.597624822551189,3.80854772878239, 189.789852066885, + concat( + _point_ref(_even_perms([0.192893711352359,0.218483370127321,2.097053835252087]), sign="even"), + _point_ref(_even_perms([0,0.7554672605165955,1.9778389654202186])), + _point_ref(_even_perms([0,1.888445389283669154,1.1671234364753339])), + _point_ref(_even_perms([0.56771536946692131,0.824957552676275846,1.8654013108176956657]),sign="odd"), + _point_ref(_even_perms([0.37482165811456229,1.13706613386050418,1.746186440985826345]), sign="even"), + _point_ref(_even_perms([0.921228888309550,0.95998770139158,1.6469179406903744]),sign="even"), + _point_ref(_even_perms([0.7283351769571914773,1.2720962825758121,1.5277030708585051]),sign="odd"), + _point_ref([1.222371704903623092*[1,1,1]]) + )], +]; + + +_stellated_polyhedra_ = [ + ["great dodecahedron", "icosahedron", -sqrt(5/3-PHI)], + ["small stellated dodecahedron", "dodecahedron", sqrt((5+2*sqrt(5))/5)], + ["great stellated dodecahedron", "icosahedron", sqrt(2/3+PHI)], +]; + + +// Function: regular_polyhedron_info() +// +// Usage: regular_polyhedron_info(info, ....) +// +// Description: +// Calculate characteristics of regular polyhedra or the selection set for regular_polyhedron(). +// Invoke with the same arguments used by regular_polyhedron() and use the `info` argument to +// request the desired return value. Set `info` to: +// * `"vnf"`: vnf for the selected polyhedron +// * `"vertices"`: vertex list for the selected polyhedron +// * `"faces"`: list of faces for the selected polyhedron, where each entry on the list is a list of point index values to be used with the vertex list +// * `"face normals"`: list of normal vectors for each face +// * `"in_radius"`: in-sphere radius for the selected polyhedron +// * `"mid_radius"`: mid-sphere radius for the selected polyhedron +// * `"out_radius"`: circumscribed sphere radius for the selected polyhedron +// * `"index set"`: index set selected by your specifications; use its length to determine the valid range for `index`. +// * `"face vertices"`: number of vertices on the faces of the selected polyhedron (always a list) +// * `"edge length"`: length of the smallest edge of the selected polyhedron +// * `"center"`: center for the polyhedron +// * `"type"`: polyhedron type, one of "platonic", "archimedean", "catalan", or "trapezohedron" +// * `"name"`: name of selected polyhedron +// +// Arguments: +// name = Name of polyhedron to create. +// index = Index to select from polyhedron list. Default: 0. +// type = Type of polyhedron: "platonic", "archimedean", "catalan". +// faces = Number of faces. +// facetype = Scalar or vector listing required type of faces as vertex count. Polyhedron must have faces of every type listed and no other types. +// hasfaces = Scalar of vector list face vertex counts. Polyhedron must have at least one of the listed types of face. +// side = Length of the smallest edge of the polyhedron. Default: 1. +// ir = inner radius. Polyhedron is scaled so it has the specified inner radius. Overrides side. +// mr = middle radius. Polyhedron is scaled so it has the specified middle radius. Overrides side. +// or = outer radius. Polyhedron is scaled so it has the specified outer radius. Overrides side. +// r = outer radius. Overrides or. +// d = outer diameter. Overrides or. +// anchor = Side of the origin to anchor to. The bounding box of the polyhedron is aligned as specified. Use directional constants from `constants.scad`. Default: `CENTER` +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=UP+BACK+RIGHT`. +// facedown = If false display the solid in native orientation. If true orient it with a largest face down. If set to a vertex count, orient it so a face with the specified number of vertices is down. Default: true. +// rounding = Specify a rounding radius for the shape. Note that depending on $fn the dimensions of the shape may have small dimensional errors. +// repeat = If true then repeat the children to fill all the faces. If false use only the available children and stop. Default: true. +// draw = If true then draw the polyhedron. If false, draw the children but not the polyhedron. Default: true. +// rotate_children = If true then orient children normal to their associated face. If false orient children to the parent coordinate system. Default: true. +// stellate = Set to a number to erect a pyramid on every face of your polyhedron with the specified height. The height is a multiple of the side length. Default: false. +// longside = Specify the long side length for a trapezohedron. Ignored for other shapes. +// h = Specify the height of the apex for a trapezohedron. Ignored for other shapes. +function regular_polyhedron_info( + info=undef, name=undef, + index=undef, type=undef, + faces=undef, facetype=undef, + hasfaces=undef, side=1, + ir=undef, mr=undef, or=undef, + r=undef, d=undef, + anchor=[0,0,0], center=undef, + facedown=true, stellate=false, + longside=undef, h=undef // special parameters for trapezohedron +) = let( + anchor = !is_undef(center) ? [0,0,0] : anchor, + argcount = num_defined([ir,mr,or,r,d]) + ) + assert(argcount<=1, "You must specify only one of 'ir', 'mr', 'or', 'r', and 'd'") + let( + ////////////////////// + //Index values into the _polyhedra_ array + // + pname = 0, // name of polyhedron + class = 1, // class name (e.g. platonic, archimedean) + facecount = 2, // number of faces + facevertices = 3, // vertices on the faces, e.g. [3] for all triangles, [3,4] for triangles and squares + edgelen = 4, // length of the edge for the vertex list in the database + in_radius = 5, // in radius for unit polyhedron (shortest side 1) + mid_radius = 6, // mid radius for unit polyhedron + out_radius = 7, // out radius for unit polyhedron + volume = 8, // volume of unit polyhedron (data not validated, not used right now) + vertices = 9, // vertex list (in arbitrary order) + ////////////////////// + r = !is_undef(d) ? d/2 : r, + or = !is_undef(r) ? r : or, + stellate_index = search([name], _stellated_polyhedra_, 1, 0)[0], + name = stellate_index==[] ? name : _stellated_polyhedra_[stellate_index][1], + stellate = stellate_index==[] ? stellate : _stellated_polyhedra_[stellate_index][2], + indexlist = ( + name=="trapezohedron" ? [0] : [ // dumy list of one item + for(i=[0:1:len(_polyhedra_)-1]) ( + if ( + (is_undef(name) || _polyhedra_[i][pname]==name) && + (is_undef(type) || _polyhedra_[i][class]==type) && + (is_undef(faces) || _polyhedra_[i][facecount]==faces) && + ( + is_undef(facetype) || 0==compare_lists( + is_list(facetype)? reverse(sort(facetype)) : [facetype], + _polyhedra_[i][facevertices] + ) + ) && + (is_undef(hasfaces) || any([for (ft=hasfaces) in_list(ft,_polyhedra_[i][facevertices])])) + ) i + ) + ] + ) + ) + assert(len(indexlist)>0, "No polyhedra meet your specification") + let(validindex = is_undef(index) || (index>=0 && index0 ? 1 : -1], + maxvertex = len(vertices), + newpts = [for(i=[0:1:len(faces)-1]) mean(select(vertices,faces[i]))+stellate*scalefactor*faces_normals[1][i]], + newfaces = [for(i=[0:1:len(faces)-1], j=[0:len(faces[i])-1]) concat([i+maxvertex],select(faces[i], [j, j+direction[i]]))], + allpts = concat(vertices, newpts), + normals = [for(face=newfaces) _facenormal(allpts,face)] + ) [newfaces, normals, allpts]; + + +function trapezohedron(faces, r, side, longside, h) = + assert(faces%2==0, "Number of faces must be even") + let( + N = faces/2, + parmcount = num_defined([r,side,longside,h]) + ) + assert(parmcount==2,"Must define exactly two of 'r', 'side', 'longside', and 'height'") + let( + separation = ( + !is_undef(h) ? 2*h*sqr(tan(90/N)) : + (!is_undef(r) && !is_undef(side))? sqrt(side*side+2*r*r*(cos(180/N)-1)) : + (!is_undef(r) && !is_undef(longside))? 2 * sqrt(sqr(longside)-sqr(r)) / (1-sqr(tan(90/N))) * sqr(tan(90/N)) : + 2*sqr(sin(90/N))*sqrt((sqr(side) + 2*sqr(longside)*(cos(180/N)-1)) / (cos(180/N)-1) / (cos(180/N)+cos(360/N))) + ) + ) + assert(separation==separation, "Impossible trapezohedron specification") + let( + h = !is_undef(h) ? h : 0.5*separation / sqr(tan(90/N)), + r = ( + !is_undef(r) ? r : + !is_undef(side) ? sqrt((sqr(separation) - sqr(side))/2/(cos(180/N)-1)) : + sqrt(sqr(longside) - sqr(h-separation/2)) + ), + top = [for(i=[0:1:N-1]) [r*cos(360/N*i), r*sin(360/N*i),separation/2]], + bot = [for(i=[0:1:N-1]) [r*cos(180/N+360/N*i), r*sin(180/N+360/N*i),-separation/2]], + vertices = concat([[0,0,h],[0,0,-h]],top,bot) + ) [ + "trapezohedron", "trapezohedron", faces, [4], + !is_undef(side)? side : sqrt(sqr(separation)-2*r*(cos(180/N)-1)), // actual side length + h*r/sqrt(r*r+sqr(h+separation/2)), // in_radius + h*r/sqrt(r*r+sqr(h-separation/2)), // mid_radius + max(h,sqrt(r*r+sqr(separation/2))), // out_radius + undef, // volume + vertices + ]; + + +function _facenormal(pts, face) = unit(cross(pts[face[2]]-pts[face[0]], pts[face[1]]-pts[face[0]])); + +// hull() function returns triangulated faces. This function identifies the vertices that belong to each face +// by grouping together the face triangles that share normal vectors. The output gives the face polygon +// point indices in arbitrary order (not usable as input to a polygon call) and a normal vector. + +function _full_faces(pts,faces) = + let( + normals = [for(face=faces) quant(_facenormal(pts,face),1e-12)], + groups = _unique_groups(normals), + faces = [for(entry=groups) unique(flatten(select(faces, entry)))], + facenormals = [for(entry=groups) normals[entry[0]]] + ) [faces, facenormals]; + + +// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/shapes.scad b/shapes.scad new file mode 100644 index 0000000..16b970d --- /dev/null +++ b/shapes.scad @@ -0,0 +1,1755 @@ +////////////////////////////////////////////////////////////////////// +// LibFile: shapes.scad +// Common useful shapes and structured objects. +// To use, add the following lines to the beginning of your file: +// ``` +// include +// ``` +////////////////////////////////////////////////////////////////////// + + +// Section: Cuboids + + +// Module: cuboid() +// +// Description: +// Creates a cube or cuboid object, with optional chamfering or rounding. +// Negative chamfers and roundings can be applied to create external masks, +// but only apply to edges around the top or bottom faces. +// +// Arguments: +// size = The size of the cube. +// chamfer = Size of chamfer, inset from sides. Default: No chamfering. +// rounding = Radius of the edge rounding. Default: No rounding. +// edges = Edges to chamfer/round. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: All edges. +// except_edges = Edges to explicitly NOT chamfer/round. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: No edges. +// trimcorners = If true, rounds or chamfers corners where three chamfered/rounded edges meet. Default: `true` +// p1 = Align the cuboid's corner at `p1`, if given. Forces `anchor=ALLNEG`. +// p2 = If given with `p1`, defines the cornerpoints of the cuboid. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: Simple regular cube. +// cuboid(40); +// Example: Cube with minimum cornerpoint given. +// cuboid(20, p1=[10,0,0]); +// Example: Rectangular cube, with given X, Y, and Z sizes. +// cuboid([20,40,50]); +// Example: Cube by Opposing Corners. +// cuboid(p1=[0,10,0], p2=[20,30,30]); +// Example: Chamferred Edges and Corners. +// cuboid([30,40,50], chamfer=5); +// Example: Chamferred Edges, Untrimmed Corners. +// cuboid([30,40,50], chamfer=5, trimcorners=false); +// Example: Rounded Edges and Corners +// cuboid([30,40,50], rounding=10); +// Example: Rounded Edges, Untrimmed Corners +// cuboid([30,40,50], rounding=10, trimcorners=false); +// Example: Chamferring Selected Edges +// cuboid([30,40,50], chamfer=5, edges=[TOP+FRONT,TOP+RIGHT,FRONT+RIGHT], $fn=24); +// Example: Rounding Selected Edges +// cuboid([30,40,50], rounding=5, edges=[TOP+FRONT,TOP+RIGHT,FRONT+RIGHT], $fn=24); +// Example: Negative Chamferring +// cuboid([30,40,50], chamfer=-5, edges=[TOP,BOT], except_edges=RIGHT, $fn=24); +// Example: Negative Chamferring, Untrimmed Corners +// cuboid([30,40,50], chamfer=-5, edges=[TOP,BOT], except_edges=RIGHT, trimcorners=false, $fn=24); +// Example: Negative Rounding +// cuboid([30,40,50], rounding=-5, edges=[TOP,BOT], except_edges=RIGHT, $fn=24); +// Example: Negative Rounding, Untrimmed Corners +// cuboid([30,40,50], rounding=-5, edges=[TOP,BOT], except_edges=RIGHT, trimcorners=false, $fn=24); +// Example: Standard Connectors +// cuboid(40) show_anchors(); +module cuboid( + size=[1,1,1], + p1=undef, p2=undef, + chamfer=undef, + rounding=undef, + edges=EDGES_ALL, + except_edges=[], + trimcorners=true, + anchor=CENTER, + spin=0, + orient=UP +) { + size = scalar_vec3(size); + edges = edges(edges, except=except_edges); + if (!is_undef(p1)) { + if (!is_undef(p2)) { + translate(pointlist_bounds([p1,p2])[0]) { + cuboid(size=vabs(p2-p1), chamfer=chamfer, rounding=rounding, edges=edges, trimcorners=trimcorners, anchor=ALLNEG) children(); + } + } else { + translate(p1) { + cuboid(size=size, chamfer=chamfer, rounding=rounding, edges=edges, trimcorners=trimcorners, anchor=ALLNEG) children(); + } + } + } else { + if (chamfer != undef) { + if (any(edges[0])) assert(chamfer <= size.y/2 && chamfer <=size.z/2, "chamfer must be smaller than half the cube length or height."); + if (any(edges[1])) assert(chamfer <= size.x/2 && chamfer <=size.z/2, "chamfer must be smaller than half the cube width or height."); + if (any(edges[2])) assert(chamfer <= size.x/2 && chamfer <=size.y/2, "chamfer must be smaller than half the cube width or length."); + } + if (rounding != undef) { + if (any(edges[0])) assert(rounding <= size.y/2 && rounding<=size.z/2, "rounding radius must be smaller than half the cube length or height."); + if (any(edges[1])) assert(rounding <= size.x/2 && rounding<=size.z/2, "rounding radius must be smaller than half the cube width or height."); + if (any(edges[2])) assert(rounding <= size.x/2 && rounding<=size.y/2, "rounding radius must be smaller than half the cube width or length."); + } + majrots = [[0,90,0], [90,0,0], [0,0,0]]; + attachable(anchor,spin,orient, size=size) { + if (chamfer != undef) { + if (edges == EDGES_ALL && trimcorners) { + if (chamfer<0) { + cube(size, center=true) { + attach(TOP) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(BOT) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + } + } else { + isize = [for (v = size) max(0.001, v-2*chamfer)]; + hull() { + cube([size.x, isize.y, isize.z], center=true); + cube([isize.x, size.y, isize.z], center=true); + cube([isize.x, isize.y, size.z], center=true); + } + } + } else if (chamfer<0) { + ach = abs(chamfer); + cube(size, center=true); + + // External-Chamfer mask edges + difference() { + union() { + for (i = [0:3], axis=[0:1]) { + if (edges[axis][i]>0) { + vec = EDGE_OFFSETS[axis][i]; + translate(vmul(vec/2, size+[ach,ach,-ach])) { + rotate(majrots[axis]) { + cube([ach, ach, size[axis]], center=true); + } + } + } + } + + // Add multi-edge corners. + if (trimcorners) { + for (za=[-1,1], ya=[-1,1], xa=[-1,1]) { + if (corner_edge_count(edges, [xa,ya,za]) > 1) { + translate(vmul([xa,ya,za]/2, size+[ach-0.01,ach-0.01,-ach])) { + cube([ach+0.01,ach+0.01,ach], center=true); + } + } + } + } + } + + // Remove bevels from overhangs. + for (i = [0:3], axis=[0:1]) { + if (edges[axis][i]>0) { + vec = EDGE_OFFSETS[axis][i]; + translate(vmul(vec/2, size+[2*ach,2*ach,-2*ach])) { + rotate(majrots[axis]) { + zrot(45) cube([ach*sqrt(2), ach*sqrt(2), size[axis]+2.1*ach], center=true); + } + } + } + } + } + } else { + difference() { + cube(size, center=true); + + // Chamfer edges + for (i = [0:3], axis=[0:2]) { + if (edges[axis][i]>0) { + translate(vmul(EDGE_OFFSETS[axis][i], size/2)) { + rotate(majrots[axis]) { + zrot(45) cube([chamfer*sqrt(2), chamfer*sqrt(2), size[axis]+0.01], center=true); + } + } + } + } + + // Chamfer triple-edge corners. + if (trimcorners) { + for (za=[-1,1], ya=[-1,1], xa=[-1,1]) { + if (corner_edge_count(edges, [xa,ya,za]) > 2) { + translate(vmul([xa,ya,za]/2, size-[1,1,1]*chamfer*4/3)) { + rot(from=UP, to=[xa,ya,za]) { + cube(chamfer*3, anchor=BOTTOM); + } + } + } + } + } + } + } + } else if (rounding != undef) { + sides = quantup(segs(rounding),4); + if (edges == EDGES_ALL) { + if(rounding<0) { + cube(size, center=true); + zflip_copy() { + up(size.z/2) { + difference() { + down(-rounding/2) cube([size.x-2*rounding, size.y-2*rounding, -rounding], center=true); + down(-rounding) { + ycopies(size.y-2*rounding) xcyl(l=size.x-3*rounding, r=-rounding); + xcopies(size.x-2*rounding) ycyl(l=size.y-3*rounding, r=-rounding); + } + } + } + } + } else { + isize = [for (v = size) max(0.001, v-2*rounding)]; + minkowski() { + cube(isize, center=true); + if (trimcorners) { + spheroid(r=rounding, style="octa", $fn=sides); + } else { + intersection() { + cyl(r=rounding, h=rounding*2, $fn=sides); + rotate([90,0,0]) cyl(r=rounding, h=rounding*2, $fn=sides); + rotate([0,90,0]) cyl(r=rounding, h=rounding*2, $fn=sides); + } + } + } + } + } else if (rounding<0) { + ard = abs(rounding); + cube(size, center=true); + + // External-Chamfer mask edges + difference() { + union() { + for (i = [0:3], axis=[0:1]) { + if (edges[axis][i]>0) { + vec = EDGE_OFFSETS[axis][i]; + translate(vmul(vec/2, size+[ard,ard,-ard])) { + rotate(majrots[axis]) { + cube([ard, ard, size[axis]], center=true); + } + } + } + } + + // Add multi-edge corners. + if (trimcorners) { + for (za=[-1,1], ya=[-1,1], xa=[-1,1]) { + if (corner_edge_count(edges, [xa,ya,za]) > 1) { + translate(vmul([xa,ya,za]/2, size+[ard-0.01,ard-0.01,-ard])) { + cube([ard+0.01,ard+0.01,ard], center=true); + } + } + } + } + } + + // Remove roundings from overhangs. + for (i = [0:3], axis=[0:1]) { + if (edges[axis][i]>0) { + vec = EDGE_OFFSETS[axis][i]; + translate(vmul(vec/2, size+[2*ard,2*ard,-2*ard])) { + rotate(majrots[axis]) { + cyl(l=size[axis]+2.1*ard, r=ard); + } + } + } + } + } + } else { + difference() { + cube(size, center=true); + + // Round edges. + for (i = [0:3], axis=[0:2]) { + if (edges[axis][i]>0) { + difference() { + translate(vmul(EDGE_OFFSETS[axis][i], size/2)) { + rotate(majrots[axis]) cube([rounding*2, rounding*2, size[axis]+0.1], center=true); + } + translate(vmul(EDGE_OFFSETS[axis][i], size/2 - [1,1,1]*rounding)) { + rotate(majrots[axis]) cyl(h=size[axis]+0.2, r=rounding, $fn=sides); + } + } + } + } + + // Round triple-edge corners. + if (trimcorners) { + for (za=[-1,1], ya=[-1,1], xa=[-1,1]) { + if (corner_edge_count(edges, [xa,ya,za]) > 2) { + difference() { + translate(vmul([xa,ya,za], size/2)) { + cube(rounding*2, center=true); + } + translate(vmul([xa,ya,za], size/2-[1,1,1]*rounding)) { + spheroid(r=rounding, style="octa", $fn=sides); + } + } + } + } + } + } + } + } else { + cube(size=size, center=true); + } + children(); + } + } +} + + + +// Section: Prismoids + + +// Function&Module: prismoid() +// +// Usage: As Module +// prismoid(size1, size2, h|l, [shift], [rounding], [chamfer]); +// prismoid(size1, size2, h|l, [shift], [rounding1], [rounding2], [chamfer1], [chamfer2]); +// Usage: As Function +// vnf = prismoid(size1, size2, h|l, [shift], [rounding], [chamfer]); +// vnf = prismoid(size1, size2, h|l, [shift], [rounding1], [rounding2], [chamfer1], [chamfer2]); +// +// Description: +// Creates a rectangular prismoid shape with optional roundovers and chamfering. +// You can only round or chamfer the vertical(ish) edges. For those edges, you can +// specify rounding and/or chamferring per-edge, and for top and bottom separately. +// Note: if using chamfers or rounding, you **must** also include the hull.scad file: +// ``` +// include +// ``` +// +// Arguments: +// size1 = [width, length] of the axis-negative end of the prism. +// size2 = [width, length] of the axis-positive end of the prism. +// h|l = Height of the prism. +// shift = [X,Y] amount to shift the center of the top with respect to the center of the bottom. +// rounding = The roundover radius for the edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no rounding) +// rounding1 = The roundover radius for the bottom corners of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. +// rounding2 = The roundover radius for the top corners of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. +// chamfer = The chamfer size for the edges of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no chamfer) +// chamfer1 = The chamfer size for the bottom corners of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. +// chamfer2 = The chamfer size for the top corners of the prismoid. Requires including hull.scad. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: Rectangular Pyramid +// prismoid([40,40], [0,0], h=20); +// Example: Prism +// prismoid(size1=[40,40], size2=[0,40], h=20); +// Example: Truncated Pyramid +// prismoid(size1=[35,50], size2=[20,30], h=20); +// Example: Wedge +// prismoid(size1=[60,35], size2=[30,0], h=30); +// Example: Truncated Tetrahedron +// prismoid(size1=[10,40], size2=[40,10], h=40); +// Example: Inverted Truncated Pyramid +// prismoid(size1=[15,5], size2=[30,20], h=20); +// Example: Right Prism +// prismoid(size1=[30,60], size2=[0,60], shift=[-15,0], h=30); +// Example(FlatSpin): Shifting/Skewing +// prismoid(size1=[50,30], size2=[20,20], h=20, shift=[15,5]); +// Example: Rounding +// include +// prismoid(100, 80, rounding=10, h=30); +// Example: Outer Chamfer Only +// include +// prismoid(100, 80, chamfer=5, h=30); +// Example: Gradiant Rounding +// include +// prismoid(100, 80, rounding1=10, rounding2=0, h=30); +// Example: Per Corner Rounding +// include +// prismoid(100, 80, rounding=[0,5,10,15], h=30); +// Example: Per Corner Chamfer +// include +// prismoid(100, 80, chamfer=[0,5,10,15], h=30); +// Example: Mixing Chamfer and Rounding +// include +// prismoid(100, 80, chamfer=[0,5,0,10], rounding=[5,0,10,0], h=30); +// Example: Really Mixing It Up +// include +// prismoid( +// size1=[100,80], size2=[80,60], h=20, +// chamfer1=[0,5,0,10], chamfer2=[5,0,10,0], +// rounding1=[5,0,10,0], rounding2=[0,5,0,10] +// ); +// Example(Spin): Standard Connectors +// prismoid(size1=[50,30], size2=[20,20], h=20, shift=[15,5]) +// show_anchors(); +module prismoid( + size1, size2, h, shift=[0,0], + rounding=0, rounding1, rounding2, + chamfer=0, chamfer1, chamfer2, + l, center, + anchor, spin=0, orient=UP +) { + assert(is_num(size1) || is_vector(size1,2)); + assert(is_num(size2) || is_vector(size2,2)); + assert(is_num(h) || is_num(l)); + assert(is_vector(shift,2)); + assert(is_num(rounding) || is_vector(rounding,4), "Bad rounding argument."); + assert(is_undef(rounding1) || is_num(rounding1) || is_vector(rounding1,4), "Bad rounding1 argument."); + assert(is_undef(rounding2) || is_num(rounding2) || is_vector(rounding2,4), "Bad rounding2 argument."); + assert(is_num(chamfer) || is_vector(chamfer,4), "Bad chamfer argument."); + assert(is_undef(chamfer1) || is_num(chamfer1) || is_vector(chamfer1,4), "Bad chamfer1 argument."); + assert(is_undef(chamfer2) || is_num(chamfer2) || is_vector(chamfer2,4), "Bad chamfer2 argument."); + eps = pow(2,-14); + size1 = is_num(size1)? [size1,size1] : size1; + size2 = is_num(size2)? [size2,size2] : size2; + s1 = [max(size1.x, eps), max(size1.y, eps)]; + s2 = [max(size2.x, eps), max(size2.y, eps)]; + rounding1 = default(rounding1, rounding); + rounding2 = default(rounding2, rounding); + chamfer1 = default(chamfer1, chamfer); + chamfer2 = default(chamfer2, chamfer); + anchor = get_anchor(anchor, center, BOT, BOT); + vnf = prismoid( + size1=size1, size2=size2, h=h, shift=shift, + rounding=rounding, rounding1=rounding1, rounding2=rounding2, + chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, + l=l, center=CENTER + ); + attachable(anchor,spin,orient, size=[s1.x,s1.y,h], size2=s2, shift=shift) { + vnf_polyhedron(vnf, convexity=4); + children(); + } +} + +function prismoid( + size1, size2, h, shift=[0,0], + rounding=0, rounding1, rounding2, + chamfer=0, chamfer1, chamfer2, + l, center, + anchor=DOWN, spin=0, orient=UP +) = + assert(is_vector(size1,2)) + assert(is_vector(size2,2)) + assert(is_num(h) || is_num(l)) + assert(is_vector(shift,2)) + assert(is_num(rounding) || is_vector(rounding,4), "Bad rounding argument.") + assert(is_undef(rounding1) || is_num(rounding1) || is_vector(rounding1,4), "Bad rounding1 argument.") + assert(is_undef(rounding2) || is_num(rounding2) || is_vector(rounding2,4), "Bad rounding2 argument.") + assert(is_num(chamfer) || is_vector(chamfer,4), "Bad chamfer argument.") + assert(is_undef(chamfer1) || is_num(chamfer1) || is_vector(chamfer1,4), "Bad chamfer1 argument.") + assert(is_undef(chamfer2) || is_num(chamfer2) || is_vector(chamfer2,4), "Bad chamfer2 argument.") + let( + eps = pow(2,-14), + h = first_defined([h,l,1]), + shiftby = point3d(point2d(shift)), + s1 = [max(size1.x, eps), max(size1.y, eps)], + s2 = [max(size2.x, eps), max(size2.y, eps)], + rounding1 = default(rounding1, rounding), + rounding2 = default(rounding2, rounding), + chamfer1 = default(chamfer1, chamfer), + chamfer2 = default(chamfer2, chamfer), + anchor = get_anchor(anchor, center, BOT, BOT), + vnf = (rounding1==0 && rounding2==0 && chamfer1==0 && chamfer2==0)? ( + let( + corners = [[1,1],[1,-1],[-1,-1],[-1,1]] * 0.5, + points = [ + for (p=corners) point3d(vmul(s2,p), +h/2) + shiftby, + for (p=corners) point3d(vmul(s1,p), -h/2) + ], + faces=[ + [0,1,2], [0,2,3], [0,4,5], [0,5,1], + [1,5,6], [1,6,2], [2,6,7], [2,7,3], + [3,7,4], [3,4,0], [4,7,6], [4,6,5], + ] + ) [points, faces] + ) : ( + let( + path1 = rect(size1, rounding=rounding1, chamfer=chamfer1, anchor=CTR), + path2 = rect(size2, rounding=rounding2, chamfer=chamfer2, anchor=CTR), + points = [ + each path3d(path1, -h/2), + each path3d(move(shiftby, p=path2), +h/2), + ], + faces = hull(points) + ) [points, faces] + ) + ) reorient(anchor,spin,orient, size=[s1.x,s1.y,h], size2=s2, shift=shift, p=vnf); + + +// Module: right_triangle() +// +// Usage: +// right_triangle(size, [center]); +// +// Description: +// Creates a 3D right triangular prism with the hypotenuse in the X+Y+ quadrant. +// +// Arguments: +// size = [width, thickness, height] +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `ALLNEG` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: Centered +// right_triangle([60, 40, 10], center=true); +// Example: *Non*-Centered +// right_triangle([60, 40, 10]); +// Example: Standard Connectors +// right_triangle([60, 40, 15]) show_anchors(); +module right_triangle(size=[1, 1, 1], center, anchor, spin=0, orient=UP) +{ + size = scalar_vec3(size); + anchor = get_anchor(anchor, center, ALLNEG, ALLNEG); + attachable(anchor,spin,orient, size=size) { + linear_extrude(height=size.z, convexity=2, center=true) { + polygon([[-size.x/2,-size.y/2], [-size.x/2,size.y/2], [size.x/2,-size.y/2]]); + } + children(); + } +} + + + +// Section: Cylindroids + + +// Module: cyl() +// +// Description: +// Creates cylinders in various anchors and orientations, +// with optional rounding and chamfers. You can use `r` and `l` +// interchangably, and all variants allow specifying size +// by either `r`|`d`, or `r1`|`d1` and `r2`|`d2`. +// Note that that chamfers and rounding cannot cross the +// midpoint of the cylinder's length. +// +// Usage: Normal Cylinders +// cyl(l|h, r|d, [circum], [realign], [center]); +// cyl(l|h, r1|d1, r2/d2, [circum], [realign], [center]); +// +// Usage: Chamferred Cylinders +// cyl(l|h, r|d, chamfer, [chamfang], [from_end], [circum], [realign], [center]); +// cyl(l|h, r|d, chamfer1, [chamfang1], [from_end], [circum], [realign], [center]); +// cyl(l|h, r|d, chamfer2, [chamfang2], [from_end], [circum], [realign], [center]); +// cyl(l|h, r|d, chamfer1, chamfer2, [chamfang1], [chamfang2], [from_end], [circum], [realign], [center]); +// +// Usage: Rounded End Cylinders +// cyl(l|h, r|d, rounding, [circum], [realign], [center]); +// cyl(l|h, r|d, rounding1, [circum], [realign], [center]); +// cyl(l|h, r|d, rounding2, [circum], [realign], [center]); +// cyl(l|h, r|d, rounding1, rounding2, [circum], [realign], [center]); +// +// Arguments: +// l / h = Length of cylinder along oriented axis. (Default: 1.0) +// r = Radius of cylinder. +// r1 = Radius of the negative (X-, Y-, Z-) end of cylinder. +// r2 = Radius of the positive (X+, Y+, Z+) end of cylinder. +// d = Diameter of cylinder. +// d1 = Diameter of the negative (X-, Y-, Z-) end of cylinder. +// d2 = Diameter of the positive (X+, Y+, Z+) end of cylinder. +// circum = If true, cylinder should circumscribe the circle of the given size. Otherwise inscribes. Default: `false` +// chamfer = The size of the chamfers on the ends of the cylinder. Default: none. +// chamfer1 = The size of the chamfer on the axis-negative end of the cylinder. Default: none. +// chamfer2 = The size of the chamfer on the axis-positive end of the cylinder. Default: none. +// chamfang = The angle in degrees of the chamfers on the ends of the cylinder. +// chamfang1 = The angle in degrees of the chamfer on the axis-negative end of the cylinder. +// chamfang2 = The angle in degrees of the chamfer on the axis-positive end of the cylinder. +// from_end = If true, chamfer is measured from the end of the cylinder, instead of inset from the edge. Default: `false`. +// rounding = The radius of the rounding on the ends of the cylinder. Default: none. +// rounding1 = The radius of the rounding on the axis-negative end of the cylinder. +// rounding2 = The radius of the rounding on the axis-positive end of the cylinder. +// realign = If true, rotate the cylinder by half the angle of one face. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=DOWN`. +// +// Example: By Radius +// xdistribute(30) { +// cyl(l=40, r=10); +// cyl(l=40, r1=10, r2=5); +// } +// +// Example: By Diameter +// xdistribute(30) { +// cyl(l=40, d=25); +// cyl(l=40, d1=25, d2=10); +// } +// +// Example: Chamferring +// xdistribute(60) { +// // Shown Left to right. +// cyl(l=40, d=40, chamfer=7); // Default chamfang=45 +// cyl(l=40, d=40, chamfer=7, chamfang=30, from_end=false); +// cyl(l=40, d=40, chamfer=7, chamfang=30, from_end=true); +// } +// +// Example: Rounding +// cyl(l=40, d=40, rounding=10); +// +// Example: Heterogenous Chamfers and Rounding +// ydistribute(80) { +// // Shown Front to Back. +// cyl(l=40, d=40, rounding1=15, orient=UP); +// cyl(l=40, d=40, chamfer2=5, orient=UP); +// cyl(l=40, d=40, chamfer1=12, rounding2=10, orient=UP); +// } +// +// Example: Putting it all together +// cyl(l=40, d1=25, d2=15, chamfer1=10, chamfang1=30, from_end=true, rounding2=5); +// +// Example: External Chamfers +// cyl(l=50, r=30, chamfer=-5, chamfang=30, $fa=1, $fs=1); +// +// Example: External Roundings +// cyl(l=50, r=30, rounding1=-5, rounding2=5, $fa=1, $fs=1); +// +// Example: Standard Connectors +// xdistribute(40) { +// cyl(l=30, d=25) show_anchors(); +// cyl(l=30, d1=25, d2=10) show_anchors(); +// } +// +module cyl( + l=undef, h=undef, + r=undef, r1=undef, r2=undef, + d=undef, d1=undef, d2=undef, + chamfer=undef, chamfer1=undef, chamfer2=undef, + chamfang=undef, chamfang1=undef, chamfang2=undef, + rounding=undef, rounding1=undef, rounding2=undef, + circum=false, realign=false, from_end=false, + center, anchor, spin=0, orient=UP +) { + r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=1); + r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=1); + l = first_defined([l, h, 1]); + sides = segs(max(r1,r2)); + sc = circum? 1/cos(180/sides) : 1; + phi = atan2(l, r2-r1); + anchor = get_anchor(anchor,center,BOT,CENTER); + attachable(anchor,spin,orient, r1=r1, r2=r2, l=l) { + zrot(realign? 180/sides : 0) { + if (!any_defined([chamfer, chamfer1, chamfer2, rounding, rounding1, rounding2])) { + cylinder(h=l, r1=r1*sc, r2=r2*sc, center=true, $fn=sides); + } else { + vang = atan2(l, r1-r2)/2; + chang1 = 90-first_defined([chamfang1, chamfang, vang]); + chang2 = 90-first_defined([chamfang2, chamfang, 90-vang]); + cham1 = first_defined([chamfer1, chamfer]) * (from_end? 1 : tan(chang1)); + cham2 = first_defined([chamfer2, chamfer]) * (from_end? 1 : tan(chang2)); + fil1 = first_defined([rounding1, rounding]); + fil2 = first_defined([rounding2, rounding]); + if (chamfer != undef) { + assert(chamfer <= r1, "chamfer is larger than the r1 radius of the cylinder."); + assert(chamfer <= r2, "chamfer is larger than the r2 radius of the cylinder."); + } + if (cham1 != undef) { + assert(cham1 <= r1, "chamfer1 is larger than the r1 radius of the cylinder."); + } + if (cham2 != undef) { + assert(cham2 <= r2, "chamfer2 is larger than the r2 radius of the cylinder."); + } + if (rounding != undef) { + assert(rounding <= r1, "rounding is larger than the r1 radius of the cylinder."); + assert(rounding <= r2, "rounding is larger than the r2 radius of the cylinder."); + } + if (fil1 != undef) { + assert(fil1 <= r1, "rounding1 is larger than the r1 radius of the cylinder."); + } + if (fil2 != undef) { + assert(fil2 <= r2, "rounding2 is larger than the r1 radius of the cylinder."); + } + dy1 = abs(first_defined([cham1, fil1, 0])); + dy2 = abs(first_defined([cham2, fil2, 0])); + assert(dy1+dy2 <= l, "Sum of fillets and chamfer sizes must be less than the length of the cylinder."); + + path = concat( + [[0,l/2]], + + !is_undef(cham2)? ( + let( + p1 = [r2-cham2/tan(chang2),l/2], + p2 = lerp([r2,l/2],[r1,-l/2],abs(cham2)/l) + ) [p1,p2] + ) : !is_undef(fil2)? ( + let( + cn = find_circle_2tangents([r2-fil2,l/2], [r2,l/2], [r1,-l/2], r=abs(fil2)), + ang = fil2<0? phi : phi-180, + steps = ceil(abs(ang)/360*segs(abs(fil2))), + step = ang/steps, + pts = [for (i=[0:1:steps]) let(a=90+i*step) cn[0]+abs(fil2)*[cos(a),sin(a)]] + ) pts + ) : [[r2,l/2]], + + !is_undef(cham1)? ( + let( + p1 = lerp([r1,-l/2],[r2,l/2],abs(cham1)/l), + p2 = [r1-cham1/tan(chang1),-l/2] + ) [p1,p2] + ) : !is_undef(fil1)? ( + let( + cn = find_circle_2tangents([r1-fil1,-l/2], [r1,-l/2], [r2,l/2], r=abs(fil1)), + ang = fil1<0? 180-phi : -phi, + steps = ceil(abs(ang)/360*segs(abs(fil1))), + step = ang/steps, + pts = [for (i=[0:1:steps]) let(a=(fil1<0?180:0)+(phi-90)+i*step) cn[0]+abs(fil1)*[cos(a),sin(a)]] + ) pts + ) : [[r1,-l/2]], + + [[0,-l/2]] + ); + rotate_extrude(convexity=2) { + polygon(path); + } + } + } + children(); + } +} + + + +// Module: xcyl() +// +// Description: +// Creates a cylinder oriented along the X axis. +// +// Usage: +// xcyl(l|h, r|d, [anchor]); +// xcyl(l|h, r1|d1, r2|d2, [anchor]); +// +// Arguments: +// l / h = Length of cylinder along oriented axis. (Default: `1.0`) +// r = Radius of cylinder. +// r1 = Optional radius of left (X-) end of cylinder. +// r2 = Optional radius of right (X+) end of cylinder. +// d = Optional diameter of cylinder. (use instead of `r`) +// d1 = Optional diameter of left (X-) end of cylinder. +// d2 = Optional diameter of right (X+) end of cylinder. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// +// Example: By Radius +// ydistribute(50) { +// xcyl(l=35, r=10); +// xcyl(l=35, r1=15, r2=5); +// } +// +// Example: By Diameter +// ydistribute(50) { +// xcyl(l=35, d=20); +// xcyl(l=35, d1=30, d2=10); +// } +module xcyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h=undef, anchor=CENTER) +{ + anchor = rot(from=RIGHT, to=UP, p=anchor); + cyl(l=l, h=h, r=r, r1=r1, r2=r2, d=d, d1=d1, d2=d2, orient=RIGHT, anchor=anchor) children(); +} + + + +// Module: ycyl() +// +// Description: +// Creates a cylinder oriented along the Y axis. +// +// Usage: +// ycyl(l|h, r|d, [anchor]); +// ycyl(l|h, r1|d1, r2|d2, [anchor]); +// +// Arguments: +// l / h = Length of cylinder along oriented axis. (Default: `1.0`) +// r = Radius of cylinder. +// r1 = Radius of front (Y-) end of cone. +// r2 = Radius of back (Y+) end of one. +// d = Diameter of cylinder. +// d1 = Diameter of front (Y-) end of one. +// d2 = Diameter of back (Y+) end of one. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// +// Example: By Radius +// xdistribute(50) { +// ycyl(l=35, r=10); +// ycyl(l=35, r1=15, r2=5); +// } +// +// Example: By Diameter +// xdistribute(50) { +// ycyl(l=35, d=20); +// ycyl(l=35, d1=30, d2=10); +// } +module ycyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h=undef, anchor=CENTER) +{ + anchor = rot(from=BACK, to=UP, p=anchor); + cyl(l=l, h=h, r=r, r1=r1, r2=r2, d=d, d1=d1, d2=d2, orient=BACK, anchor=anchor) children(); +} + + + +// Module: zcyl() +// +// Description: +// Creates a cylinder oriented along the Z axis. +// +// Usage: +// zcyl(l|h, r|d, [anchor]); +// zcyl(l|h, r1|d1, r2|d2, [anchor]); +// +// Arguments: +// l / h = Length of cylinder along oriented axis. (Default: 1.0) +// r = Radius of cylinder. +// r1 = Radius of front (Y-) end of cone. +// r2 = Radius of back (Y+) end of one. +// d = Diameter of cylinder. +// d1 = Diameter of front (Y-) end of one. +// d2 = Diameter of back (Y+) end of one. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// +// Example: By Radius +// xdistribute(50) { +// zcyl(l=35, r=10); +// zcyl(l=35, r1=15, r2=5); +// } +// +// Example: By Diameter +// xdistribute(50) { +// zcyl(l=35, d=20); +// zcyl(l=35, d1=30, d2=10); +// } +module zcyl(l=undef, r=undef, d=undef, r1=undef, r2=undef, d1=undef, d2=undef, h=undef, anchor=CENTER) +{ + cyl(l=l, h=h, r=r, r1=r1, r2=r2, d=d, d1=d1, d2=d2, orient=UP, anchor=anchor) children(); +} + + + +// Module: tube() +// +// Description: +// Makes a hollow tube with the given outer size and wall thickness. +// +// Usage: +// tube(h|l, ir|id, wall, [realign]); +// tube(h|l, or|od, wall, [realign]); +// tube(h|l, ir|id, or|od, [realign]); +// tube(h|l, ir1|id1, ir2|id2, wall, [realign]); +// tube(h|l, or1|od1, or2|od2, wall, [realign]); +// tube(h|l, ir1|id1, ir2|id2, or1|od1, or2|od2, [realign]); +// +// Arguments: +// h|l = height of tube. (Default: 1) +// or = Outer radius of tube. +// or1 = Outer radius of bottom of tube. (Default: value of r) +// or2 = Outer radius of top of tube. (Default: value of r) +// od = Outer diameter of tube. +// od1 = Outer diameter of bottom of tube. +// od2 = Outer diameter of top of tube. +// wall = horizontal thickness of tube wall. (Default 0.5) +// ir = Inner radius of tube. +// ir1 = Inner radius of bottom of tube. +// ir2 = Inner radius of top of tube. +// id = Inner diameter of tube. +// id1 = Inner diameter of bottom of tube. +// id2 = Inner diameter of top of tube. +// realign = If true, rotate the tube by half the angle of one face. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: These all Produce the Same Tube +// tube(h=30, or=40, wall=5); +// tube(h=30, ir=35, wall=5); +// tube(h=30, or=40, ir=35); +// tube(h=30, od=80, id=70); +// Example: These all Produce the Same Conical Tube +// tube(h=30, or1=40, or2=25, wall=5); +// tube(h=30, ir1=35, or2=20, wall=5); +// tube(h=30, or1=40, or2=25, ir1=35, ir2=20); +// Example: Circular Wedge +// tube(h=30, or1=40, or2=30, ir1=20, ir2=30); +// Example: Standard Connectors +// tube(h=30, or=40, wall=5) show_anchors(); +module tube( + h, wall=undef, + r=undef, r1=undef, r2=undef, + d=undef, d1=undef, d2=undef, + or=undef, or1=undef, or2=undef, + od=undef, od1=undef, od2=undef, + ir=undef, id=undef, ir1=undef, + ir2=undef, id1=undef, id2=undef, + anchor, spin=0, orient=UP, + center, realign=false, l +) { + h = first_defined([h,l,1]); + r1 = first_defined([or1, od1/2, r1, d1/2, or, od/2, r, d/2, ir1+wall, id1/2+wall, ir+wall, id/2+wall]); + r2 = first_defined([or2, od2/2, r2, d2/2, or, od/2, r, d/2, ir2+wall, id2/2+wall, ir+wall, id/2+wall]); + ir1 = first_defined([ir1, id1/2, ir, id/2, r1-wall, d1/2-wall, r-wall, d/2-wall]); + ir2 = first_defined([ir2, id2/2, ir, id/2, r2-wall, d2/2-wall, r-wall, d/2-wall]); + assert(ir1 <= r1, "Inner radius is larger than outer radius."); + assert(ir2 <= r2, "Inner radius is larger than outer radius."); + sides = segs(max(r1,r2)); + anchor = get_anchor(anchor, center, BOT, BOT); + attachable(anchor,spin,orient, r1=r1, r2=r2, l=h) { + zrot(realign? 180/sides : 0) { + difference() { + cyl(h=h, r1=r1, r2=r2, $fn=sides) children(); + cyl(h=h+0.05, r1=ir1, r2=ir2); + } + } + children(); + } +} + + +// Module: rect_tube() +// Usage: +// rect_tube(size, wall, h, [center]); +// rect_tube(isize, wall, h, [center]); +// rect_tube(size, isize, h, [center]); +// rect_tube(size1, size2, wall, h, [center]); +// rect_tube(isize1, isize2, wall, h, [center]); +// rect_tube(size1, size2, isize1, isize2, h, [center]); +// Description: +// Creates a rectangular or prismoid tube with optional roundovers and/or chamfers. +// You can only round or chamfer the vertical(ish) edges. For those edges, you can +// specify rounding and/or chamferring per-edge, and for top and bottom, inside and +// outside separately. +// Note: if using chamfers or rounding, you **must** also include the hull.scad file: +// ``` +// include +// ``` +// Arguments: +// size = The outer [X,Y] size of the rectangular tube. +// isize = The inner [X,Y] size of the rectangular tube. +// h|l = The height or length of the rectangular tube. Default: 1 +// wall = The thickness of the rectangular tube wall. +// size1 = The [X,Y] side of the outside of the bottom of the rectangular tube. +// size2 = The [X,Y] side of the outside of the top of the rectangular tube. +// isize1 = The [X,Y] side of the inside of the bottom of the rectangular tube. +// isize2 = The [X,Y] side of the inside of the top of the rectangular tube. +// rounding = The roundover radius for the outside edges of the rectangular tube. +// rounding1 = The roundover radius for the outside bottom corner of the rectangular tube. +// rounding2 = The roundover radius for the outside top corner of the rectangular tube. +// chamfer = The chamfer size for the outside edges of the rectangular tube. +// chamfer1 = The chamfer size for the outside bottom corner of the rectangular tube. +// chamfer2 = The chamfer size for the outside top corner of the rectangular tube. +// irounding = The roundover radius for the inside edges of the rectangular tube. Default: Same as `rounding` +// irounding1 = The roundover radius for the inside bottom corner of the rectangular tube. +// irounding2 = The roundover radius for the inside top corner of the rectangular tube. +// ichamfer = The chamfer size for the inside edges of the rectangular tube. Default: Same as `chamfer` +// ichamfer1 = The chamfer size for the inside bottom corner of the rectangular tube. +// ichamfer2 = The chamfer size for the inside top corner of the rectangular tube. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `BOTTOM` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Examples: +// rect_tube(size=50, wall=5, h=30); +// rect_tube(size=[100,60], wall=5, h=30); +// rect_tube(isize=[60,80], wall=5, h=30); +// rect_tube(size=[100,60], isize=[90,50], h=30); +// rect_tube(size1=[100,60], size2=[70,40], wall=5, h=30); +// rect_tube(size1=[100,60], size2=[70,40], isize1=[40,20], isize2=[65,35], h=15); +// Example: Outer Rounding Only +// include +// rect_tube(size=100, wall=5, rounding=10, irounding=0, h=30); +// Example: Outer Chamfer Only +// include +// rect_tube(size=100, wall=5, chamfer=5, ichamfer=0, h=30); +// Example: Outer Rounding, Inner Chamfer +// include +// rect_tube(size=100, wall=5, rounding=10, ichamfer=8, h=30); +// Example: Inner Rounding, Outer Chamfer +// include +// rect_tube(size=100, wall=5, chamfer=10, irounding=8, h=30); +// Example: Gradiant Rounding +// include +// rect_tube(size1=100, size2=80, wall=5, rounding1=10, rounding2=0, irounding1=8, irounding2=0, h=30); +// Example: Per Corner Rounding +// include +// rect_tube(size=100, wall=10, rounding=[0,5,10,15], irounding=0, h=30); +// Example: Per Corner Chamfer +// include +// rect_tube(size=100, wall=10, chamfer=[0,5,10,15], ichamfer=0, h=30); +// Example: Mixing Chamfer and Rounding +// include +// rect_tube(size=100, wall=10, chamfer=[0,5,0,10], ichamfer=0, rounding=[5,0,10,0], irounding=0, h=30); +// Example: Really Mixing It Up +// include +// rect_tube( +// size1=[100,80], size2=[80,60], +// isize1=[50,30], isize2=[70,50], h=20, +// chamfer1=[0,5,0,10], ichamfer1=[0,3,0,8], +// chamfer2=[5,0,10,0], ichamfer2=[3,0,8,0], +// rounding1=[5,0,10,0], irounding1=[3,0,8,0], +// rounding2=[0,5,0,10], irounding2=[0,3,0,8] +// ); +module rect_tube( + size, isize, + h, shift=[0,0], wall, + size1, size2, + isize1, isize2, + rounding=0, rounding1, rounding2, + irounding=0, irounding1, irounding2, + chamfer=0, chamfer1, chamfer2, + ichamfer=0, ichamfer1, ichamfer2, + anchor, spin=0, orient=UP, + center, l +) { + h = first_defined([h,l,1]); + assert(is_num(h), "l or h argument required."); + assert(is_vector(shift,2)); + s1 = is_num(size1)? [size1, size1] : + is_vector(size1,2)? size1 : + is_num(size)? [size, size] : + is_vector(size,2)? size : + undef; + s2 = is_num(size2)? [size2, size2] : + is_vector(size2,2)? size2 : + is_num(size)? [size, size] : + is_vector(size,2)? size : + undef; + is1 = is_num(isize1)? [isize1, isize1] : + is_vector(isize1,2)? isize1 : + is_num(isize)? [isize, isize] : + is_vector(isize,2)? isize : + undef; + is2 = is_num(isize2)? [isize2, isize2] : + is_vector(isize2,2)? isize2 : + is_num(isize)? [isize, isize] : + is_vector(isize,2)? isize : + undef; + size1 = is_def(s1)? s1 : + (is_def(wall) && is_def(is1))? (is1+2*[wall,wall]) : + undef; + size2 = is_def(s2)? s2 : + (is_def(wall) && is_def(is2))? (is2+2*[wall,wall]) : + undef; + isize1 = is_def(is1)? is1 : + (is_def(wall) && is_def(s1))? (s1-2*[wall,wall]) : + undef; + isize2 = is_def(is2)? is2 : + (is_def(wall) && is_def(s2))? (s2-2*[wall,wall]) : + undef; + assert(wall==undef || is_num(wall)); + assert(size1!=undef, "Bad size/size1 argument."); + assert(size2!=undef, "Bad size/size2 argument."); + assert(isize1!=undef, "Bad isize/isize1 argument."); + assert(isize2!=undef, "Bad isize/isize2 argument."); + assert(isize1.x < size1.x, "Inner size is larger than outer size."); + assert(isize1.y < size1.y, "Inner size is larger than outer size."); + assert(isize2.x < size2.x, "Inner size is larger than outer size."); + assert(isize2.y < size2.y, "Inner size is larger than outer size."); + anchor = get_anchor(anchor, center, BOT, BOT); + attachable(anchor,spin,orient, size=[each size1, h], size2=size2, shift=shift) { + diff("_H_o_L_e_") + prismoid( + size1, size2, h=h, shift=shift, + rounding=rounding, rounding1=rounding1, rounding2=rounding2, + chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, + anchor=CTR + ) { + children(); + tags("_H_o_L_e_") prismoid( + isize1, isize2, h=h+0.05, shift=shift, + rounding=irounding, rounding1=irounding1, rounding2=irounding2, + chamfer=ichamfer, chamfer1=ichamfer1, chamfer2=ichamfer2, + anchor=CTR + ); + } + children(); + } +} + + +// Module: torus() +// +// Descriptiom: +// Creates a torus shape. +// +// Usage: +// torus(r|d, r2|d2); +// torus(or|od, ir|id); +// +// Arguments: +// r = major radius of torus ring. (use with of 'r2', or 'd2') +// r2 = minor radius of torus ring. (use with of 'r', or 'd') +// d = major diameter of torus ring. (use with of 'r2', or 'd2') +// d2 = minor diameter of torus ring. (use with of 'r', or 'd') +// or = outer radius of the torus. (use with 'ir', or 'id') +// ir = inside radius of the torus. (use with 'or', or 'od') +// od = outer diameter of the torus. (use with 'ir' or 'id') +// id = inside diameter of the torus. (use with 'or' or 'od') +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: +// // These all produce the same torus. +// torus(r=22.5, r2=7.5); +// torus(d=45, d2=15); +// torus(or=30, ir=15); +// torus(od=60, id=30); +// Example: Standard Connectors +// torus(od=60, id=30) show_anchors(); +module torus( + r=undef, d=undef, + r2=undef, d2=undef, + or=undef, od=undef, + ir=undef, id=undef, + center, anchor, spin=0, orient=UP +) { + orr = get_radius(r=or, d=od, dflt=1.0); + irr = get_radius(r=ir, d=id, dflt=0.5); + majrad = get_radius(r=r, d=d, dflt=(orr+irr)/2); + minrad = get_radius(r=r2, d=d2, dflt=(orr-irr)/2); + anchor = get_anchor(anchor, center, BOT, CENTER); + attachable(anchor,spin,orient, r=(majrad+minrad), l=minrad*2) { + rotate_extrude(convexity=4) { + right(majrad) circle(r=minrad); + } + children(); + } +} + + + +// Section: Spheroid + + +// Function&Module: spheroid() +// Usage: As Module +// spheroid(r|d, [circum], [style]) +// Usage: As Function +// vnf = spheroid(r|d, [circum], [style]) +// Description: +// Creates a spheroid object, with support for anchoring and attachments. +// This is a drop-in replacement for the built-in `sphere()` module. +// When called as a function, returns a [VNF](vnf.scad) for a spheroid. +// Arguments: +// r = Radius of the spheroid. +// d = Diameter of the spheroid. +// circum = If true, the spheroid is made large enough to circumscribe the sphere of the ideal side. Otherwise inscribes. Default: false (inscribes) +// style = The style of the spheroid's construction. One of "orig", "aligned", "stagger", "octa", or "icosa". Default: "aligned" +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Example: By Radius +// spheroid(r=50); +// Example: By Diameter +// spheroid(d=100); +// Example: style="orig" +// spheroid(d=100, style="orig", $fn=10); +// Example: style="aligned" +// spheroid(d=100, style="aligned", $fn=10); +// Example: style="stagger" +// spheroid(d=100, style="stagger", $fn=10); +// Example: style="octa", octahedral based tesselation. +// spheroid(d=100, style="octa", $fn=10); +// // In "octa" style, $fn is quantized +// // to the nearest multiple of 4. +// Example: style="icosa", icosahedral based tesselation. +// spheroid(d=100, style="icosa", $fn=10); +// // In "icosa" style, $fn is quantized +// // to the nearest multiple of 5. +// Example: Anchoring +// spheroid(d=100, anchor=FRONT); +// Example: Spin +// spheroid(d=100, anchor=FRONT, spin=45); +// Example: Orientation +// spheroid(d=100, anchor=FRONT, spin=45, orient=FWD); +// Example: Standard Connectors +// spheroid(d=50) show_anchors(); +// Example: Called as Function +// vnf = spheroid(d=100, style="icosa"); +// vnf_polyhedron(vnf); +module spheroid(r, d, circum=false, style="aligned", anchor=CENTER, spin=0, orient=UP) +{ + r = get_radius(r=r, d=d, dflt=1); + sides = segs(r); + attachable(anchor,spin,orient, r=r) { + if (style=="orig") { + rotate_extrude(convexity=2,$fn=sides) { + difference() { + oval(r=r, circum=circum, $fn=sides); + left(r) square(2*r,center=true); + } + } + } else if (style=="aligned") { + rotate_extrude(convexity=2,$fn=sides) { + difference() { + zrot(180/sides) oval(r=r, circum=circum, $fn=sides); + left(r) square(2*r,center=true); + } + } + } else { + vnf = spheroid(r=r, circum=circum, style=style); + vnf_polyhedron(vnf, convexity=2); + } + children(); + } +} + + +function spheroid(r, d, circum=false, style="aligned", anchor=CENTER, spin=0, orient=UP) = + let( + r = get_radius(r=r, d=d, dflt=1), + hsides = segs(r), + vsides = max(2,ceil(hsides/2)), + octa_steps = round(max(4,hsides)/4), + icosa_steps = round(max(5,hsides)/5), + rr = circum? (r / cos(90/vsides) / cos(180/hsides)) : r, + stagger = style=="stagger", + verts = style=="orig"? [ + for (i=[0:1:vsides-1]) let(phi = (i+0.5)*180/(vsides)) + for (j=[0:1:hsides-1]) let(theta = j*360/hsides) + spherical_to_xyz(rr, theta, phi), + ] : style=="aligned" || style=="stagger"? [ + spherical_to_xyz(rr, 0, 0), + for (i=[1:1:vsides-1]) let(phi = i*180/vsides) + for (j=[0:1:hsides-1]) let(theta = (j+((stagger && i%2!=0)?0.5:0))*360/hsides) + spherical_to_xyz(rr, theta, phi), + spherical_to_xyz(rr, 0, 180) + ] : style=="octa"? let( + meridians = [ + 1, + for (i = [1:1:octa_steps]) i*4, + for (i = [octa_steps-1:-1:1]) i*4, + 1, + ] + ) [ + for (i=idx(meridians), j=[0:1:meridians[i]-1]) + spherical_to_xyz(rr, j*360/meridians[i], i*180/(len(meridians)-1)) + ] : style=="icosa"? [ + for (tb=[0,1], j=[0,2], i = [0:1:4]) let( + theta0 = i*360/5, + theta1 = (i-0.5)*360/5, + theta2 = (i+0.5)*360/5, + phi0 = 180/3 * j, + phi1 = 180/3, + v0 = spherical_to_xyz(1,theta0,phi0), + v1 = spherical_to_xyz(1,theta1,phi1), + v2 = spherical_to_xyz(1,theta2,phi1), + ax0 = vector_axis(v0, v1), + ang0 = vector_angle(v0, v1), + ax1 = vector_axis(v0, v2), + ang1 = vector_angle(v0, v2) + ) + for (k = [0:1:icosa_steps]) let( + u = k/icosa_steps, + vv0 = rot(ang0*u, ax0, p=v0), + vv1 = rot(ang1*u, ax1, p=v0), + ax2 = vector_axis(vv0, vv1), + ang2 = vector_angle(vv0, vv1) + ) + for (l = [0:1:k]) let( + v = k? l/k : 0, + pt = rot(ang2*v, v=ax2, p=vv0) * rr * (tb? -1 : 1) + ) pt + ] : assert(in_list(style,["orig","aligned","stagger","octa","icosa"])), + lv = len(verts), + faces = style=="orig"? [ + [for (i=[0:1:hsides-1]) hsides-i-1], + [for (i=[0:1:hsides-1]) lv-hsides+i], + for (i=[0:1:vsides-2], j=[0:1:hsides-1]) each [ + [(i+1)*hsides+j, i*hsides+j, i*hsides+(j+1)%hsides], + [(i+1)*hsides+j, i*hsides+(j+1)%hsides, (i+1)*hsides+(j+1)%hsides], + ] + ] : style=="aligned" || style=="stagger"? [ + for (i=[0:1:hsides-1]) let( + b2 = lv-2-hsides + ) each [ + [i+1, 0, ((i+1)%hsides)+1], + [lv-1, b2+i+1, b2+((i+1)%hsides)+1], + ], + for (i=[0:1:vsides-3], j=[0:1:hsides-1]) let( + base = 1 + hsides*i + ) each ( + (stagger && i%2!=0)? [ + [base+j, base+hsides+j%hsides, base+hsides+(j+hsides-1)%hsides], + [base+j, base+(j+1)%hsides, base+hsides+j], + ] : [ + [base+j, base+(j+1)%hsides, base+hsides+(j+1)%hsides], + [base+j, base+hsides+(j+1)%hsides, base+hsides+j], + ] + ) + ] : style=="octa"? let( + meridians = [ + 0, 1, + for (i = [1:1:octa_steps]) i*4, + for (i = [octa_steps-1:-1:1]) i*4, + 1, + ], + offs = cumsum(meridians), + pc = select(offs,-1)-1, + os = octa_steps * 2 + ) [ + for (i=[0:1:3]) [0, 1+(i+1)%4, 1+i], + for (i=[0:1:3]) [pc-0, pc-(1+(i+1)%4), pc-(1+i)], + for (i=[1:1:octa_steps-1]) let( + m = meridians[i+2]/4 + ) + for (j=[0:1:3], k=[0:1:m-1]) let( + m1 = meridians[i+1], + m2 = meridians[i+2], + p1 = offs[i+0] + (j*m1/4 + k+0) % m1, + p2 = offs[i+0] + (j*m1/4 + k+1) % m1, + p3 = offs[i+1] + (j*m2/4 + k+0) % m2, + p4 = offs[i+1] + (j*m2/4 + k+1) % m2, + p5 = offs[os-i+0] + (j*m1/4 + k+0) % m1, + p6 = offs[os-i+0] + (j*m1/4 + k+1) % m1, + p7 = offs[os-i-1] + (j*m2/4 + k+0) % m2, + p8 = offs[os-i-1] + (j*m2/4 + k+1) % m2 + ) each [ + [p1, p4, p3], + if (k0) [v1-1,v1,v2], + [v1,v3,v2], + ], + faces2 = (tb+j)%2? [for (f=faces) reverse(f)] : faces + ) each faces2 + ] : [] + ) [reorient(anchor,spin,orient, r=r, p=verts), faces]; + + + +// Section: 3D Printing Shapes + + +// Module: teardrop() +// +// Description: +// Makes a teardrop shape in the XZ plane. Useful for 3D printable holes. +// +// Usage: +// teardrop(r|d, l|h, [ang], [cap_h]) +// +// Arguments: +// r = Radius of circular part of teardrop. (Default: 1) +// d = Diameter of circular portion of bottom. (Use instead of r) +// l = Thickness of teardrop. (Default: 1) +// ang = Angle of hat walls from the Z axis. (Default: 45 degrees) +// cap_h = If given, height above center where the shape will be truncated. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: Typical Shape +// teardrop(r=30, h=10, ang=30); +// Example: Crop Cap +// teardrop(r=30, h=10, ang=30, cap_h=40); +// Example: Close Crop +// teardrop(r=30, h=10, ang=30, cap_h=20); +module teardrop(r=undef, d=undef, l=undef, h=undef, ang=45, cap_h=undef, anchor=CENTER, spin=0, orient=UP) +{ + r = get_radius(r=r, d=d, dflt=1); + l = first_defined([l, h, 1]); + size = [r*2,l,r*2]; + attachable(anchor,spin,orient, size=size) { + rot(from=UP,to=FWD) { + linear_extrude(height=l, center=true, slices=2) { + teardrop2d(r=r, ang=ang, cap_h=cap_h); + } + } + children(); + } +} + + +// Module: onion() +// +// Description: +// Creates a sphere with a conical hat, to make a 3D teardrop. +// +// Usage: +// onion(r|d, [maxang], [cap_h]); +// +// Arguments: +// r = radius of spherical portion of the bottom. (Default: 1) +// d = diameter of spherical portion of bottom. +// cap_h = height above sphere center to truncate teardrop shape. +// maxang = angle of cone on top from vertical. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: Typical Shape +// onion(r=30, maxang=30); +// Example: Crop Cap +// onion(r=30, maxang=30, cap_h=40); +// Example: Close Crop +// onion(r=30, maxang=30, cap_h=20); +// Example: Standard Connectors +// onion(r=30, maxang=30, cap_h=40) show_anchors(); +module onion(cap_h=undef, r=undef, d=undef, maxang=45, h=undef, anchor=CENTER, spin=0, orient=UP) +{ + r = get_radius(r=r, d=d, dflt=1); + h = first_defined([cap_h, h]); + maxd = 3*r/tan(maxang); + anchors = [ + ["cap", [0,0,h], UP, 0] + ]; + attachable(anchor,spin,orient, r=r, anchors=anchors) { + rotate_extrude(convexity=2) { + difference() { + teardrop2d(r=r, ang=maxang, cap_h=h); + left(r) square(size=[2*r,maxd], center=true); + } + } + children(); + } +} + + + +// Section: Miscellaneous + + +// Module: nil() +// +// Description: +// Useful when you MUST pass a child to a module, but you want it to be nothing. +module nil() union(){} + + +// Module: noop() +// +// Description: +// Passes through the children passed to it, with no action at all. +// Useful while debugging when you want to replace a command. +// +// Arguments: +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +module noop(spin=0, orient=UP) attachable(CENTER,spin,orient, d=0.01) {nil(); children();} + + +// Module: pie_slice() +// +// Description: +// Creates a pie slice shape. +// +// Usage: +// pie_slice(ang, l|h, r|d, [center]); +// pie_slice(ang, l|h, r1|d1, r2|d2, [center]); +// +// Arguments: +// ang = pie slice angle in degrees. +// h = height of pie slice. +// r = radius of pie slice. +// r1 = bottom radius of pie slice. +// r2 = top radius of pie slice. +// d = diameter of pie slice. +// d1 = bottom diameter of pie slice. +// d2 = top diameter of pie slice. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=UP`. +// +// Example: Cylindrical Pie Slice +// pie_slice(ang=45, l=20, r=30); +// Example: Conical Pie Slice +// pie_slice(ang=60, l=20, d1=50, d2=70); +module pie_slice( + ang=30, l=undef, + r=undef, r1=undef, r2=undef, + d=undef, d1=undef, d2=undef, + h=undef, center, + anchor, spin=0, orient=UP +) { + l = first_defined([l, h, 1]); + r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=10); + r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=10); + maxd = max(r1,r2)+0.1; + anchor = get_anchor(anchor, center, BOT, BOT); + attachable(anchor,spin,orient, r1=r1, r2=r2, l=l) { + difference() { + cyl(r1=r1, r2=r2, h=l); + if (ang<180) rotate(ang) back(maxd/2) cube([2*maxd, maxd, l+0.1], center=true); + difference() { + fwd(maxd/2) cube([2*maxd, maxd, l+0.2], center=true); + if (ang>180) rotate(ang-180) back(maxd/2) cube([2*maxd, maxd, l+0.1], center=true); + } + } + children(); + } +} + + +// Module: interior_fillet() +// +// Description: +// Creates a shape that can be unioned into a concave joint between two faces, to fillet them. +// Center this part along the concave edge to be chamfered and union it in. +// +// Usage: +// interior_fillet(l, r, [ang], [overlap]); +// +// Arguments: +// l = length of edge to fillet. +// r = radius of fillet. +// ang = angle between faces to fillet. +// overlap = overlap size for unioning with faces. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `FRONT+LEFT` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: +// union() { +// translate([0,2,-4]) cube([20, 4, 24], anchor=BOTTOM); +// translate([0,-10,-4]) cube([20, 20, 4], anchor=BOTTOM); +// color("green") interior_fillet(l=20, r=10, spin=180, orient=RIGHT); +// } +// +// Example: +// interior_fillet(l=40, r=10, spin=-90); +// +// Example: Using with Attachments +// cube(50,center=true) { +// position(FRONT+LEFT) +// interior_fillet(l=50, r=10, spin=-90); +// position(BOT+FRONT) +// interior_fillet(l=50, r=10, spin=180, orient=RIGHT); +// } +module interior_fillet(l=1.0, r=1.0, ang=90, overlap=0.01, anchor=FRONT+LEFT, spin=0, orient=UP) { + dy = r/tan(ang/2); + steps = ceil(segs(r)*ang/360); + step = ang/steps; + attachable(anchor,spin,orient, size=[r,r,l]) { + linear_extrude(height=l, convexity=4, center=true) { + path = concat( + [[0,0]], + [for (i=[0:1:steps]) let(a=270-i*step) r*[cos(a),sin(a)]+[dy,r]] + ); + translate(-[r,r]/2) polygon(path); + } + children(); + } +} + + + +// Module: slot() +// +// Description: +// Makes a linear slot with rounded ends, appropriate for bolts to slide along. +// +// Usage: +// slot(h, l, r|d, [center]); +// slot(h, p1, p2, r|d, [center]); +// slot(h, l, r1|d1, r2|d2, [center]); +// slot(h, p1, p2, r1|d1, r2|d2, [center]); +// +// Arguments: +// p1 = center of starting circle of slot. +// p2 = center of ending circle of slot. +// l = length of slot along the X axis. +// h = height of slot shape. (default: 10) +// r = radius of slot circle. (default: 5) +// r1 = bottom radius of slot cone. +// r2 = top radius of slot cone. +// d = diameter of slot circle. +// d1 = bottom diameter of slot cone. +// d2 = top diameter of slot cone. +// +// Example: Between Two Points +// slot([0,0,0], [50,50,0], r1=5, r2=10, h=5); +// Example: By Length +// slot(l=50, r1=5, r2=10, h=5); +module slot( + p1=undef, p2=undef, h=10, l=undef, + r=undef, r1=undef, r2=undef, + d=undef, d1=undef, d2=undef +) { + r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=5); + r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=5); + sides = quantup(segs(max(r1, r2)), 4); + // TODO: implement orient and anchors. + hull() line_of(p1=p1, p2=p2, l=l, n=2) cyl(l=h, r1=r1, r2=r2, center=true, $fn=sides); +} + + +// Module: arced_slot() +// +// Description: +// Makes an arced slot, appropriate for bolts to slide along. +// +// Usage: +// arced_slot(h, r|d, sr|sd, [sa], [ea], [center], [$fn2]); +// arced_slot(h, r|d, sr1|sd1, sr2|sd2, [sa], [ea], [center], [$fn2]); +// +// Arguments: +// cp = Centerpoint of slot arc. Default: `[0, 0, 0]` +// h = Height of slot arc shape. Default: `1` +// r = Radius of slot arc. Default: `0.5` +// d = Diameter of slot arc. Default: `1` +// sr = Radius of slot channel. Default: `0.5` +// sd = Diameter of slot channel. Default: `0.5` +// sr1 = Bottom radius of slot channel cone. Use instead of `sr`. +// sr2 = Top radius of slot channel cone. Use instead of `sr`. +// sd1 = Bottom diameter of slot channel cone. Use instead of `sd`. +// sd2 = Top diameter of slot channel cone. Use instead of `sd`. +// sa = Starting angle. Default: `0` +// ea = Ending angle. Default: `90` +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// $fn2 = The `$fn` value to use on the small round endcaps. The major arcs are still based on `$fn`. Default: `$fn` +// +// Example(Med): Typical Arced Slot +// arced_slot(d=60, h=5, sd=10, sa=60, ea=280); +// Example(Med): Conical Arced Slot +// arced_slot(r=60, h=5, sd1=10, sd2=15, sa=45, ea=180); +module arced_slot( + r=undef, d=undef, h=1.0, + sr=undef, sr1=undef, sr2=undef, + sd=undef, sd1=undef, sd2=undef, + sa=0, ea=90, cp=[0,0,0], + anchor=TOP, spin=0, orient=UP, + $fn2 = undef +) { + r = get_radius(r=r, d=d, dflt=2); + sr1 = get_radius(r1=sr1, r=sr, d1=sd1, d=sd, dflt=2); + sr2 = get_radius(r1=sr2, r=sr, d1=sd2, d=sd, dflt=2); + fn_minor = first_defined([$fn2, $fn]); + da = ea - sa; + attachable(anchor,spin,orient, r1=r+sr1, r2=r+sr2, l=h) { + translate(cp) { + zrot(sa) { + difference() { + pie_slice(ang=da, l=h, r1=r+sr1, r2=r+sr2, orient=UP, anchor=CENTER); + cyl(h=h+0.1, r1=r-sr1, r2=r-sr2); + } + right(r) cyl(h=h, r1=sr1, r2=sr2, $fn=fn_minor); + zrot(da) right(r) cyl(h=h, r1=sr1, r2=sr2, $fn=fn_minor); + } + } + children(); + } +} + + +// Module: heightfield() +// Usage: +// heightfield(heightfield, [size], [bottom]); +// Description: +// Given a regular rectangular 2D grid of scalar values, generates a 3D surface where the height at +// any given point is the scalar value for that position. +// Arguments: +// heightfield = The 2D rectangular array of heights. +// size = The [X,Y] size of the surface to create. If given as a scalar, use it for both X and Y sizes. +// bottom = The Z coordinate for the bottom of the heightfield object to create. Must be less than the minimum heightfield value. Default: 0 +// convexity = Max number of times a line could intersect a wall of the surface being formed. +// Example: +// heightfield(size=[100,100], bottom=-20, heightfield=[ +// for (x=[-180:4:180]) [for(y=[-180:4:180]) 10*cos(3*norm([x,y]))] +// ]); +// Example: +// intersection() { +// heightfield(size=[100,100], heightfield=[ +// for (x=[-180:5:180]) [for(y=[-180:5:180]) 10+5*cos(3*x)*sin(3*y)] +// ]); +// cylinder(h=50,d=100); +// } +module heightfield(heightfield, size=[100,100], bottom=0, convexity=10) +{ + size = is_num(size)? [size,size] : point2d(size); + dim = array_dim(heightfield); + assert(dim.x!=undef); + assert(dim.y!=undef); + assert(bottom +// ``` +////////////////////////////////////////////////////////////////////// + + +// Section: 2D Drawing Helpers + +// Module: stroke() +// Usage: +// stroke(path, [width], [closed], [endcaps], [endcap_width], [endcap_length], [endcap_extent], [trim]); +// stroke(path, [width], [closed], [endcap1], [endcap2], [endcap_width1], [endcap_width2], [endcap_length1], [endcap_length2], [endcap_extent1], [endcap_extent2], [trim1], [trim2]); +// Description: +// Draws a 2D or 3D path with a given line width. Endcaps can be specified for each end individually. +// Figure(2D,Big): Endcap Types +// endcaps = [ +// ["butt", "square", "round", "chisel", "tail", "tail2"], +// ["line", "cross", "dot", "diamond", "x", "arrow", "arrow2"] +// ]; +// for (x=idx(endcaps), y=idx(endcaps[x])) { +// cap = endcaps[x][y]; +// right(x*60-60+5) fwd(y*10+15) { +// right(28) color("black") text(text=cap, size=5, halign="left", valign="center"); +// stroke([[0,0], [20,0]], width=3, endcap_width=3, endcap1=false, endcap2=cap); +// color("black") stroke([[0,0], [20,0]], width=0.25, endcaps=false); +// } +// } +// Arguments: +// path = The 2D path to draw along. +// width = The width of the line to draw. If given as a list of widths, (one for each path point), draws the line with varying thickness to each point. +// closed = If true, draw an additional line from the end of the path to the start. +// endcaps = Specifies the endcap type for both ends of the line. If a 2D path is given, use that to draw custom endcaps. +// endcap1 = Specifies the endcap type for the start of the line. If a 2D path is given, use that to draw a custom endcap. +// endcap2 = Specifies the endcap type for the end of the line. If a 2D path is given, use that to draw a custom endcap. +// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width. Default: 3.5 +// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width. Default: 3.5 +// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width. Default: 3.5 +// endcap_length = Length of endcaps, in multiples of the line width. Default: `endcap_width*0.5` +// endcap_length1 = Length of starting endcap, in multiples of the line width. Default: `endcap_width1*0.5` +// endcap_length2 = Length of ending endcap, in multiples of the line width. Default: `endcap_width2*0.5` +// endcap_extent = Extents length of endcaps, in multiples of the line width. Default: `endcap_width*0.5` +// endcap_extent1 = Extents length of starting endcap, in multiples of the line width. Default: `endcap_width1*0.5` +// endcap_extent2 = Extents length of ending endcap, in multiples of the line width. Default: `endcap_width2*0.5` +// endcap_angle = Extra axial rotation given to flat endcaps for 3D paths, in degrees. If not given, the endcaps are fully spun. Default: `undef` (Fully spun cap) +// endcap_angle1 = Extra axial rotation given to a flat starting endcap for 3D paths, in degrees. If not given, the endcap is fully spun. Default: `undef` (Fully spun cap) +// endcap_angle2 = Extra axial rotation given to a flat ending endcap for 3D paths, in degrees. If not given, the endcap is fully spun. Default: `undef` (Fully spun cap) +// trim = Trim the the start and end line segments by this much, to keep them from interfering with custom endcaps. +// trim1 = Trim the the starting line segment by this much, to keep it from interfering with a custom endcap. +// trim2 = Trim the the ending line segment by this much, to keep it from interfering with a custom endcap. +// convexity = Max number of times a line could intersect a wall of an endcap. +// hull = If true, use `hull()` to make higher quality joints between segments, at the cost of being much slower. Default: true +// Example(2D): Drawing a Path +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=20); +// Example(2D): Closing a Path +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=20, endcaps=true, closed=true); +// Example(2D): Fancy Arrow Endcaps +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=10, endcaps="arrow2"); +// Example(2D): Modified Fancy Arrow Endcaps +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=10, endcaps="arrow2", endcap_width=6, endcap_length=3, endcap_extent=2); +// Example(2D): Mixed Endcaps +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=10, endcap1="tail2", endcap2="arrow2"); +// Example(2D): Custom Endcap Shapes +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// arrow = [[0,0], [2,-3], [0.5,-2.3], [2,-4], [0.5,-3.5], [-0.5,-3.5], [-2,-4], [-0.5,-2.3], [-2,-3]]; +// stroke(path, width=10, trim=3.5, endcaps=arrow); +// Example(2D): Variable Line Width +// path = circle(d=50,$fn=18); +// widths = [for (i=idx(path)) 10*i/len(path)+2]; +// stroke(path,width=widths,$fa=1,$fs=1); +// Example: 3D Path with Endcaps +// path = rot([15,30,0], p=path3d(pentagon(d=50))); +// stroke(path, width=2, endcaps="arrow2", $fn=18); +// Example: 3D Path with Flat Endcaps +// path = rot([15,30,0], p=path3d(pentagon(d=50))); +// stroke(path, width=2, endcaps="arrow2", endcap_angle=0, $fn=18); +// Example: 3D Path with Mixed Endcaps +// path = rot([15,30,0], p=path3d(pentagon(d=50))); +// stroke(path, width=2, endcap1="arrow2", endcap2="tail", endcap_angle2=0, $fn=18); +module stroke( + path, width=1, closed=false, + endcaps, endcap1, endcap2, + trim, trim1, trim2, + endcap_width, endcap_width1, endcap_width2, + endcap_length, endcap_length1, endcap_length2, + endcap_extent, endcap_extent1, endcap_extent2, + endcap_angle, endcap_angle1, endcap_angle2, + convexity=10, hull=true +) { + function _endcap_shape(cap,linewidth,w,l,l2) = ( + let(sq2=sqrt(2), l3=l-l2) + (cap=="round" || cap==true)? circle(d=1, $fn=max(8, segs(w/2))) : + cap=="chisel"? [[-0.5,0], [0,0.5], [0.5,0], [0,-0.5]] : + cap=="square"? [[-0.5,-0.5], [-0.5,0.5], [0.5,0.5], [0.5,-0.5]] : + cap=="diamond"? [[0,w/2], [w/2,0], [0,-w/2], [-w/2,0]] : + cap=="dot"? circle(d=3, $fn=max(12, segs(w*3/2))) : + cap=="x"? [for (a=[0:90:270]) each rot(a,p=[[w+sq2/2,w-sq2/2]/2, [w-sq2/2,w+sq2/2]/2, [0,sq2/2]]) ] : + cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[1,w]/2, [-1,w]/2, [-1,1]/2]) ] : + cap=="line"? [[w/2,0.5], [w/2,-0.5], [-w/2,-0.5], [-w/2,0.5]] : + cap=="arrow"? [[0,0], [w/2,-l2], [w/2,-l2-l], [0,-l], [-w/2,-l2-l], [-w/2,-l2]] : + cap=="arrow2"? [[0,0], [w/2,-l2-l], [0,-l], [-w/2,-l2-l]] : + cap=="tail"? [[0,0], [w/2,l2], [w/2,l2-l], [0,-l], [-w/2,l2-l], [-w/2,l2]] : + cap=="tail2"? [[w/2,0], [w/2,-l], [0,-l-l2], [-w/2,-l], [-w/2,0]] : + is_path(cap)? cap : + [] + ) * linewidth; + + assert(is_bool(closed)); + assert(is_list(path)); + if (len(path) > 1) { + assert(is_path(path,[2,3]), "The path argument must be a list of 2D or 3D points."); + } + path = deduplicate( closed? close_path(path) : path ); + + assert(is_num(width) || (is_vector(width) && len(width)==len(path))); + width = is_num(width)? [for (x=path) width] : width; + + endcap1 = first_defined([endcap1, endcaps, "round"]); + endcap2 = first_defined([endcap2, endcaps, "round"]); + assert(is_bool(endcap1) || is_string(endcap1) || is_path(endcap1)); + assert(is_bool(endcap2) || is_string(endcap2) || is_path(endcap2)); + + endcap_width1 = first_defined([endcap_width1, endcap_width, 3.5]); + endcap_width2 = first_defined([endcap_width2, endcap_width, 3.5]); + assert(is_num(endcap_width1)); + assert(is_num(endcap_width2)); + + endcap_length1 = first_defined([endcap_length1, endcap_length, endcap_width1*0.5]); + endcap_length2 = first_defined([endcap_length2, endcap_length, endcap_width2*0.5]); + assert(is_num(endcap_length1)); + assert(is_num(endcap_length2)); + + endcap_extent1 = first_defined([endcap_extent1, endcap_extent, endcap_width1*0.5]); + endcap_extent2 = first_defined([endcap_extent2, endcap_extent, endcap_width2*0.5]); + assert(is_num(endcap_extent1)); + assert(is_num(endcap_extent2)); + + endcap_angle1 = first_defined([endcap_angle1, endcap_angle]); + endcap_angle2 = first_defined([endcap_angle2, endcap_angle]); + assert(is_undef(endcap_angle1)||is_num(endcap_angle1)); + assert(is_undef(endcap_angle2)||is_num(endcap_angle2)); + + endcap_shape1 = _endcap_shape(endcap1, select(width,0), endcap_width1, endcap_length1, endcap_extent1); + endcap_shape2 = _endcap_shape(endcap2, select(width,-1), endcap_width2, endcap_length2, endcap_extent2); + + trim1 = select(width,0) * first_defined([ + trim1, trim, + (endcap1=="arrow")? endcap_length1-0.01 : + (endcap1=="arrow2")? endcap_length1*3/4 : + 0 + ]); + assert(is_num(trim1)); + + trim2 = select(width,-1) * first_defined([ + trim2, trim, + (endcap2=="arrow")? endcap_length2-0.01 : + (endcap2=="arrow2")? endcap_length2*3/4 : + 0 + ]); + assert(is_num(trim2)); + + if (len(path) == 1) { + if (len(path[0]) == 2) { + translate(path[0]) circle(d=width[0]); + } else { + translate(path[0]) sphere(d=width[0]); + } + } else { + spos = path_pos_from_start(path,trim1,closed=false); + epos = path_pos_from_end(path,trim2,closed=false); + path2 = path_subselect(path, spos[0], spos[1], epos[0], epos[1]); + widths = concat( + [lerp(width[spos[0]], width[(spos[0]+1)%len(width)], spos[1])], + [for (i = [spos[0]+1:1:epos[0]]) width[i]], + [lerp(width[epos[0]], width[(epos[0]+1)%len(width)], epos[1])] + ); + + start_vec = select(path,0) - select(path,1); + end_vec = select(path,-1) - select(path,-2); + + if (len(path[0]) == 2) { + // Straight segments + for (i = idx(path2,end=-2)) { + seg = select(path2,i,i+1); + delt = seg[1] - seg[0]; + translate(seg[0]) { + rot(from=BACK,to=delt) { + trapezoid(w1=widths[i], w2=widths[i+1], h=norm(delt), anchor=FRONT); + } + } + } + + // Joints + for (i = [1:1:len(path2)-2]) { + $fn = quantup(segs(widths[i]/2),4); + if (hull) { + hull() { + translate(path2[i]) { + rot(from=BACK, to=path2[i]-path2[i-1]) + circle(d=widths[i]); + rot(from=BACK, to=path2[i+1]-path2[i]) + circle(d=widths[i]); + } + } + } else { + translate(path2[i]) { + rot(from=BACK, to=path2[i]-path2[i-1]) + circle(d=widths[i]); + rot(from=BACK, to=path2[i+1]-path2[i]) + circle(d=widths[i]); + } + } + } + + // Endcap1 + translate(path[0]) { + start_vec = select(path,0) - select(path,1); + rot(from=BACK, to=start_vec) { + polygon(endcap_shape1); + } + } + + // Endcap2 + translate(select(path,-1)) { + rot(from=BACK, to=end_vec) { + polygon(endcap_shape2); + } + } + } else { + quatsums = Q_Cumulative([ + for (i = idx(path2,end=-2)) let( + vec1 = i==0? UP : unit(path2[i]-path2[i-1], UP), + vec2 = unit(path2[i+1]-path2[i], UP), + axis = vector_axis(vec1,vec2), + ang = vector_angle(vec1,vec2) + ) Quat(axis,ang) + ]); + rotmats = [for (q=quatsums) Q_Matrix4(q)]; + sides = [ + for (i = idx(path2,end=-2)) + quantup(segs(max(widths[i],widths[i+1])/2),4) + ]; + + // Straight segments + for (i = idx(path2,end=-2)) { + dist = norm(path2[i+1] - path2[i]); + w1 = widths[i]/2; + w2 = widths[i+1]/2; + $fn = sides[i]; + translate(path2[i]) { + multmatrix(rotmats[i]) { + cylinder(r1=w1, r2=w2, h=dist, center=false); + } + } + } + + // Joints + for (i = [1:1:len(path2)-2]) { + $fn = sides[i]; + translate(path2[i]) { + if (hull) { + hull(){ + multmatrix(rotmats[i]) { + sphere(d=widths[i]); + } + multmatrix(rotmats[i-1]) { + sphere(d=widths[i]); + } + } + } else { + multmatrix(rotmats[i]) { + sphere(d=widths[i]); + } + multmatrix(rotmats[i-1]) { + sphere(d=widths[i]); + } + } + } + } + + // Endcap1 + translate(path[0]) { + multmatrix(rotmats[0] * xrot(180)) { + $fn = sides[0]; + if (is_undef(endcap_angle1)) { + rotate_extrude(convexity=convexity) { + right_half(planar=true) { + polygon(endcap_shape1); + } + } + } else { + rotate([90,0,endcap_angle1]) { + linear_extrude(height=widths[0], center=true, convexity=convexity) { + polygon(endcap_shape1); + } + } + } + } + } + + // Endcap2 + translate(select(path,-1)) { + multmatrix(select(rotmats,-1)) { + $fn = select(sides,-1); + if (is_undef(endcap_angle2)) { + rotate_extrude(convexity=convexity) { + right_half(planar=true) { + polygon(endcap_shape2); + } + } + } else { + rotate([90,0,endcap_angle2]) { + linear_extrude(height=select(widths,-1), center=true, convexity=convexity) { + polygon(endcap_shape2); + } + } + } + } + } + } + } +} + + +// Function&Module: arc() +// Usage: 2D arc from 0º to `angle` degrees. +// arc(N, r|d, angle); +// Usage: 2D arc from START to END degrees. +// arc(N, r|d, angle=[START,END]) +// Usage: 2D arc from `start` to `start+angle` degrees. +// arc(N, r|d, start, angle) +// Usage: 2D circle segment by `width` and `thickness`, starting and ending on the X axis. +// arc(N, width, thickness) +// Usage: Shortest 2D or 3D arc around centerpoint `cp`, starting at P0 and ending on the vector pointing from `cp` to `P1`. +// arc(N, cp, points=[P0,P1],[long],[cw],[ccw]) +// Usage: 2D or 3D arc, starting at `P0`, passing through `P1` and ending at `P2`. +// arc(N, points=[P0,P1,P2]) +// Description: +// If called as a function, returns a 2D or 3D path forming an arc. +// If called as a module, creates a 2D arc polygon or pie slice shape. +// Arguments: +// N = Number of vertices to form the arc curve from. +// r = Radius of the arc. +// d = Diameter of the arc. +// angle = If a scalar, specifies the end angle in degrees. If a vector of two scalars, specifies start and end angles. +// cp = Centerpoint of arc. +// points = Points on the arc. +// long = if given with cp and points takes the long arc instead of the default short arc. Default: false +// cw = if given with cp and 2 points takes the arc in the clockwise direction. Default: false +// ccw = if given with cp and 2 points takes the arc in the counter-clockwise direction. Default: false +// width = If given with `thickness`, arc starts and ends on X axis, to make a circle segment. +// thickness = If given with `width`, arc starts and ends on X axis, to make a circle segment. +// start = Start angle of arc. +// wedge = If true, include centerpoint `cp` in output to form pie slice shape. +// Examples(2D): +// arc(N=4, r=30, angle=30, wedge=true); +// arc(r=30, angle=30, wedge=true); +// arc(d=60, angle=30, wedge=true); +// arc(d=60, angle=120); +// arc(d=60, angle=120, wedge=true); +// arc(r=30, angle=[75,135], wedge=true); +// arc(r=30, start=45, angle=75, wedge=true); +// arc(width=60, thickness=20); +// arc(cp=[-10,5], points=[[20,10],[0,35]], wedge=true); +// arc(points=[[30,-5],[20,10],[-10,20]], wedge=true); +// arc(points=[[5,30],[-10,-10],[30,5]], wedge=true); +// Example(2D): +// path = arc(points=[[5,30],[-10,-10],[30,5]], wedge=true); +// stroke(closed=true, path); +// Example(FlatSpin): +// path = arc(points=[[0,30,0],[0,0,30],[30,0,0]]); +// trace_polyline(path, showpts=true, color="cyan"); +function arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false, long=false, cw=false, ccw=false) = + // First try for 2D arc specified by width and thickness + is_def(width) && is_def(thickness)? ( + assert(!any_defined([r,cp,points]) && !any([cw,ccw,long]),"Conflicting or invalid parameters to arc") + assert(width>0, "Width must be postive") + assert(thickness>0, "Thickness must be positive") + arc(N,points=[[width/2,0], [0,thickness], [-width/2,0]],wedge=wedge) + ) : is_def(angle)? ( + let( + parmok = !any_defined([points,width,thickness]) && + ((is_vector(angle,2) && is_undef(start)) || is_num(angle)) + ) + assert(parmok,"Invalid parameters in arc") + let( + cp = first_defined([cp,[0,0]]), + start = is_def(start)? start : is_vector(angle) ? angle[0] : 0, + angle = is_vector(angle)? angle[1]-angle[0] : angle, + r = get_radius(r=r, d=d) + ) + assert(is_vector(cp,2),"Centerpoint must be a 2d vector") + assert(angle!=0, "Arc has zero length") + assert(r>0, "Arc radius invalid") + let( + N = max(3, is_undef(N)? ceil(segs(r)*abs(angle)/360) : N), + arcpoints = [for(i=[0:N-1]) let(theta = start + i*angle/(N-1)) r*[cos(theta),sin(theta)]+cp], + extra = wedge? [cp] : [] + ) + concat(extra,arcpoints) + ) : + assert(is_path(points,[2,3]),"Point list is invalid") + // Arc is 3D, so transform points to 2D and make a recursive call, then remap back to 3D + len(points[0])==3? ( + assert(!(cw || ccw), "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false") + assert(is_undef(cp) || is_vector(cp,3),"points are 3d so cp must be 3d") + let( + thirdpoint = is_def(cp) ? cp : points[2], + center2d = is_def(cp) ? project_plane(cp,thirdpoint,points[0],points[1]) : undef, + points2d = project_plane(points,thirdpoint,points[0],points[1]) + ) + lift_plane(arc(N,cp=center2d,points=points2d,wedge=wedge,long=long),thirdpoint,points[0],points[1]) + ) : is_def(cp)? ( + // Arc defined by center plus two points, will have radius defined by center and points[0] + // and extent defined by direction of point[1] from the center + assert(is_vector(cp,2), "Centerpoint must be a 2d vector") + assert(len(points)==2, "When pointlist has length 3 centerpoint is not allowed") + assert(points[0]!=points[1], "Arc endpoints are equal") + assert(cp!=points[0]&&cp!=points[1], "Centerpoint equals an arc endpoint") + assert(count_true([long,cw,ccw])<=1, str("Only one of `long`, `cw` and `ccw` can be true",cw,ccw,long)) + let( + angle = vector_angle(points[0], cp, points[1]), + v1 = points[0]-cp, + v2 = points[1]-cp, + prelim_dir = sign(det2([v1,v2])), // z component of cross product + dir = prelim_dir != 0 + ? prelim_dir + : assert(cw || ccw, "Collinear inputs don't define a unique arc") + 1, + r=norm(v1), + final_angle = long || (ccw && dir<0) || (cw && dir>0) ? -dir*(360-angle) : dir*angle + ) + arc(N,cp=cp,r=r,start=atan2(v1.y,v1.x),angle=final_angle,wedge=wedge) + ) : ( + // Final case is arc passing through three points, starting at point[0] and ending at point[3] + let(col = collinear(points[0],points[1],points[2])) + assert(!col, "Collinear inputs do not define an arc") + let( + cp = line_intersection(_normal_segment(points[0],points[1]),_normal_segment(points[1],points[2])), + // select order to be counterclockwise + dir = det2([points[1]-points[0],points[2]-points[1]]) > 0, + points = dir? select(points,[0,2]) : select(points,[2,0]), + r = norm(points[0]-cp), + theta_start = atan2(points[0].y-cp.y, points[0].x-cp.x), + theta_end = atan2(points[1].y-cp.y, points[1].x-cp.x), + angle = posmod(theta_end-theta_start, 360), + arcpts = arc(N,cp=cp,r=r,start=theta_start,angle=angle,wedge=wedge) + ) + dir ? arcpts : reverse(arcpts) + ); + + +module arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false) +{ + path = arc(N=N, r=r, angle=angle, d=d, cp=cp, points=points, width=width, thickness=thickness, start=start, wedge=wedge); + polygon(path); +} + + +function _normal_segment(p1,p2) = + let(center = (p1+p2)/2) + [center, center + norm(p1-p2)/2 * line_normal(p1,p2)]; + + +// Function: turtle() +// Usage: +// turtle(commands, [state], [return_state]) +// Description: +// Use a sequence of turtle graphics commands to generate a path. The parameter `commands` is a list of +// turtle commands and optional parameters for each command. The turtle state has a position, movement direction, +// movement distance, and default turn angle. If you do not give `state` as input then the turtle starts at the +// origin, pointed along the positive x axis with a movement distance of 1. By default, `turtle` returns just +// the computed turtle path. If you set `full_state` to true then it instead returns the full turtle state. +// You can invoke `turtle` again with this full state to continue the turtle path where you left off. +// +// The turtle state is a list with three entries: the path constructed so far, the current step as a 2-vector, and the current default angle. +// +// For the list below, `dist` is the current movement distance. +// +// Commands | Arguments | What it does +// ------------ | ------------------ | ------------------------------- +// "move" | [dist] | Move turtle scale*dist units in the turtle direction. Default dist=1. +// "xmove" | [dist] | Move turtle scale*dist units in the x direction. Default dist=1. Does not change turtle direction. +// "ymove" | [dist] | Move turtle scale*dist units in the y direction. Default dist=1. Does not change turtle direction. +// "xymove" | vector | Move turtle by the specified vector. Does not change turtle direction. +// "untilx" | xtarget | Move turtle in turtle direction until x==xtarget. Produces an error if xtarget is not reachable. +// "untily" | ytarget | Move turtle in turtle direction until y==ytarget. Produces an error if xtarget is not reachable. +// "jump" | point | Move the turtle to the specified point +// "xjump" | x | Move the turtle's x position to the specified value +// "yjump | y | Move the turtle's y position to the specified value +// "turn" | [angle] | Turn turtle direction by specified angle, or the turtle's default turn angle. The default angle starts at 90. +// "left" | [angle] | Same as "turn" +// "right" | [angle] | Same as "turn", -angle +// "angle" | angle | Set the default turn angle. +// "setdir" | dir | Set turtle direction. The parameter `dir` can be an angle or a vector. +// "length" | length | Change the turtle move distance to `length` +// "scale" | factor | Multiply turtle move distance by `factor` +// "addlength" | length | Add `length` to the turtle move distance +// "repeat" | count, commands | Repeats a list of commands `count` times. +// "arcleft" | radius, [angle] | Draw an arc from the current position toward the left at the specified radius and angle. The turtle turns by `angle`. A negative angle draws the arc to the right instead of the left, and leaves the turtle facing right. A negative radius draws the arc to the right but leaves the turtle facing left. +// "arcright" | radius, [angle] | Draw an arc from the current position toward the right at the specified radius and angle +// "arcleftto" | radius, angle | Draw an arc at the given radius turning toward the left until reaching the specified absolute angle. +// "arcrightto" | radius, angle | Draw an arc at the given radius turning toward the right until reaching the specified absolute angle. +// "arcsteps" | count | Specifies the number of segments to use for drawing arcs. If you set it to zero then the standard `$fn`, `$fa` and `$fs` variables define the number of segments. +// +// Arguments: +// commands = List of turtle commands +// state = Starting turtle state (from previous call) or starting point. Default: start at the origin, pointing right. +// full_state = If true return the full turtle state for continuing the path in subsequent turtle calls. Default: false +// repeat = Number of times to repeat the command list. Default: 1 +// +// Example(2D): Simple rectangle +// path = turtle(["xmove",3, "ymove", "xmove",-3, "ymove",-1]); +// stroke(path,width=.1); +// Example(2D): Pentagon +// path=turtle(["angle",360/5,"move","turn","move","turn","move","turn","move"]); +// stroke(path,width=.1,closed=true); +// Example(2D): Pentagon using the repeat argument +// path=turtle(["move","turn",360/5],repeat=5); +// stroke(path,width=.1,closed=true); +// Example(2D): Pentagon using the repeat turtle command, setting the turn angle +// path=turtle(["angle",360/5,"repeat",5,["move","turn"]]); +// stroke(path,width=.1,closed=true); +// Example(2D): Pentagram +// path = turtle(["move","left",144], repeat=4); +// stroke(path,width=.05,closed=true); +// Example(2D): Sawtooth path +// path = turtle([ +// "turn", 55, +// "untily", 2, +// "turn", -55-90, +// "untily", 0, +// "turn", 55+90, +// "untily", 2.5, +// "turn", -55-90, +// "untily", 0, +// "turn", 55+90, +// "untily", 3, +// "turn", -55-90, +// "untily", 0 +// ]); +// stroke(path, width=.1); +// Example(2D): Simpler way to draw the sawtooth. The direction of the turtle is preserved when executing "yjump". +// path = turtle([ +// "turn", 55, +// "untily", 2, +// "yjump", 0, +// "untily", 2.5, +// "yjump", 0, +// "untily", 3, +// "yjump", 0, +// ]); +// stroke(path, width=.1); +// Example(2DMed): square spiral +// path = turtle(["move","left","addlength",1],repeat=50); +// stroke(path,width=.2); +// Example(2DMed): pentagonal spiral +// path = turtle(["move","left",360/5,"addlength",1],repeat=50); +// stroke(path,width=.2); +// Example(2DMed): yet another spiral, without using `repeat` +// path = turtle(concat(["angle",71],flatten(repeat(["move","left","addlength",1],50)))); +// stroke(path,width=.2); +// Example(2DMed): The previous spiral grows linearly and eventually intersects itself. This one grows geometrically and does not. +// path = turtle(["move","left",71,"scale",1.05],repeat=50); +// stroke(path,width=.05); +// Example(2D): Koch Snowflake +// function koch_unit(depth) = +// depth==0 ? ["move"] : +// concat( +// koch_unit(depth-1), +// ["right"], +// koch_unit(depth-1), +// ["left","left"], +// koch_unit(depth-1), +// ["right"], +// koch_unit(depth-1) +// ); +// koch=concat(["angle",60,"repeat",3],[concat(koch_unit(3),["left","left"])]); +// polygon(turtle(koch)); +function turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) = + let( state = is_vector(state) ? [[state],[1,0],90,0] : state ) + repeat == 1? + _turtle(commands,state,full_state) : + _turtle_repeat(commands, state, full_state, repeat); + +function _turtle_repeat(commands, state, full_state, repeat) = + repeat==1? + _turtle(commands,state,full_state) : + _turtle_repeat(commands, _turtle(commands, state, true), full_state, repeat-1); + +function _turtle_command_len(commands, index) = + let( one_or_two_arg = ["arcleft","arcright", "arcleftto", "arcrightto"] ) + commands[index] == "repeat"? 3 : // Repeat command requires 2 args + // For these, the first arg is required, second arg is present if it is not a string + in_list(commands[index], one_or_two_arg) && len(commands)>index+2 && !is_string(commands[index+2]) ? 3 : + is_string(commands[index+1])? 1 : // If 2nd item is a string it's must be a new command + 2; // Otherwise we have command and arg + +function _turtle(commands, state, full_state, index=0) = + index < len(commands) ? + _turtle(commands, + _turtle_command(commands[index],commands[index+1],commands[index+2],state,index), + full_state, + index+_turtle_command_len(commands,index) + ) : + ( full_state ? state : state[0] ); + +// Turtle state: state = [path, step_vector, default angle] + +function _turtle_command(command, parm, parm2, state, index) = + command == "repeat"? + assert(is_num(parm),str("\"repeat\" command requires a numeric repeat count at index ",index)) + assert(is_list(parm2),str("\"repeat\" command requires a command list parameter at index ",index)) + _turtle_repeat(parm2, state, true, parm) : + let( + path = 0, + step=1, + angle=2, + arcsteps=3, + parm = !is_string(parm) ? parm : undef, + parm2 = !is_string(parm2) ? parm2 : undef, + needvec = ["jump", "xymove"], + neednum = ["untilx","untily","xjump","yjump","angle","length","scale","addlength"], + needeither = ["setdir"], + chvec = !in_list(command,needvec) || is_vector(parm,2), + chnum = !in_list(command,neednum) || is_num(parm), + vec_or_num = !in_list(command,needeither) || (is_num(parm) || is_vector(parm,2)), + lastpt = select(state[path],-1) + ) + assert(chvec,str("\"",command,"\" requires a vector parameter at index ",index)) + assert(chnum,str("\"",command,"\" requires a numeric parameter at index ",index)) + assert(vec_or_num,str("\"",command,"\" requires a vector or numeric parameter at index ",index)) + + command=="move" ? list_set(state, path, concat(state[path],[default(parm,1)*state[step]+lastpt])) : + command=="untilx" ? ( + let( + int = line_intersection([lastpt,lastpt+state[step]], [[parm,0],[parm,1]]), + xgood = sign(state[step].x) == sign(int.x-lastpt.x) + ) + assert(xgood,str("\"untilx\" never reaches desired goal at index ",index)) + list_set(state,path,concat(state[path],[int])) + ) : + command=="untily" ? ( + let( + int = line_intersection([lastpt,lastpt+state[step]], [[0,parm],[1,parm]]), + ygood = is_def(int) && sign(state[step].y) == sign(int.y-lastpt.y) + ) + assert(ygood,str("\"untily\" never reaches desired goal at index ",index)) + list_set(state,path,concat(state[path],[int])) + ) : + command=="xmove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[1,0]+lastpt])): + command=="ymove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[0,1]+lastpt])): + command=="xymove" ? list_set(state, path, concat(state[path], [lastpt+parm])): + command=="jump" ? list_set(state, path, concat(state[path],[parm])): + command=="xjump" ? list_set(state, path, concat(state[path],[[parm,lastpt.y]])): + command=="yjump" ? list_set(state, path, concat(state[path],[[lastpt.x,parm]])): + command=="turn" || command=="left" ? list_set(state, step, rot(default(parm,state[angle]),p=state[step],planar=true)) : + command=="right" ? list_set(state, step, rot(-default(parm,state[angle]),p=state[step],planar=true)) : + command=="angle" ? list_set(state, angle, parm) : + command=="setdir" ? ( + is_vector(parm) ? + list_set(state, step, norm(state[step]) * unit(parm)) : + list_set(state, step, norm(state[step]) * [cos(parm),sin(parm)]) + ) : + command=="length" ? list_set(state, step, parm*unit(state[step])) : + command=="scale" ? list_set(state, step, parm*state[step]) : + command=="addlength" ? list_set(state, step, state[step]+unit(state[step])*parm) : + command=="arcsteps" ? list_set(state, arcsteps, parm) : + command=="arcleft" || command=="arcright" ? + assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index)) + let( + myangle = default(parm2,state[angle]), + lrsign = command=="arcleft" ? 1 : -1, + radius = parm*sign(myangle), + center = lastpt + lrsign*radius*line_normal([0,0],state[step]), + steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps], + arcpath = myangle == 0 || radius == 0 ? [] : arc( + steps, + points = [ + lastpt, + rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle/2), + rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle) + ] + ) + ) + list_set( + state, [path,step], [ + concat(state[path], slice(arcpath,1,-1)), + rot(lrsign * myangle,p=state[step],planar=true) + ] + ) : + command=="arcleftto" || command=="arcrightto" ? + assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index)) + assert(is_num(parm2),str("\"",command,"\" command requires a numeric angle value at index ",index)) + let( + radius = parm, + lrsign = command=="arcleftto" ? 1 : -1, + center = lastpt + lrsign*radius*line_normal([0,0],state[step]), + steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps], + start_angle = posmod(atan2(state[step].y, state[step].x),360), + end_angle = posmod(parm2,360), + delta_angle = -start_angle + (lrsign * end_angle < lrsign*start_angle ? end_angle+lrsign*360 : end_angle), + arcpath = delta_angle == 0 || radius==0 ? [] : arc( + steps, + points = [ + lastpt, + rot(cp=center, p=lastpt, a=sign(radius)*delta_angle/2), + rot(cp=center, p=lastpt, a=sign(radius)*delta_angle) + ] + ) + ) + list_set( + state, [path,step], [ + concat(state[path], slice(arcpath,1,-1)), + rot(delta_angle,p=state[step],planar=true) + ] + ) : + assert(false,str("Unknown turtle command \"",command,"\" at index",index)) + []; + + + +// Section: 2D Primitives + +// Function&Module: rect() +// Usage: +// rect(size, [center], [rounding], [chamfer], [anchor], [spin]) +// Description: +// When called as a module, creates a 2D rectangle of the given size, with optional rounding or chamfering. +// When called as a function, returns a 2D path/list of points for a square/rectangle of the given size. +// Arguments: +// size = The size of the rectangle to create. If given as a scalar, both X and Y will be the same size. +// rounding = The rounding radius for the corners. If given as a list of four numbers, gives individual radii for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no rounding) +// chamfer = The chamfer size for the corners. If given as a list of four numbers, gives individual chamfers for each corner, in the order [X+Y+,X-Y+,X-Y-,X+Y-]. Default: 0 (no chamfer) +// center = If given and true, overrides `anchor` to be `CENTER`. If given and false, overrides `anchor` to be `FRONT+LEFT`. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Example(2D): +// rect(40); +// Example(2D): Centered +// rect([40,30], center=true); +// Example(2D): Anchored +// rect([40,30], anchor=FRONT); +// Example(2D): Spun +// rect([40,30], anchor=FRONT, spin=30); +// Example(2D): Chamferred Rect +// rect([40,30], chamfer=5, center=true); +// Example(2D): Rounded Rect +// rect([40,30], rounding=5, center=true); +// Example(2D): Mixed Chamferring and Rounding +// rect([40,30],center=true,rounding=[5,0,10,0],chamfer=[0,8,0,15],$fa=1,$fs=1); +// Example(2D): Called as Function +// path = rect([40,30], chamfer=5, anchor=FRONT, spin=30); +// stroke(path, closed=true); +// move_copies(path) color("blue") circle(d=2,$fn=8); +module rect(size=1, center, rounding=0, chamfer=0, anchor, spin=0) { + size = is_num(size)? [size,size] : point2d(size); + anchor = get_anchor(anchor, center, FRONT+LEFT, FRONT+LEFT); + if (rounding==0 && chamfer==0) { + attachable(anchor,spin, two_d=true, size=size) { + square(size, center=true); + children(); + } + } else { + pts = rect(size=size, rounding=rounding, chamfer=chamfer, center=true); + attachable(anchor,spin, two_d=true, path=pts) { + polygon(pts); + children(); + } + } +} + + +function rect(size=1, center, rounding=0, chamfer=0, anchor, spin=0) = + assert(is_num(size) || is_vector(size)) + assert(is_num(chamfer) || len(chamfer)==4) + assert(is_num(rounding) || len(rounding)==4) + let( + size = is_num(size)? [size,size] : point2d(size), + anchor = get_anchor(anchor, center, FRONT+LEFT, FRONT+LEFT), + complex = rounding!=0 || chamfer!=0 + , xxx = echo(anchor=anchor)echo(size=size) + ) + (rounding==0 && chamfer==0)? let( + path = [ + [ size.x/2, -size.y/2], + [-size.x/2, -size.y/2], + [-size.x/2, size.y/2], + [ size.x/2, size.y/2] + ] + ) rot(spin, p=move(-vmul(anchor,size/2), p=path)) : + let( + chamfer = is_list(chamfer)? chamfer : [for (i=[0:3]) chamfer], + rounding = is_list(rounding)? rounding : [for (i=[0:3]) rounding], + quadorder = [3,2,1,0], + quadpos = [[1,1],[-1,1],[-1,-1],[1,-1]], + insets = [for (i=[0:3]) chamfer[i]>0? chamfer[i] : rounding[i]>0? rounding[i] : 0], + insets_x = max(insets[0]+insets[1],insets[2]+insets[3]), + insets_y = max(insets[0]+insets[3],insets[1]+insets[2]) + ) + assert(insets_x <= size.x, "Requested roundings and/or chamfers exceed the rect width.") + assert(insets_y <= size.y, "Requested roundings and/or chamfers exceed the rect height.") + let( + path = [ + for(i = [0:3]) + let( + quad = quadorder[i], + inset = insets[quad], + cverts = quant(segs(inset),4)/4, + cp = vmul(size/2-[inset,inset], quadpos[quad]), + step = 90/cverts, + angs = + chamfer[quad] > 0? [0,-90]-90*[i,i] : + rounding[quad] > 0? [for (j=[0:1:cverts]) 360-j*step-i*90] : + [0] + ) + each [for (a = angs) cp + inset*[cos(a),sin(a)]] + ] + ) complex? + reorient(anchor,spin, two_d=true, path=path, p=path) : + reorient(anchor,spin, two_d=true, size=size, p=path); + + +// Function&Module: oval() +// Usage: +// oval(r|d, [realign], [circum]) +// Description: +// When called as a module, creates a 2D polygon that approximates a circle of the given size. +// When called as a function, returns a 2D list of points (path) for a polygon that approximates a circle of the given size. +// Arguments: +// r = Radius of the circle/oval to create. Can be a scalar, or a list of sizes per axis. +// d = Diameter of the circle/oval to create. Can be a scalar, or a list of sizes per axis. +// realign = If true, rotates the polygon that approximates the circle/oval by half of one size. +// circum = If true, the polygon that approximates the circle will be upsized slightly to circumscribe the theoretical circle. If false, it inscribes the theoretical circle. Default: false +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Example(2D): By Radius +// oval(r=25); +// Example(2D): By Diameter +// oval(d=50); +// Example(2D): Anchoring +// oval(d=50, anchor=FRONT); +// Example(2D): Spin +// oval(d=50, anchor=FRONT, spin=45); +// Example(NORENDER): Called as Function +// path = oval(d=50, anchor=FRONT, spin=45); +module oval(r, d, realign=false, circum=false, anchor=CENTER, spin=0) { + r = get_radius(r=r, d=d, dflt=1); + sides = segs(max(r)); + sc = circum? (1 / cos(180/sides)) : 1; + rx = default(r[0],r) * sc; + ry = default(r[1],r) * sc; + attachable(anchor,spin, two_d=true, r=[rx,ry]) { + if (rx < ry) { + xscale(rx/ry) { + zrot(realign? 180/sides : 0) { + circle(r=ry, $fn=sides); + } + } + } else { + yscale(ry/rx) { + zrot(realign? 180/sides : 0) { + circle(r=rx, $fn=sides); + } + } + } + children(); + } +} + + +function oval(r, d, realign=false, circum=false, anchor=CENTER, spin=0) = + let( + r = get_radius(r=r, d=d, dflt=1), + sides = segs(max(r)), + offset = realign? 180/sides : 0, + sc = circum? (1 / cos(180/sides)) : 1, + rx = default(r[0],r) * sc, + ry = default(r[1],r) * sc, + pts = [for (i=[0:1:sides-1]) let(a=360-offset-i*360/sides) [rx*cos(a), ry*sin(a)]] + ) reorient(anchor,spin, two_d=true, r=[rx,ry], p=pts); + + + +// Section: 2D N-Gons + +// Function&Module: regular_ngon() +// Usage: +// regular_ngon(n, r|d|or|od, [realign]); +// regular_ngon(n, ir|id, [realign]); +// regular_ngon(n, side, [realign]); +// Description: +// When called as a function, returns a 2D path for a regular N-sided polygon. +// When called as a module, creates a 2D regular N-sided polygon. +// Arguments: +// n = The number of sides. +// or = Outside radius, at points. +// r = Same as or +// od = Outside diameter, at points. +// d = Same as od +// ir = Inside radius, at center of sides. +// id = Inside diameter, at center of sides. +// side = Length of each side. +// rounding = Radius of rounding for the tips of the polygon. Default: 0 (no rounding) +// realign = If false, a tip is aligned with the Y+ axis. If true, the midpoint of a side is aligned with the Y+ axis. Default: false +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Extra Anchors: +// "tip0", "tip1", etc. = Each tip has an anchor, pointing outwards. +// "side0", "side1", etc. = The center of each side has an anchor, pointing outwards. +// Example(2D): by Outer Size +// regular_ngon(n=5, or=30); +// regular_ngon(n=5, od=60); +// Example(2D): by Inner Size +// regular_ngon(n=5, ir=30); +// regular_ngon(n=5, id=60); +// Example(2D): by Side Length +// regular_ngon(n=8, side=20); +// Example(2D): Realigned +// regular_ngon(n=8, side=20, realign=true); +// Example(2D): Rounded +// regular_ngon(n=5, od=100, rounding=20, $fn=20); +// Example(2D): Called as Function +// stroke(closed=true, regular_ngon(n=6, or=30)); +function regular_ngon(n=6, r, d, or, od, ir, id, side, rounding=0, realign=false, anchor=CENTER, spin=0) = + let( + sc = 1/cos(180/n), + r = get_radius(r1=ir*sc, r2=or, r=r, d1=id*sc, d2=od, d=d, dflt=side/2/sin(180/n)) + ) + assert(!is_undef(r), "regular_ngon(): need to specify one of r, d, or, od, ir, id, side.") + let( + inset = opp_ang_to_hyp(rounding, (180-360/n)/2), + path = rounding==0? oval(r=r, realign=realign, $fn=n) : ( + let( + steps = floor(segs(r)/n), + step = 360/n/steps, + path2 = [ + for (i = [0:1:n-1]) let( + a = 360 - i*360/n - (realign? 180/n : 0), + p = polar_to_xy(r-inset, a) + ) + each arc(N=steps, cp=p, r=rounding, start=a+180/n, angle=-360/n) + ], + maxx_idx = max_index(subindex(path2,0)), + path3 = polygon_shift(path2,maxx_idx) + ) path3 + ), + anchors = !is_string(anchor)? [] : [ + for (i = [0:1:n-1]) let( + a1 = 360 - i*360/n - (realign? 180/n : 0), + a2 = a1 - 360/n, + p1 = polar_to_xy(r,a1), + p2 = polar_to_xy(r,a2), + tipp = polar_to_xy(r-inset+rounding,a1), + pos = (p1+p2)/2 + ) each [ + anchorpt(str("tip",i), tipp, unit(tipp,BACK), 0), + anchorpt(str("side",i), pos, unit(pos,BACK), 0), + ] + ] + ) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path, anchors=anchors); + + +module regular_ngon(n=6, r, d, or, od, ir, id, side, rounding=0, realign=false, anchor=CENTER, spin=0) { + sc = 1/cos(180/n); + r = get_radius(r1=ir*sc, r2=or, r=r, d1=id*sc, d2=od, d=d, dflt=side/2/sin(180/n)); + assert(!is_undef(r), "regular_ngon(): need to specify one of r, d, or, od, ir, id, side."); + path = regular_ngon(n=n, r=r, rounding=rounding, realign=realign); + inset = opp_ang_to_hyp(rounding, (180-360/n)/2); + anchors = [ + for (i = [0:1:n-1]) let( + a1 = 360 - i*360/n - (realign? 180/n : 0), + a2 = a1 - 360/n, + p1 = polar_to_xy(r,a1), + p2 = polar_to_xy(r,a2), + tipp = polar_to_xy(r-inset+rounding,a1), + pos = (p1+p2)/2 + ) each [ + anchorpt(str("tip",i), tipp, unit(tipp,BACK), 0), + anchorpt(str("side",i), pos, unit(pos,BACK), 0), + ] + ]; + attachable(anchor,spin, two_d=true, path=path, extent=false, anchors=anchors) { + polygon(path); + children(); + } +} + + +// Function&Module: pentagon() +// Usage: +// pentagon(or|od, [realign]); +// pentagon(ir|id, [realign]); +// pentagon(side, [realign]); +// Description: +// When called as a function, returns a 2D path for a regular pentagon. +// When called as a module, creates a 2D regular pentagon. +// Arguments: +// or = Outside radius, at points. +// r = Same as or. +// od = Outside diameter, at points. +// d = Same as od. +// ir = Inside radius, at center of sides. +// id = Inside diameter, at center of sides. +// side = Length of each side. +// rounding = Radius of rounding for the tips of the polygon. Default: 0 (no rounding) +// realign = If false, a tip is aligned with the Y+ axis. If true, the midpoint of a side is aligned with the Y+ axis. Default: false +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Extra Anchors: +// "tip0" ... "tip4" = Each tip has an anchor, pointing outwards. +// "side0" ... "side4" = The center of each side has an anchor, pointing outwards. +// Example(2D): by Outer Size +// pentagon(or=30); +// pentagon(od=60); +// Example(2D): by Inner Size +// pentagon(ir=30); +// pentagon(id=60); +// Example(2D): by Side Length +// pentagon(side=20); +// Example(2D): Realigned +// pentagon(side=20, realign=true); +// Example(2D): Rounded +// pentagon(od=100, rounding=20, $fn=20); +// Example(2D): Called as Function +// stroke(closed=true, pentagon(or=30)); +function pentagon(r, d, or, od, ir, id, side, rounding=0, realign=false, anchor=CENTER, spin=0) = + regular_ngon(n=5, r=r, d=d, or=or, od=od, ir=ir, id=id, side=side, rounding=rounding, realign=realign, anchor=anchor, spin=spin); + + +module pentagon(r, d, or, od, ir, id, side, rounding=0, realign=false, anchor=CENTER, spin=0) + regular_ngon(n=5, r=r, d=d, or=or, od=od, ir=ir, id=id, side=side, rounding=rounding, realign=realign, anchor=anchor, spin=spin) children(); + + +// Function&Module: hexagon() +// Usage: +// hexagon(or, od, ir, id, side); +// Description: +// When called as a function, returns a 2D path for a regular hexagon. +// When called as a module, creates a 2D regular hexagon. +// Arguments: +// or = Outside radius, at points. +// r = Same as or +// od = Outside diameter, at points. +// d = Same as od +// ir = Inside radius, at center of sides. +// id = Inside diameter, at center of sides. +// side = Length of each side. +// rounding = Radius of rounding for the tips of the polygon. Default: 0 (no rounding) +// realign = If false, a tip is aligned with the Y+ axis. If true, the midpoint of a side is aligned with the Y+ axis. Default: false +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Extra Anchors: +// "tip0" ... "tip5" = Each tip has an anchor, pointing outwards. +// "side0" ... "side5" = The center of each side has an anchor, pointing outwards. +// Example(2D): by Outer Size +// hexagon(or=30); +// hexagon(od=60); +// Example(2D): by Inner Size +// hexagon(ir=30); +// hexagon(id=60); +// Example(2D): by Side Length +// hexagon(side=20); +// Example(2D): Realigned +// hexagon(side=20, realign=true); +// Example(2D): Rounded +// hexagon(od=100, rounding=20, $fn=20); +// Example(2D): Called as Function +// stroke(closed=true, hexagon(or=30)); +function hexagon(r, d, or, od, ir, id, side, rounding=0, realign=false, anchor=CENTER, spin=0) = + regular_ngon(n=6, r=r, d=d, or=or, od=od, ir=ir, id=id, side=side, rounding=rounding, realign=realign, anchor=anchor, spin=spin); + + +module hexagon(r, d, or, od, ir, id, side, rounding=0, realign=false, anchor=CENTER, spin=0) + regular_ngon(n=6, r=r, d=d, or=or, od=od, ir=ir, id=id, side=side, rounding=rounding, realign=realign, anchor=anchor, spin=spin) children(); + + +// Function&Module: octagon() +// Usage: +// octagon(or, od, ir, id, side); +// Description: +// When called as a function, returns a 2D path for a regular octagon. +// When called as a module, creates a 2D regular octagon. +// Arguments: +// or = Outside radius, at points. +// r = Same as or +// od = Outside diameter, at points. +// d = Same as od +// ir = Inside radius, at center of sides. +// id = Inside diameter, at center of sides. +// side = Length of each side. +// rounding = Radius of rounding for the tips of the polygon. Default: 0 (no rounding) +// realign = If false, a tip is aligned with the Y+ axis. If true, the midpoint of a side is aligned with the Y+ axis. Default: false +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Extra Anchors: +// "tip0" ... "tip7" = Each tip has an anchor, pointing outwards. +// "side0" ... "side7" = The center of each side has an anchor, pointing outwards. +// Example(2D): by Outer Size +// octagon(or=30); +// octagon(od=60); +// Example(2D): by Inner Size +// octagon(ir=30); +// octagon(id=60); +// Example(2D): by Side Length +// octagon(side=20); +// Example(2D): Realigned +// octagon(side=20, realign=true); +// Example(2D): Rounded +// octagon(od=100, rounding=20, $fn=20); +// Example(2D): Called as Function +// stroke(closed=true, octagon(or=30)); +function octagon(r, d, or, od, ir, id, side, rounding=0, realign=false, anchor=CENTER, spin=0) = + regular_ngon(n=8, r=r, d=d, or=or, od=od, ir=ir, id=id, side=side, rounding=rounding, realign=realign, anchor=anchor, spin=spin); + + +module octagon(r, d, or, od, ir, id, side, rounding=0, realign=false, anchor=CENTER, spin=0) + regular_ngon(n=8, r=r, d=d, or=or, od=od, ir=ir, id=id, side=side, rounding=rounding, realign=realign, anchor=anchor, spin=spin) children(); + + + +// Section: Other 2D Shapes + + +// Function&Module: trapezoid() +// Usage: +// trapezoid(h, w1, w2); +// Description: +// When called as a function, returns a 2D path for a trapezoid with parallel front and back sides. +// When called as a module, creates a 2D trapezoid with parallel front and back sides. +// Arguments: +// h = The Y axis height of the trapezoid. +// w1 = The X axis width of the front end of the trapezoid. +// w2 = The X axis width of the back end of the trapezoid. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Examples(2D): +// trapezoid(h=30, w1=40, w2=20); +// trapezoid(h=25, w1=20, w2=35); +// trapezoid(h=20, w1=40, w2=0); +// Example(2D): Called as Function +// stroke(closed=true, trapezoid(h=30, w1=40, w2=20)); +function trapezoid(h, w1, w2, anchor=CENTER, spin=0) = + let( + path = [[w1/2,-h/2], [-w1/2,-h/2], [-w2/2,h/2], [w2/2,h/2]] + ) reorient(anchor,spin, two_d=true, size=[w1,h], size2=w2, p=path); + + + +module trapezoid(h, w1, w2, anchor=CENTER, spin=0) { + path = [[w1/2,-h/2], [-w1/2,-h/2], [-w2/2,h/2], [w2/2,h/2]]; + attachable(anchor,spin, two_d=true, size=[w1,h], size2=w2) { + polygon(path); + children(); + } +} + + +// Function&Module: teardrop2d() +// +// Description: +// Makes a 2D teardrop shape. Useful for extruding into 3D printable holes. +// +// Usage: +// teardrop2d(r|d, [ang], [cap_h]); +// +// Arguments: +// r = radius of circular part of teardrop. (Default: 1) +// d = diameter of spherical portion of bottom. (Use instead of r) +// ang = angle of hat walls from the Y axis. (Default: 45 degrees) +// cap_h = if given, height above center where the shape will be truncated. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// +// Example(2D): Typical Shape +// teardrop2d(r=30, ang=30); +// Example(2D): Crop Cap +// teardrop2d(r=30, ang=30, cap_h=40); +// Example(2D): Close Crop +// teardrop2d(r=30, ang=30, cap_h=20); +module teardrop2d(r, d, ang=45, cap_h, anchor=CENTER, spin=0) +{ + path = teardrop2d(r=r, d=d, ang=ang, cap_h=cap_h); + attachable(anchor,spin, two_d=true, path=path) { + polygon(path); + children(); + } +} + + +function teardrop2d(r, d, ang=45, cap_h, anchor=CENTER, spin=0) = + let( + r = get_radius(r=r, d=d, dflt=1), + cord = 2 * r * cos(ang), + cord_h = r * sin(ang), + tip_y = (cord/2)/tan(ang), + cap_h = min((!is_undef(cap_h)? cap_h : tip_y+cord_h), tip_y+cord_h), + cap_w = cord * (1 - (cap_h - cord_h)/tip_y), + ang = min(ang,asin(cap_h/r)), + sa = 180 - ang, + ea = 360 + ang, + steps = segs(r)*(ea-sa)/360, + step = (ea-sa)/steps, + path = deduplicate( + [ + [ cap_w/2,cap_h], + for (i=[0:1:steps]) let(a=ea-i*step) r*[cos(a),sin(a)], + [-cap_w/2,cap_h] + ], closed=true + ), + maxx_idx = max_index(subindex(path,0)), + path2 = polygon_shift(path,maxx_idx) + ) reorient(anchor,spin, two_d=true, path=path2, p=path2); + + + +// Function&Module: glued_circles() +// Usage: +// glued_circles(r|d, spread, tangent); +// Description: +// When called as a function, returns a 2D path forming a shape of two circles joined by curved waist. +// When called as a module, creates a 2D shape of two circles joined by curved waist. +// Arguments: +// r = The radius of the end circles. +// d = The diameter of the end circles. +// spread = The distance between the centers of the end circles. +// tangent = The angle in degrees of the tangent point for the joining arcs, measured away from the Y axis. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Examples(2D): +// glued_circles(r=15, spread=40, tangent=45); +// glued_circles(d=30, spread=30, tangent=30); +// glued_circles(d=30, spread=30, tangent=15); +// glued_circles(d=30, spread=30, tangent=-30); +// Example(2D): Called as Function +// stroke(closed=true, glued_circles(r=15, spread=40, tangent=45)); +function glued_circles(r, d, spread=10, tangent=30, anchor=CENTER, spin=0) = + let( + r = get_radius(r=r, d=d, dflt=10), + r2 = (spread/2 / sin(tangent)) - r, + cp1 = [spread/2, 0], + cp2 = [0, (r+r2)*cos(tangent)], + sa1 = 90-tangent, + ea1 = 270+tangent, + lobearc = ea1-sa1, + lobesegs = floor(segs(r)*lobearc/360), + lobestep = lobearc / lobesegs, + sa2 = 270-tangent, + ea2 = 270+tangent, + subarc = ea2-sa2, + arcsegs = ceil(segs(r2)*abs(subarc)/360), + arcstep = subarc / arcsegs, + path = concat( + [for (i=[0:1:lobesegs]) let(a=sa1+i*lobestep) r * [cos(a),sin(a)] - cp1], + tangent==0? [] : [for (i=[0:1:arcsegs]) let(a=ea2-i*arcstep+180) r2 * [cos(a),sin(a)] - cp2], + [for (i=[0:1:lobesegs]) let(a=sa1+i*lobestep+180) r * [cos(a),sin(a)] + cp1], + tangent==0? [] : [for (i=[0:1:arcsegs]) let(a=ea2-i*arcstep) r2 * [cos(a),sin(a)] + cp2] + ), + maxx_idx = max_index(subindex(path,0)), + path2 = reverse_polygon(polygon_shift(path,maxx_idx)) + ) reorient(anchor,spin, two_d=true, path=path2, extent=true, p=path2); + + +module glued_circles(r, d, spread=10, tangent=30, anchor=CENTER, spin=0) { + path = glued_circles(r=r, d=d, spread=spread, tangent=tangent); + attachable(anchor,spin, two_d=true, path=path, extent=true) { + polygon(path); + children(); + } +} + + +// Function&Module: star() +// Usage: +// star(n, r|d|or|od, ir|id|step, [realign]); +// Description: +// When called as a function, returns the path needed to create a star polygon with N points. +// When called as a module, creates a star polygon with N points. +// Arguments: +// n = The number of stellate tips on the star. +// r = The radius to the tips of the star. +// or = Same as r +// d = The diameter to the tips of the star. +// od = Same as d +// ir = The radius to the inner corners of the star. +// id = The diameter to the inner corners of the star. +// step = Calculates the radius of the inner star corners by virtually drawing a straight line `step` tips around the star. 2 <= step < n/2 +// realign = If false, a tip is aligned with the Y+ axis. If true, an inner corner is aligned with the Y+ axis. Default: false +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Extra Anchors: +// "tip0" ... "tip4" = Each tip has an anchor, pointing outwards. +// "corner0" ... "corner4" = The inside corner between each tip has an anchor, pointing outwards. +// "midpt0" ... "midpt4" = The center-point between each pair or tips has an anchor, pointing outwards. +// Examples(2D): +// star(n=5, r=50, ir=25); +// star(n=5, r=50, step=2); +// star(n=7, r=50, step=2); +// star(n=7, r=50, step=3); +// Example(2D): Realigned +// star(n=7, r=50, step=3, realign=true); +// Example(2D): Called as Function +// stroke(closed=true, star(n=5, r=50, ir=25)); +function star(n, r, d, or, od, ir, id, step, realign=false, anchor=CENTER, spin=0) = + let( + r = get_radius(r1=or, d1=od, r=r, d=d), + count = num_defined([ir,id,step]), + stepOK = is_undef(step) || (step>1 && step0 && angle<90) + assert(is_num(excess)) + let( + r = get_radius(r=r, d=d, dflt=1), + n = ceil(segs(r) * angle/360), + cp = [r,r], + tp = cp + polar_to_xy(r,180+angle), + bp = [tp.x+adj_ang_to_opp(tp.y,angle), 0], + step = angle/n, + path = [ + bp, bp-[0,excess], [-excess,-excess], [-excess,r], + for (i=[0:1:n]) cp+polar_to_xy(r,180+i*step) + ] + ) reorient(anchor,spin, two_d=true, path=path, p=path); + +module mask2d_teardrop(r,d,angle=45,excess=0.1,anchor=CENTER,spin=0) { + path = mask2d_teardrop(r=r, d=d, angle=angle, excess=excess); + attachable(anchor,spin, two_d=true, path=path) { + polygon(path); + children(); + } +} + +// Function&Module: mask2d_ogee() +// Usage: +// mask2d_ogee(pattern, [excess]); +// +// Description: +// Creates a 2D Ogee mask shape that is useful for extruding into a 3D mask for a 90º edge. +// This 2D mask is designed to be `difference()`d away from the edge of a shape that is in the first (X+Y+) quadrant. +// Since there are a number of shapes that fall under the name ogee, the shape of this mask is given as a pattern. +// Patterns are given as TYPE, VALUE pairs. ie: `["fillet",10, "xstep",2, "step",[5,5], ...]`. See Patterns below. +// If called as a function, this just returns a 2D path of the outline of the mask shape. +// +// ### Patterns +// +// Type | Argument | Description +// -------- | --------- | ---------------- +// "step" | [x,y] | Makes a line to a point `x` right and `y` down. +// "xstep" | dist | Makes a `dist` length line towards X+. +// "ystep" | dist | Makes a `dist` length line towards Y-. +// "round" | radius | Makes an arc that will mask a roundover. +// "fillet" | radius | Makes an arc that will mask a fillet. +// +// Arguments: +// pattern = A list of pattern pieces to describe the Ogee. +// excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. +// +// Example(2D): 2D Ogee Mask +// mask2d_ogee([ +// "xstep",1, "ystep",1, // Starting shoulder. +// "fillet",5, "round",5, // S-curve. +// "ystep",1, "xstep",1 // Ending shoulder. +// ]); +// Example: Masking by Edge Attachment +// diff("mask") +// cube([50,60,70],center=true) +// edge_profile(TOP) +// mask2d_ogee([ +// "xstep",1, "ystep",1, // Starting shoulder. +// "fillet",5, "round",5, // S-curve. +// "ystep",1, "xstep",1 // Ending shoulder. +// ]); +module mask2d_ogee(pattern, excess, anchor=CENTER,spin=0) { + path = mask2d_ogee(pattern, excess=excess); + attachable(anchor,spin, two_d=true, path=path) { + polygon(path); + children(); + } +} + +function mask2d_ogee(pattern, excess, anchor=CENTER, spin=0) = + assert(is_list(pattern)) + assert(len(pattern)>0) + assert(len(pattern)%2==0,"pattern must be a list of TYPE, VAL pairs.") + assert(all([for (i = idx(pattern,step=2)) in_list(pattern[i],["step","xstep","ystep","round","fillet"])])) + let( + excess = default(excess,$overlap), + x = concat([0], cumsum([ + for (i=idx(pattern,step=2)) let( + type = pattern[i], + val = pattern[i+1] + ) ( + type=="step"? val.x : + type=="xstep"? val : + type=="round"? val : + type=="fillet"? val : + 0 + ) + ])), + y = concat([0], cumsum([ + for (i=idx(pattern,step=2)) let( + type = pattern[i], + val = pattern[i+1] + ) ( + type=="step"? val.y : + type=="ystep"? val : + type=="round"? val : + type=="fillet"? val : + 0 + ) + ])), + tot_x = select(x,-1), + tot_y = select(y,-1), + data = [ + for (i=idx(pattern,step=2)) let( + type = pattern[i], + val = pattern[i+1], + pt = [x[i/2], tot_y-y[i/2]] + ( + type=="step"? [val.x,-val.y] : + type=="xstep"? [val,0] : + type=="ystep"? [0,-val] : + type=="round"? [val,0] : + type=="fillet"? [0,-val] : + [0,0] + ) + ) [type, val, pt] + ], + path = [ + [tot_x,-excess], + [-excess,-excess], + [-excess,tot_y], + for (pat = data) each + pat[0]=="step"? [pat[2]] : + pat[0]=="xstep"? [pat[2]] : + pat[0]=="ystep"? [pat[2]] : + let( + r = pat[1], + steps = segs(abs(r)), + step = 90/steps + ) [ + for (i=[0:1:steps]) let( + a = pat[0]=="round"? (180+i*step) : (90-i*step) + ) pat[2] + abs(r)*[cos(a),sin(a)] + ] + ], + path2 = deduplicate(path) + ) reorient(anchor,spin, two_d=true, path=path2, p=path2); + + + +// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap