diff --git a/affine.scad b/affine.scad index f510022..b1a328a 100644 --- a/affine.scad +++ b/affine.scad @@ -142,6 +142,9 @@ function affine3d_to_2d(m) = // Applies the specified transformation matrix to a point, pointlist, bezier patch or VNF. // Both inputs can be 2D or 3D, and it is also allowed to supply 3D transformations with 2D // data as long as the the only action on the z coordinate is a simple scaling. +// . +// If you construct your own matrices you can also use a transform that acts like a projection +// with fewer rows to produce lower dimensional output. // Arguments: // transform = The 2D or 3D transformation matrix to apply to the point/points. // points = The point, pointlist, bezier patch, or VNF to apply the transformation to. @@ -173,14 +176,15 @@ function apply(transform,points) = ? /* BezPatch */ [for (x=points) apply(transform,x)] : let( tdim = len(transform[0])-1, - datadim = len(points[0]) + datadim = len(points[0]), + outdim = min(datadim,len(transform)), + matrix = [for(i=[0:1:tdim]) [for(j=[0:1:outdim-1]) transform[j][i]]] ) - tdim == 3 && datadim == 3 ? [for(p=points) point3d(transform*concat(p,[1]))] : - tdim == 2 && datadim == 2 ? [for(p=points) point2d(transform*concat(p,[1]))] : - tdim == 3 && datadim == 2 ? + tdim==datadim && (datadim==3 || datadim==2) ? [for(p=points) concat(p,1)] * matrix + : tdim == 3 && datadim == 2 ? assert(is_2d_transform(transform), str("Transforms is 3d but points are 2d")) - [for(p=points) point2d(transform*concat(p,[0,1]))] : - assert(false, str("Unsupported combination: transform with dimension ",tdim,", data of dimension ",datadim)); + [for(p=points) concat(p,[0,1])]*matrix + : assert(false, str("Unsupported combination: transform with dimension ",tdim,", data of dimension ",datadim)); // Function: rot_decode() diff --git a/coords.scad b/coords.scad index 030d908..217a4a6 100644 --- a/coords.scad +++ b/coords.scad @@ -217,7 +217,7 @@ function xy_to_polar(x,y=undef) = let( // stroke(xypath,closed=true); function project_plane(plane,p) = is_matrix(plane,3,3) && is_undef(p) ? // no data, 3 points given - assert(!collinear(plane),"Points defining the plane must not be collinear") + assert(!is_collinear(plane),"Points defining the plane must not be collinear") let( v = plane[2]-plane[0], y = unit(plane[1]-plane[0]), // y axis goes to point b @@ -242,7 +242,7 @@ function project_plane(plane,p) = [for(plist=p) project_plane(plane,plist)] : assert(is_vector(p,3) || is_path(p,3),str("Data must be a 3d point, path, region, vnf or bezier patch",p)) is_matrix(plane,3,3) ? - assert(!collinear(plane),"Points defining the plane must not be collinear") + assert(!is_collinear(plane),"Points defining the plane must not be collinear") let( v = plane[2]-plane[0], y = unit(plane[1]-plane[0]), // y axis goes to point b diff --git a/distributors.scad b/distributors.scad index 4932463..5e08d37 100644 --- a/distributors.scad +++ b/distributors.scad @@ -7,7 +7,7 @@ ////////////////////////////////////////////////////////////////////// -// Section: Translational Distributors +// Section: Translating copies of all the children ////////////////////////////////////////////////////////////////////// @@ -267,185 +267,6 @@ module zcopies(spacing, n, l, sp) -// Module: distribute() -// -// Description: -// Spreads out each individual child along the direction `dir`. -// Every child is placed at a different position, in order. -// This is useful for laying out groups of disparate objects -// where you only really care about the spacing between them. -// -// Usage: -// distribute(spacing, dir, [sizes]) ... -// distribute(l, dir, [sizes]) ... -// -// Arguments: -// spacing = Spacing to add between each child. (Default: 10.0) -// sizes = Array containing how much space each child will need. -// dir = Vector direction to distribute copies along. -// l = Length to distribute copies along. -// -// Side Effects: -// `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. -// `$idx` is set to the index number of each child being copied. -// -// Example: -// distribute(sizes=[100, 30, 50], dir=UP) { -// sphere(r=50); -// cube([10,20,30], center=true); -// cylinder(d=30, h=50, center=true); -// } -module distribute(spacing=undef, sizes=undef, dir=RIGHT, l=undef) -{ - gaps = ($children < 2)? [0] : - !is_undef(sizes)? [for (i=[0:1:$children-2]) sizes[i]/2 + sizes[i+1]/2] : - [for (i=[0:1:$children-2]) 0]; - spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); - gaps2 = [for (gap = gaps) gap+spc]; - spos = dir * -sum(gaps2)/2; - spacings = cumsum([0, each gaps2]); - for (i=[0:1:$children-1]) { - $pos = spos + spacings[i] * dir; - $idx = i; - translate($pos) children(i); - } -} - - -// Module: xdistribute() -// -// Description: -// Spreads out each individual child along the X axis. -// Every child is placed at a different position, in order. -// This is useful for laying out groups of disparate objects -// where you only really care about the spacing between them. -// -// Usage: -// xdistribute(spacing, [sizes]) ... -// xdistribute(l, [sizes]) ... -// -// Arguments: -// spacing = spacing between each child. (Default: 10.0) -// sizes = Array containing how much space each child will need. -// l = Length to distribute copies along. -// -// Side Effects: -// `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. -// `$idx` is set to the index number of each child being copied. -// -// Example: -// xdistribute(sizes=[100, 10, 30], spacing=40) { -// sphere(r=50); -// cube([10,20,30], center=true); -// cylinder(d=30, h=50, center=true); -// } -module xdistribute(spacing=10, sizes=undef, l=undef) -{ - dir = RIGHT; - gaps = ($children < 2)? [0] : - !is_undef(sizes)? [for (i=[0:1:$children-2]) sizes[i]/2 + sizes[i+1]/2] : - [for (i=[0:1:$children-2]) 0]; - spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); - gaps2 = [for (gap = gaps) gap+spc]; - spos = dir * -sum(gaps2)/2; - spacings = cumsum([0, each gaps2]); - for (i=[0:1:$children-1]) { - $pos = spos + spacings[i] * dir; - $idx = i; - translate($pos) children(i); - } -} - - -// Module: ydistribute() -// -// Description: -// Spreads out each individual child along the Y axis. -// Every child is placed at a different position, in order. -// This is useful for laying out groups of disparate objects -// where you only really care about the spacing between them. -// -// Usage: -// ydistribute(spacing, [sizes]) -// ydistribute(l, [sizes]) -// -// Arguments: -// spacing = spacing between each child. (Default: 10.0) -// sizes = Array containing how much space each child will need. -// l = Length to distribute copies along. -// -// Side Effects: -// `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. -// `$idx` is set to the index number of each child being copied. -// -// Example: -// ydistribute(sizes=[30, 20, 100], spacing=40) { -// cylinder(d=30, h=50, center=true); -// cube([10,20,30], center=true); -// sphere(r=50); -// } -module ydistribute(spacing=10, sizes=undef, l=undef) -{ - dir = BACK; - gaps = ($children < 2)? [0] : - !is_undef(sizes)? [for (i=[0:1:$children-2]) sizes[i]/2 + sizes[i+1]/2] : - [for (i=[0:1:$children-2]) 0]; - spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); - gaps2 = [for (gap = gaps) gap+spc]; - spos = dir * -sum(gaps2)/2; - spacings = cumsum([0, each gaps2]); - for (i=[0:1:$children-1]) { - $pos = spos + spacings[i] * dir; - $idx = i; - translate($pos) children(i); - } -} - - -// Module: zdistribute() -// -// Description: -// Spreads out each individual child along the Z axis. -// Every child is placed at a different position, in order. -// This is useful for laying out groups of disparate objects -// where you only really care about the spacing between them. -// -// Usage: -// zdistribute(spacing, [sizes]) -// zdistribute(l, [sizes]) -// -// Arguments: -// spacing = spacing between each child. (Default: 10.0) -// sizes = Array containing how much space each child will need. -// l = Length to distribute copies along. -// -// Side Effects: -// `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. -// `$idx` is set to the index number of each child being copied. -// -// Example: -// zdistribute(sizes=[30, 20, 100], spacing=40) { -// cylinder(d=30, h=50, center=true); -// cube([10,20,30], center=true); -// sphere(r=50); -// } -module zdistribute(spacing=10, sizes=undef, l=undef) -{ - dir = UP; - gaps = ($children < 2)? [0] : - !is_undef(sizes)? [for (i=[0:1:$children-2]) sizes[i]/2 + sizes[i+1]/2] : - [for (i=[0:1:$children-2]) 0]; - spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); - gaps2 = [for (gap = gaps) gap+spc]; - spos = dir * -sum(gaps2)/2; - spacings = cumsum([0, each gaps2]); - for (i=[0:1:$children-1]) { - $pos = spos + spacings[i] * dir; - $idx = i; - translate($pos) children(i); - } -} - // Module: grid2d() @@ -632,7 +453,7 @@ module grid3d(xa=[0], ya=[0], za=[0], n=undef, spacing=undef) ////////////////////////////////////////////////////////////////////// -// Section: Rotational Distributors +// Section: Rotating copies of all children ////////////////////////////////////////////////////////////////////// @@ -1018,6 +839,7 @@ module ovoid_spread(r=undef, d=undef, n=100, cone_ang=90, scale=[1,1,1], perp=tr } } +// Section: Placing copies of all children on a path // Module: path_spread() @@ -1149,7 +971,7 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals ////////////////////////////////////////////////////////////////////// -// Section: Reflectional Distributors +// Section: Making a copy of all children with reflection ////////////////////////////////////////////////////////////////////// @@ -1305,6 +1127,190 @@ module zflip_copy(offset=0, z=0) mirror_copy(v=[0,0,1], offset=offset, cp=[0,0,z]) children(); } +//////////////////// +// Section: Distributing children individually along a line +/////////////////// + +// Module: distribute() +// +// Description: +// Spreads out each individual child along the direction `dir`. +// Every child is placed at a different position, in order. +// This is useful for laying out groups of disparate objects +// where you only really care about the spacing between them. +// +// Usage: +// distribute(spacing, dir, [sizes]) ... +// distribute(l, dir, [sizes]) ... +// +// Arguments: +// spacing = Spacing to add between each child. (Default: 10.0) +// sizes = Array containing how much space each child will need. +// dir = Vector direction to distribute copies along. +// l = Length to distribute copies along. +// +// Side Effects: +// `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. +// `$idx` is set to the index number of each child being copied. +// +// Example: +// distribute(sizes=[100, 30, 50], dir=UP) { +// sphere(r=50); +// cube([10,20,30], center=true); +// cylinder(d=30, h=50, center=true); +// } +module distribute(spacing=undef, sizes=undef, dir=RIGHT, l=undef) +{ + gaps = ($children < 2)? [0] : + !is_undef(sizes)? [for (i=[0:1:$children-2]) sizes[i]/2 + sizes[i+1]/2] : + [for (i=[0:1:$children-2]) 0]; + spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); + gaps2 = [for (gap = gaps) gap+spc]; + spos = dir * -sum(gaps2)/2; + spacings = cumsum([0, each gaps2]); + for (i=[0:1:$children-1]) { + $pos = spos + spacings[i] * dir; + $idx = i; + translate($pos) children(i); + } +} + + +// Module: xdistribute() +// +// Description: +// Spreads out each individual child along the X axis. +// Every child is placed at a different position, in order. +// This is useful for laying out groups of disparate objects +// where you only really care about the spacing between them. +// +// Usage: +// xdistribute(spacing, [sizes]) ... +// xdistribute(l, [sizes]) ... +// +// Arguments: +// spacing = spacing between each child. (Default: 10.0) +// sizes = Array containing how much space each child will need. +// l = Length to distribute copies along. +// +// Side Effects: +// `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. +// `$idx` is set to the index number of each child being copied. +// +// Example: +// xdistribute(sizes=[100, 10, 30], spacing=40) { +// sphere(r=50); +// cube([10,20,30], center=true); +// cylinder(d=30, h=50, center=true); +// } +module xdistribute(spacing=10, sizes=undef, l=undef) +{ + dir = RIGHT; + gaps = ($children < 2)? [0] : + !is_undef(sizes)? [for (i=[0:1:$children-2]) sizes[i]/2 + sizes[i+1]/2] : + [for (i=[0:1:$children-2]) 0]; + spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); + gaps2 = [for (gap = gaps) gap+spc]; + spos = dir * -sum(gaps2)/2; + spacings = cumsum([0, each gaps2]); + for (i=[0:1:$children-1]) { + $pos = spos + spacings[i] * dir; + $idx = i; + translate($pos) children(i); + } +} + + +// Module: ydistribute() +// +// Description: +// Spreads out each individual child along the Y axis. +// Every child is placed at a different position, in order. +// This is useful for laying out groups of disparate objects +// where you only really care about the spacing between them. +// +// Usage: +// ydistribute(spacing, [sizes]) +// ydistribute(l, [sizes]) +// +// Arguments: +// spacing = spacing between each child. (Default: 10.0) +// sizes = Array containing how much space each child will need. +// l = Length to distribute copies along. +// +// Side Effects: +// `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. +// `$idx` is set to the index number of each child being copied. +// +// Example: +// ydistribute(sizes=[30, 20, 100], spacing=40) { +// cylinder(d=30, h=50, center=true); +// cube([10,20,30], center=true); +// sphere(r=50); +// } +module ydistribute(spacing=10, sizes=undef, l=undef) +{ + dir = BACK; + gaps = ($children < 2)? [0] : + !is_undef(sizes)? [for (i=[0:1:$children-2]) sizes[i]/2 + sizes[i+1]/2] : + [for (i=[0:1:$children-2]) 0]; + spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); + gaps2 = [for (gap = gaps) gap+spc]; + spos = dir * -sum(gaps2)/2; + spacings = cumsum([0, each gaps2]); + for (i=[0:1:$children-1]) { + $pos = spos + spacings[i] * dir; + $idx = i; + translate($pos) children(i); + } +} + + +// Module: zdistribute() +// +// Description: +// Spreads out each individual child along the Z axis. +// Every child is placed at a different position, in order. +// This is useful for laying out groups of disparate objects +// where you only really care about the spacing between them. +// +// Usage: +// zdistribute(spacing, [sizes]) +// zdistribute(l, [sizes]) +// +// Arguments: +// spacing = spacing between each child. (Default: 10.0) +// sizes = Array containing how much space each child will need. +// l = Length to distribute copies along. +// +// Side Effects: +// `$pos` is set to the relative centerpoint of each child copy, and can be used to modify each child individually. +// `$idx` is set to the index number of each child being copied. +// +// Example: +// zdistribute(sizes=[30, 20, 100], spacing=40) { +// cylinder(d=30, h=50, center=true); +// cube([10,20,30], center=true); +// sphere(r=50); +// } +module zdistribute(spacing=10, sizes=undef, l=undef) +{ + dir = UP; + gaps = ($children < 2)? [0] : + !is_undef(sizes)? [for (i=[0:1:$children-2]) sizes[i]/2 + sizes[i+1]/2] : + [for (i=[0:1:$children-2]) 0]; + spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); + gaps2 = [for (gap = gaps) gap+spc]; + spos = dir * -sum(gaps2)/2; + spacings = cumsum([0, each gaps2]); + for (i=[0:1:$children-1]) { + $pos = spos + spacings[i] * dir; + $idx = i; + translate($pos) children(i); + } +} + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/drawing.scad b/drawing.scad new file mode 100644 index 0000000..8064ebc --- /dev/null +++ b/drawing.scad @@ -0,0 +1,934 @@ +////////////////////////////////////////////////////////////////////// +// LibFile: drawing.scad +// This file includes stroke(), which converts a path into a +// geometric object, like drawing with a pen. It even works on +// three-dimensional paths. You can make a dashed line or add arrow +// heads. The turtle() function provides a turtle graphics style +// approach for producing paths. The arc() function produces arc paths, +// and helix() produces helix paths. +// Includes: +// include +////////////////////////////////////////////////////////////////////// + + +// Section: Line Drawing + +// 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]); +// Topics: Paths (2D), Paths (3D), Drawing Tools +// Description: +// Draws a 2D or 3D path with a given line width. Endcaps can be specified for each end individually. +// Figure(Med,NoAxes,2D,VPR=[0,0,0],VPD=250): Endcap Types +// cap_pairs = [ +// ["butt", "chisel" ], +// ["round", "square" ], +// ["line", "cross" ], +// ["x", "diamond"], +// ["dot", "block" ], +// ["tail", "arrow" ], +// ["tail2", "arrow2" ] +// ]; +// for (i = idx(cap_pairs)) { +// fwd((i-len(cap_pairs)/2+0.5)*13) { +// stroke([[-20,0], [20,0]], width=3, endcap1=cap_pairs[i][0], endcap2=cap_pairs[i][1]); +// color("black") { +// stroke([[-20,0], [20,0]], width=0.25, endcaps=false); +// left(28) text(text=cap_pairs[i][0], size=5, halign="right", valign="center"); +// right(28) text(text=cap_pairs[i][1], size=5, halign="left", valign="center"); +// } +// } +// } +// Arguments: +// path = The 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. +// plots = Specifies the plot point shape for every point of the line. If a 2D path is given, use that to draw custom plot points. +// joints = Specifies the joint shape for each joint of the line. If a 2D path is given, use that to draw custom joints. +// 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. +// plot_width = Some plot point shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. +// joint_width = Some joint shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. +// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width. +// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width. +// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width. +// plot_length = Length of plot point shape, in multiples of the line width. +// joint_length = Length of joint shape, in multiples of the line width. +// endcap_length = Length of endcaps, in multiples of the line width. +// endcap_length1 = Length of starting endcap, in multiples of the line width. +// endcap_length2 = Length of ending endcap, in multiples of the line width. +// plot_extent = Extents length of plot point shape, in multiples of the line width. +// joint_extent = Extents length of joint shape, in multiples of the line width. +// endcap_extent = Extents length of endcaps, in multiples of the line width. +// endcap_extent1 = Extents length of starting endcap, in multiples of the line width. +// endcap_extent2 = Extents length of ending endcap, in multiples of the line width. +// plot_angle = Extra rotation given to plot point shapes, in degrees. If not given, the shapes are fully spun. +// joint_angle = Extra rotation given to joint shapes, in degrees. If not given, the shapes are fully spun. +// endcap_angle = Extra rotation given to endcaps, in degrees. If not given, the endcaps are fully spun. +// endcap_angle1 = Extra rotation given to a starting endcap, in degrees. If not given, the endcap is fully spun. +// endcap_angle2 = Extra rotation given to a ending endcap, in degrees. If not given, the endcap is fully spun. +// 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): Plotting Points +// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; +// stroke(path, width=3, joints="diamond", endcaps="arrow2", plot_angle=0, plot_width=5); +// Example(2D): Joints and Endcaps +// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; +// stroke(path, width=3, joints="dot", endcaps="arrow2", joint_angle=0); +// 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); +// Example: 3D Path with Joints and Endcaps +// path = [for (i=[0:10:360]) [(i-180)/2,20*cos(3*i),20*sin(3*i)]]; +// stroke(path, width=2, joints="dot", endcap1="round", endcap2="arrow2", joint_width=2.0, endcap_width2=3, $fn=18); +function stroke( + path, width=1, closed=false, + endcaps, endcap1, endcap2, joints, plots, + endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, + endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, + endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, + endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, + trim, trim1, trim2, + convexity=10, hull=true +) = no_function("stroke"); +module stroke( + path, width=1, closed=false, + endcaps, endcap1, endcap2, joints, plots, + endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, + endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, + endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, + endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, + trim, trim1, trim2, + convexity=10, hull=true +) { + function _shape_defaults(cap) = + cap==undef? [1.00, 0.00, 0.00] : + cap==false? [1.00, 0.00, 0.00] : + cap==true? [1.00, 1.00, 0.00] : + cap=="butt"? [1.00, 0.00, 0.00] : + cap=="round"? [1.00, 1.00, 0.00] : + cap=="chisel"? [1.00, 1.00, 0.00] : + cap=="square"? [1.00, 1.00, 0.00] : + cap=="block"? [3.00, 1.00, 0.00] : + cap=="diamond"? [3.50, 1.00, 0.00] : + cap=="dot"? [3.00, 1.00, 0.00] : + cap=="x"? [3.50, 0.40, 0.00] : + cap=="cross"? [4.50, 0.22, 0.00] : + cap=="line"? [4.50, 0.22, 0.00] : + cap=="arrow"? [3.50, 0.40, 0.50] : + cap=="arrow2"? [3.50, 1.00, 0.14] : + cap=="tail"? [3.50, 0.47, 0.50] : + cap=="tail2"? [3.50, 0.28, 0.50] : + is_path(cap)? [0.00, 0.00, 0.00] : + assert(false, str("Invalid cap or joint: ",cap)); + + function _shape_path(cap,linewidth,w,l,l2) = ( + (cap=="butt" || cap==false || cap==undef)? [] : + (cap=="round" || cap==true)? scale([w,l], p=circle(d=1, $fn=max(8, segs(w/2)))) : + cap=="chisel"? scale([w,l], p=circle(d=1,$fn=4)) : + cap=="diamond"? circle(d=w,$fn=4) : + cap=="square"? scale([w,l], p=square(1,center=true)) : + cap=="block"? scale([w,l], p=square(1,center=true)) : + cap=="dot"? circle(d=w, $fn=max(12, segs(w*3/2))) : + cap=="x"? [for (a=[0:90:270]) each rot(a,p=[[w+l/2,w-l/2]/2, [w-l/2,w+l/2]/2, [0,l/2]]) ] : + cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[l,w]/2, [-l,w]/2, [-l,l]/2]) ] : + cap=="line"? scale([w,l], p=square(1,center=true)) : + 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 : + assert(false, str("Invalid endcap: ",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; + assert(all([for (w=width) w>0])); + + endcap1 = first_defined([endcap1, endcaps, if(!closed) plots, "round"]); + endcap2 = first_defined([endcap2, endcaps, plots, "round"]); + joints = first_defined([joints, plots, "round"]); + assert(is_bool(endcap1) || is_string(endcap1) || is_path(endcap1)); + assert(is_bool(endcap2) || is_string(endcap2) || is_path(endcap2)); + assert(is_bool(joints) || is_string(joints) || is_path(joints)); + + endcap1_dflts = _shape_defaults(endcap1); + endcap2_dflts = _shape_defaults(endcap2); + joint_dflts = _shape_defaults(joints); + + endcap_width1 = first_defined([endcap_width1, endcap_width, plot_width, endcap1_dflts[0]]); + endcap_width2 = first_defined([endcap_width2, endcap_width, plot_width, endcap2_dflts[0]]); + joint_width = first_defined([joint_width, plot_width, joint_dflts[0]]); + assert(is_num(endcap_width1)); + assert(is_num(endcap_width2)); + assert(is_num(joint_width)); + + endcap_length1 = first_defined([endcap_length1, endcap_length, plot_length, endcap1_dflts[1]*endcap_width1]); + endcap_length2 = first_defined([endcap_length2, endcap_length, plot_length, endcap2_dflts[1]*endcap_width2]); + joint_length = first_defined([joint_length, plot_length, joint_dflts[1]*joint_width]); + assert(is_num(endcap_length1)); + assert(is_num(endcap_length2)); + assert(is_num(joint_length)); + + endcap_extent1 = first_defined([endcap_extent1, endcap_extent, plot_extent, endcap1_dflts[2]*endcap_width1]); + endcap_extent2 = first_defined([endcap_extent2, endcap_extent, plot_extent, endcap2_dflts[2]*endcap_width2]); + joint_extent = first_defined([joint_extent, plot_extent, joint_dflts[2]*joint_width]); + assert(is_num(endcap_extent1)); + assert(is_num(endcap_extent2)); + assert(is_num(joint_extent)); + + endcap_angle1 = first_defined([endcap_angle1, endcap_angle, plot_angle]); + endcap_angle2 = first_defined([endcap_angle2, endcap_angle, plot_angle]); + joint_angle = first_defined([joint_angle, plot_angle]); + assert(is_undef(endcap_angle1)||is_num(endcap_angle1)); + assert(is_undef(endcap_angle2)||is_num(endcap_angle2)); + assert(is_undef(joint_angle)||is_num(joint_angle)); + + endcap_shape1 = _shape_path(endcap1, width[0], endcap_width1, endcap_length1, endcap_extent1); + endcap_shape2 = _shape_path(endcap2, last(width), endcap_width2, endcap_length2, endcap_extent2); + + trim1 = width[0] * first_defined([ + trim1, trim, + (endcap1=="arrow")? endcap_length1-0.01 : + (endcap1=="arrow2")? endcap_length1*3/4 : + 0 + ]); + assert(is_num(trim1)); + + trim2 = last(width) * 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 = path[0] - path[1]; + end_vec = last(path) - select(path,-2); + + if (len(path[0]) == 2) { + // Straight segments + for (i = idx(path2,e=-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); + translate(path2[i]) { + if (joints != undef) { + joint_shape = _shape_path( + joints, width[i], + joint_width, + joint_length, + joint_extent + ); + v1 = unit(path2[i] - path2[i-1]); + v2 = unit(path2[i+1] - path2[i]); + vec = unit((v1+v2)/2); + mat = is_undef(joint_angle) + ? rot(from=BACK,to=v1) + : zrot(joint_angle); + multmatrix(mat) polygon(joint_shape); + } else if (hull) { + hull() { + 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 { + 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]) { + mat = is_undef(endcap_angle1)? rot(from=BACK,to=start_vec) : + zrot(endcap_angle1); + multmatrix(mat) polygon(endcap_shape1); + } + + // Endcap2 + translate(last(path)) { + mat = is_undef(endcap_angle2)? rot(from=BACK,to=end_vec) : + zrot(endcap_angle2); + multmatrix(mat) polygon(endcap_shape2); + } + } else { + quatsums = q_cumulative([ + for (i = idx(path2,e=-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,e=-2)) + quantup(segs(max(widths[i],widths[i+1])/2),4) + ]; + + // Straight segments + for (i = idx(path2,e=-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 (joints != undef) { + joint_shape = _shape_path( + joints, width[i], + joint_width, + joint_length, + joint_extent + ); + multmatrix(rotmats[i] * xrot(180)) { + $fn = sides[i]; + if (is_undef(joint_angle)) { + rotate_extrude(convexity=convexity) { + right_half(planar=true) { + polygon(joint_shape); + } + } + } else { + rotate([90,0,joint_angle]) { + linear_extrude(height=max(widths[i],0.001), center=true, convexity=convexity) { + polygon(joint_shape); + } + } + } + } + } else if (hull) { + hull(){ + multmatrix(rotmats[i]) { + sphere(d=widths[i],style="aligned"); + } + multmatrix(rotmats[i-1]) { + sphere(d=widths[i],style="aligned"); + } + } + } else { + multmatrix(rotmats[i]) { + sphere(d=widths[i],style="aligned"); + } + multmatrix(rotmats[i-1]) { + sphere(d=widths[i],style="aligned"); + } + } + } + } + + // 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=max(widths[0],0.001), center=true, convexity=convexity) { + polygon(endcap_shape1); + } + } + } + } + } + + // Endcap2 + translate(last(path)) { + multmatrix(last(rotmats)) { + $fn = last(sides); + 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=max(last(widths),0.001), center=true, convexity=convexity) { + polygon(endcap_shape2); + } + } + } + } + } + } + } +} + + +// Function&Module: dashed_stroke() +// Usage: As a Module +// dashed_stroke(path, dashpat, [closed=]); +// Usage: As a Function +// dashes = dashed_stroke(path, dashpat, width=, [closed=]); +// Topics: Paths, Drawing Tools +// See Also: stroke(), path_cut() +// Description: +// Given a path and a dash pattern, creates a dashed line that follows that +// path with the given dash pattern. +// - When called as a function, returns a list of dash sub-paths. +// - When called as a module, draws all those subpaths using `stroke()`. +// Arguments: +// path = The path to subdivide into dashes. +// dashpat = A list of alternating dash lengths and space lengths for the dash pattern. This will be scaled by the width of the line. +// --- +// width = The width of the dashed line to draw. Module only. Default: 1 +// closed = If true, treat path as a closed polygon. Default: false +// Example(2D): Open Path +// path = [for (a=[-180:10:180]) [a/3,20*sin(a)]]; +// dashed_stroke(path, [3,2], width=1); +// Example(2D): Closed Polygon +// path = circle(d=100,$fn=72); +// dashpat = [10,2,3,2,3,2]; +// dashed_stroke(path, dashpat, width=1, closed=true); +// Example(FlatSpin,VPD=250): 3D Dashed Path +// path = [for (a=[-180:5:180]) [a/3, 20*cos(3*a), 20*sin(3*a)]]; +// dashed_stroke(path, [3,2], width=1); +function dashed_stroke(path, dashpat=[3,3], closed=false) = + let( + path = closed? close_path(path) : path, + dashpat = len(dashpat)%2==0? dashpat : concat(dashpat,[0]), + plen = path_length(path), + dlen = sum(dashpat), + doff = cumsum(dashpat), + reps = floor(plen / dlen), + step = plen / reps, + cuts = [ + for (i=[0:1:reps-1], off=doff) + let (st=i*step, x=st+off) + if (x>0 && x=2), "Number of points must be an integer 2 or larger") + // 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(is_def(r) && r>0, "Arc radius invalid") + let( + N = is_def(N) ? N : max(3, ceil(segs(r)*abs(angle)/360)), + 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( + plane = [is_def(cp) ? cp : points[2], points[0], points[1]], + center2d = is_def(cp) ? project_plane(plane,cp) : undef, + points2d = project_plane(plane, points) + ) + lift_plane(plane,arc(N,cp=center2d,points=points2d,wedge=wedge,long=long)) + ) : 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 = is_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: helix() +// Description: +// Returns a 3D helical path. +// Usage: +// helix(turns, h, n, r|d, [cp], [scale]); +// Arguments: +// h = Height of spiral. +// turns = Number of turns in spiral. +// n = Number of spiral sides. +// r = Radius of spiral. +// d = Radius of spiral. +// cp = Centerpoint of spiral. Default: `[0,0]` +// scale = [X,Y] scaling factors for each axis. Default: `[1,1]` +// Example(3D): +// trace_path(helix(turns=2.5, h=100, n=24, r=50), N=1, showpts=true); +function helix(turns=3, h=100, n=12, r, d, cp=[0,0], scale=[1,1]) = let( + rr=get_radius(r=r, d=d, dflt=100), + cnt=floor(turns*n), + dz=h/cnt + ) [ + for (i=[0:1:cnt]) [ + rr * cos(i*360/n) * scale.x + cp.x, + rr * sin(i*360/n) * scale.y + cp.y, + i*dz + ] + ]; + + + +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], [full_state=], [repeat=]) +// Topics: Shapes (2D), Path Generators (2D), Mini-Language +// See Also: turtle3d() +// 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, the current default angle, +// and the current arcsteps setting. +// . +// 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)); +module turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) {no_module();} +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, default arcsteps] + +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 = last(state[path]) + ) + 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], list_tail(arcpath)), + 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], list_tail(arcpath)), + rot(delta_angle,p=state[step],planar=true) + ] + ) : + assert(false,str("Unknown turtle command \"",command,"\" at index",index)) + []; + diff --git a/geometry.scad b/geometry.scad index 52b12a3..82b0e33 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1,6 +1,9 @@ ////////////////////////////////////////////////////////////////////// // LibFile: geometry.scad -// Geometry helpers. +// Perform calculations on lines, polygons, planes and circles, including +// normals, intersections of objects, distance between objects, and tangent lines. +// Throughout this library, lines can be treated as either unbounded lines, as rays with +// a single endpoint or as segments, bounded by endpoints at both ends. // Includes: // include ////////////////////////////////////////////////////////////////////// @@ -8,20 +11,23 @@ // Section: Lines, Rays, and Segments -// Function: point_on_segment() +// Function: is_point_on_line() // Usage: -// pt = point_on_segment(point, edge); +// pt = is_point_on_line(point, line, [bounded], [eps]); // Topics: Geometry, Points, Segments // Description: -// Determine if the point is on the line segment between two points. -// Returns true if yes, and false if not. +// Determine if the point is on the line segment, ray or segment defined by the two between two points. +// Returns true if yes, and false if not. If bounded is set to true it specifies a segment, with +// both lines bounded at the ends. Set bounded to `[true,false]` to get a ray. You can use +// the shorthands RAY and SEGMENT to set bounded. // Arguments: // point = The point to test. -// edge = Array of two points forming the line segment to test against. +// line = Array of two points defining the line, ray, or segment to test against. +// bounded = boolean or list of two booleans defining endpoint conditions for the line. If false treat the line as an unbounded line. If true treat it as a segment. If [true,false] treat as a ray, based at the first endpoint. Default: false // eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) -function point_on_segment(point, edge, eps=EPSILON) = +function is_point_on_line(point, line, bounded=false, eps=EPSILON) = assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) - point_line_distance(point, edge, SEGMENT)=0), "The tolerance should be a non-negative value." ) let( pb = furthest_point(points[0],points) ) norm(points[pb]-points[0])=0, "The tolerance should be a non-negative value." ) len(points)<=2 ? false @@ -533,7 +539,7 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = let( plane = plane_from_normal(poly_normal, poly[0]) ) - fast? plane: points_on_plane(poly, plane, eps=eps)? plane: []; + fast? plane: are_points_on_plane(poly, plane, eps=eps)? plane: []; // Function: plane_normal() @@ -621,59 +627,183 @@ function plane_line_intersection(plane, line, bounded=false, eps=EPSILON) = // Function: polygon_line_intersection() // Usage: -// pt = polygon_line_intersection(poly, line, [bounded], [eps]); +// pt = polygon_line_intersection(poly, line, [bounded], [nonzero], [eps]); // Topics: Geometry, Polygons, Lines, Intersection // Description: -// Takes a possibly bounded line, and a 3D planar polygon, and finds their intersection point. -// If the line and the polygon are on the same plane then returns a list, possibly empty, of 3D line -// segments, one for each section of the line that is inside the polygon. -// If the line is not on the plane of the polygon, but intersects it, then returns the 3D intersection -// point. If the line does not intersect the polygon, then `undef` is returned. +// Takes a possibly bounded line, and a 2D or 3D planar polygon, and finds their intersection. +// If the line does not intersect the polygon then `undef` returns `undef`. +// In 3D if the line is not on the plane of the polygon but intersects it then you get a single intersection point. +// Otherwise the polygon and line are in the same plane, or when your input is 2D, ou will get a list of segments and +// single point lists. Use `is_vector` to distinguish these two cases. +// . +// In the 2D case, when single points are in the intersection they appear on the segment list as lists of a single point +// (like single point segments) so a single point intersection in 2D has the form `[[[x,y,z]]]` as compared +// to a single point intersection in 3D which has the form `[x,y,z]`. You can identify whether an entry in the +// segment list is a true segment by checking its length, which will be 2 for a segment and 1 for a point. // Arguments: // poly = The 3D planar polygon to find the intersection with. // line = A list of two distinct 3D points on the line. // bounded = If false, the line is considered unbounded. If true, it is treated as a bounded line segment. If given as `[true, false]` or `[false, true]`, the boundedness of the points are specified individually, allowing the line to be treated as a half-bounded ray. Default: false (unbounded) +// nonzero = set to true to use the nonzero rule for determining it points are in a polygon. See point_in_polygon. Default: false. // eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) -function polygon_line_intersection(poly, line, bounded=false, eps=EPSILON) = +// Example: The line intersects the 3d hexagon in a single point. +// hex = zrot(140,p=rot([-45,40,20],p=path3d(hexagon(r=15)))); +// line = [[5,0,-13],[-3,-5,13]]; +// isect = polygon_line_intersection(hex,line); +// stroke(hex,closed=true); +// stroke(line); +// color("red")move(isect)sphere(r=1); +// Example: In 2D things are more complicated. The output is a list of intersection parts, in the simplest case a single segment. +// hex = hexagon(r=15); +// line = [[-20,10],[25,-7]]; +// isect = polygon_line_intersection(hex,line); +// stroke(hex,closed=true); +// stroke(line,endcaps="arrow2"); +// color("red") +// for(part=isect) +// if(len(part)==1) +// move(part[0]) sphere(r=1); +// else +// stroke(part); +// Example: In 2D things are more complicated. Here the line is treated as a ray. +// hex = hexagon(r=15); +// line = [[0,0],[25,-7]]; +// isect = polygon_line_intersection(hex,line,RAY); +// stroke(hex,closed=true); +// stroke(line,endcap2="arrow2"); +// color("red") +// for(part=isect) +// if(len(part)==1) +// move(part[0]) circle(r=1,$fn=12); +// else +// stroke(part); +// Example: Here the intersection is a single point, which is returned as a single point "path" on the path list. +// hex = hexagon(r=15); +// line = [[15,-10],[15,13]]; +// isect = polygon_line_intersection(hex,line,RAY); +// stroke(hex,closed=true); +// stroke(line,endcap2="arrow2"); +// color("red") +// for(part=isect) +// if(len(part)==1) +// move(part[0]) circle(r=1,$fn=12); +// else +// stroke(part); +// Example: Another way to get a single segment +// hex = hexagon(r=15); +// line = rot(30,p=[[15,-10],[15,25]],cp=[15,0]); +// isect = polygon_line_intersection(hex,line,RAY); +// stroke(hex,closed=true); +// stroke(line,endcap2="arrow2"); +// color("red") +// for(part=isect) +// if(len(part)==1) +// move(part[0]) circle(r=1,$fn=12); +// else +// stroke(part); +// Example: Single segment again +// star = star(r=15,n=8,step=2); +// line = [[20,-5],[-5,20]]; +// isect = polygon_line_intersection(star,line,RAY); +// stroke(star,closed=true); +// stroke(line,endcap2="arrow2"); +// color("red") +// for(part=isect) +// if(len(part)==1) +// move(part[0]) circle(r=1,$fn=12); +// else +// stroke(part); +// Example: Solution is two points +// star = star(r=15,n=8,step=3); +// line = rot(22.5,p=[[15,-10],[15,20]],cp=[15,0]); +// isect = polygon_line_intersection(star,line,SEGMENT); +// stroke(star,closed=true); +// stroke(line); +// color("red") +// for(part=isect) +// if(len(part)==1) +// move(part[0]) circle(r=1,$fn=12); +// else +// stroke(part); +// Example: Solution is list of three segments +// star = star(r=25,ir=9,n=8); +// line = [[-25,12],[25,12]]; +// isect = polygon_line_intersection(star,line); +// stroke(star,closed=true); +// stroke(line,endcaps="arrow2"); +// color("red") +// for(part=isect) +// if(len(part)==1) +// move(part[0]) circle(r=1,$fn=12); +// else +// stroke(part); +// Example: Solution is a mixture of segments and points +// star = star(r=25,ir=9,n=7); +// line = [left(10,p=star[8]), right(50,p=star[8])]; +// isect = polygon_line_intersection(star,line); +// stroke(star,closed=true); +// stroke(line,endcaps="arrow2"); +// color("red") +// for(part=isect) +// if(len(part)==1) +// move(part[0]) circle(r=1,$fn=12); +// else +// stroke(part); +function polygon_line_intersection(poly, line, bounded=false, nonzero=false, eps=EPSILON) = assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) - assert(is_path(poly,dim=3), "Invalid polygon." ) + assert(is_path(poly,dim=[2,3]), "Invalid polygon." ) assert(is_bool(bounded) || is_bool_list(bounded,2), "Invalid bound condition.") - assert(_valid_line(line,dim=3,eps=eps), "Invalid 3D line." ) + assert(_valid_line(line,dim=len(poly[0]),eps=eps), "Line invalid or does not match polygon dimension." ) let( bounded = force_list(bounded,2), - poly = deduplicate(poly), - indices = noncollinear_triple(poly) - ) indices==[] ? undef : - let( - p1 = poly[indices[0]], - p2 = poly[indices[1]], - p3 = poly[indices[2]], - plane = plane3pt(p1,p2,p3), - res = _general_plane_line_intersection(plane, line, eps=eps) - ) is_undef(res)? undef : - is_undef(res[1]) ? ( - let(// Line is on polygon plane. + poly = deduplicate(poly) + ) + len(poly[0])==2 ? // planar case + let( linevec = unit(line[1] - line[0]), - lp1 = line[0] + (bounded[0]? 0 : -1000000) * linevec, - lp2 = line[1] + (bounded[1]? 0 : 1000000) * linevec, - poly2d = clockwise_polygon(project_plane(plane, poly)), - line2d = project_plane(plane, [lp1,lp2]), - parts = split_path_at_region_crossings(line2d, [poly2d], closed=false), + bound = 100*max(flatten(pointlist_bounds(poly))), + boundedline = [line[0] + (bounded[0]? 0 : -bound) * linevec, + line[1] + (bounded[1]? 0 : bound) * linevec], + parts = split_path_at_region_crossings(boundedline, [poly], closed=false), inside = [ - for (part = parts) - if (point_in_polygon(mean(part), poly2d)>0) part - ] - ) !inside? undef : - let( isegs = [for (seg = inside) lift_plane(plane, seg) ] ) - isegs - ) : - bounded[0] && res[1]<0? undef : - bounded[1] && res[1]>1? undef : - let( - proj = clockwise_polygon(project_plane([p1, p2, p3], poly)), - pt = project_plane([p1, p2, p3], res[0]) - ) point_in_polygon(pt, proj) < 0 ? undef : - res[0]; + if(point_in_polygon(parts[0][0], poly, nonzero=nonzero, eps=eps) == 0) + [parts[0][0]], // Add starting point if it is on the polygon + for(part = parts) + if (point_in_polygon(mean(part), poly, nonzero=nonzero, eps=eps) >=0 ) + part + else if(len(part)==2 && point_in_polygon(part[1], poly, nonzero=nonzero, eps=eps) == 0) + [part[1]] // Add segment end if it is on the polygon + ] + ) + (len(inside)==0 ? undef : _merge_segments(inside, [inside[0]], eps)) + : // 3d case + let(indices = noncollinear_triple(poly)) + indices==[] ? undef : // Polygon is collinear + let( + plane = plane3pt(poly[indices[0]], poly[indices[1]], poly[indices[2]]), + plane_isect = plane_line_intersection(plane, line, bounded, eps) + ) + is_undef(plane_isect) ? undef : + is_vector(plane_isect,3) ? + let( + poly2d = project_plane(plane,poly), + pt2d = project_plane(plane, plane_isect) + ) + (point_in_polygon(pt2d, poly2d, nonzero=nonzero, eps=eps) < 0 ? undef : plane_isect) + : // Case where line is on the polygon plane + let( + poly2d = project_plane(plane, poly), + line2d = project_plane(plane, line), + segments = polygon_line_intersection(poly2d, line2d, bounded=bounded, nonzero=nonzero, eps=eps) + ) + segments==undef ? undef + : [for(seg=segments) len(seg)==2 ? lift_plane(plane,seg) : [lift_plane(plane,seg[0])]]; + +function _merge_segments(insegs,outsegs, eps, i=1) = + i==len(insegs) ? outsegs : + approx(last(last(outsegs)), insegs[i][0], eps) + ? _merge_segments(insegs, [each list_head(outsegs),[last(outsegs)[0],last(insegs[i])]], eps, i+1) + : _merge_segments(insegs, [each outsegs, insegs[i]], eps, i+1); // Function: plane_intersection() @@ -796,9 +926,9 @@ function _pointlist_greatest_distance(points,plane) = abs(max( max(pt_nrm) - plane[3], -min(pt_nrm) + plane[3])) / norm(normal); -// Function: points_on_plane() +// Function: are_points_on_plane() // Usage: -// test = points_on_plane(points, plane, [eps]); +// test = are_points_on_plane(points, plane, [eps]); // Topics: Geometry, Planes, Points // Description: // Returns true if the given 3D points are on the given plane. @@ -806,14 +936,14 @@ function _pointlist_greatest_distance(points,plane) = // plane = The plane to test the points on. // points = The list of 3D points to test. // eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) -function points_on_plane(points, plane, eps=EPSILON) = +function are_points_on_plane(points, plane, eps=EPSILON) = assert( _valid_plane(plane), "Invalid plane." ) assert( is_matrix(points,undef,3) && len(points)>0, "Invalid pointlist." ) // using is_matrix it accepts len(points)==1 assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) _pointlist_greatest_distance(points,plane) < eps; -// Function: above_plane() +// Function: is_above_plane() // Usage: // test = in_front_of_plane(plane, point); // Topics: Geometry, Planes @@ -825,13 +955,51 @@ function points_on_plane(points, plane, eps=EPSILON) = // Arguments: // plane = The [A,B,C,D] coefficients for the first plane equation `Ax+By+Cz=D`. // point = The 3D point to test. -function above_plane(plane, point) = +function is_above_plane(plane, point) = point_plane_distance(plane, point) > EPSILON; // Section: Circle Calculations +// Function: circle_line_intersection() +// Usage: +// isect = circle_line_intersection(c,,[line],[bounded],[eps]); +// Topics: Geometry, Circles, Lines, Intersection +// Description: +// Find intersection points between a 2d circle and a line, ray or segment specified by two points. +// By default the line is unbounded. +// Arguments: +// c = center of circle +// r = radius of circle +// --- +// d = diameter of circle +// line = two points defining the unbounded line +// bounded = false for unbounded line, true for a segment, or a vector [false,true] or [true,false] to specify a ray with the first or second end unbounded. Default: false +// eps = epsilon used for identifying the case with one solution. Default: 1e-9 +function circle_line_intersection(c,r,d,line,bounded=false,eps=EPSILON) = + let(r=get_radius(r=r,d=d,dflt=undef)) + assert(_valid_line(line,2), "Invalid 2d line.") + assert(is_vector(c,2), "Circle center must be a 2-vector") + assert(is_num(r) && r>0, "Radius must be positive") + assert(is_bool(bounded) || is_bool_list(bounded,2), "Invalid bound condition") + let( + bounded = force_list(bounded,2), + closest = line_closest_point(line,c), + d = norm(closest-c) + ) + d > r ? [] : + let( + isect = approx(d,r,eps) ? [closest] : + let( offset = sqrt(r*r-d*d), + uvec=unit(line[1]-line[0]) + ) [closest-offset*uvec, closest+offset*uvec] + ) + [for(p=isect) + if ((!bounded[0] || (p-line[0])*(line[1]-line[0])>=0) + && (!bounded[1] || (p-line[1])*(line[0]-line[1])>=0)) p]; + + // Function&Module: circle_2tangents() // Usage: As Function // circ = circle_2tangents(pt1, pt2, pt3, r|d, [tangents]); @@ -906,7 +1074,7 @@ function circle_2tangents(pt1, pt2, pt3, r, d, tangents=false) = "Invalid input points." ) is_undef(pt2) ? circle_2tangents(pt1[0], pt1[1], pt1[2], r=r, tangents=tangents) - : collinear(pt1, pt2, pt3)? undef : + : is_collinear(pt1, pt2, pt3)? undef : let( v1 = unit(pt1 - pt2), v2 = unit(pt3 - pt2), @@ -991,7 +1159,7 @@ function circle_3points(pt1, pt2, pt3) = : assert( is_vector(pt1) && is_vector(pt2) && is_vector(pt3) && max(len(pt1),len(pt2),len(pt3))<=3 && min(len(pt1),len(pt2),len(pt3))>=2, "Invalid point(s)." ) - collinear(pt1,pt2,pt3)? [undef,undef,undef] : + is_collinear(pt1,pt2,pt3)? [undef,undef,undef] : let( v = [ point3d(pt1), point3d(pt2), point3d(pt3) ], // triangle vertices ed = [for(i=[0:2]) v[(i+1)%3]-v[i] ], // triangle edge vectors @@ -1142,44 +1310,6 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = ]; -// Function: circle_line_intersection() -// Usage: -// isect = circle_line_intersection(c,,[line],[bounded],[eps]); -// Topics: Geometry, Circles, Lines, Intersection -// Description: -// Find intersection points between a 2d circle and a line, ray or segment specified by two points. -// By default the line is unbounded. -// Arguments: -// c = center of circle -// r = radius of circle -// --- -// d = diameter of circle -// line = two points defining the unbounded line -// bounded = false for unbounded line, true for a segment, or a vector [false,true] or [true,false] to specify a ray with the first or second end unbounded. Default: false -// eps = epsilon used for identifying the case with one solution. Default: 1e-9 -function circle_line_intersection(c,r,d,line,bounded=false,eps=EPSILON) = - let(r=get_radius(r=r,d=d,dflt=undef)) - assert(_valid_line(line,2), "Invalid 2d line.") - assert(is_vector(c,2), "Circle center must be a 2-vector") - assert(is_num(r) && r>0, "Radius must be positive") - assert(is_bool(bounded) || is_bool_list(bounded,2), "Invalid bound condition") - let( - bounded = force_list(bounded,2), - closest = line_closest_point(line,c), - d = norm(closest-c) - ) - d > r ? [] : - let( - isect = approx(d,r,eps) ? [closest] : - let( offset = sqrt(r*r-d*d), - uvec=unit(line[1]-line[0]) - ) [closest-offset*uvec, closest+offset*uvec] - ) - [for(p=isect) - if ((!bounded[0] || (p-line[0])*(line[1]-line[0])>=0) - && (!bounded[1] || (p-line[1])*(line[0]-line[1])>=0)) p]; - - // Section: Pointlists @@ -1302,17 +1432,16 @@ function centroid(poly, eps=EPSILON) = function polygon_normal(poly) = assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( - L=len(poly), - area_vec = sum([for(i=idx(poly)) - cross(poly[(i+1)%L]-poly[0], - poly[(i+2)%L]-poly[(i+1)%L])]) + area_vec = sum([for(i=[1:len(poly)-2]) + cross(poly[i]-poly[0], + poly[i+1]-poly[i])]) ) - norm(area_vec)2, "The point and polygon should be in 2D. The polygon should have more that 2 points." ) @@ -1401,7 +1530,7 @@ function point_in_polygon(point, poly, nonzero=true, eps=EPSILON) = for (i = [0:1:len(poly)-1]) let( seg = select(poly,i,i+1) ) if (!approx(seg[0],seg[1],eps) ) - point_on_segment(point, seg, eps=eps)? 1:0 + is_point_on_line(point, seg, SEGMENT, eps=eps)? 1:0 ] ) sum(on_brd) > 0? 0 : @@ -1432,6 +1561,124 @@ function point_in_polygon(point, poly, nonzero=true, eps=EPSILON) = ) 2*(len(cross)%2)-1; +// Function: polygon_triangulate() +// Usage: +// triangles = polygon_triangulate(poly, [ind], [eps]) +// Description: +// Given a simple polygon in 2D or 3D, triangulates it and returns a list +// of triples indexing into the polygon vertices. When the optional argument `ind` is +// given, the it is used as an index list into `poly` to define the polygon. In that case, +// `poly` may have a length greater than `ind`. Otherwise, all points in `poly` +// are considered as vertices of the polygon. +// . +// The function may issue an error if it finds that the polygon is not simple +// (self-intersecting) or its vertices are collinear. It can work for 3d non-planar polygons +// if they are close enough to planar but may otherwise issue an error for this case. +// . +// For 2d polygons, the output triangles will have the same winding (CW or CCW) of +// the input polygon. For 3d polygons, the triangle windings will induce a normal +// vector with the same direction of the polygon normal. +// Arguments: +// poly = Array of vertices for the polygon. +// ind = A list indexing the vertices of the polygon in `poly`. +// eps = A maximum tolerance in geometrical tests. Default: EPSILON +// Example: +// poly = star(id=10, od=15,n=11); +// tris = polygon_triangulate(poly); +// polygon(poly); +// up(1) +// color("blue"); +// for(tri=tris) trace_path(select(poly,tri), size=.1, closed=true); +// +// Example: +// include +// vnf = regular_polyhedron_info(name="dodecahedron",side=5,info="vnf"); +// %vnf_polyhedron(vnf); +// vnf_tri = [vnf[0], [for(face=vnf[1]) each polygon_triangulate(vnf[0], face) ] ]; +// color("blue") +// vnf_wireframe(vnf_tri, d=.15); + +function polygon_triangulate(poly, ind, eps=EPSILON) = + assert(is_path(poly), "Polygon `poly` should be a list of 2d or 3d points") + assert(is_undef(ind) + || (is_vector(ind) && min(ind)>=0 && max(ind)=len(ind) ? undef : // poly has no ears + let( // the _i-th ear candidate + p0 = poly[ind[_i]], + p1 = poly[ind[(_i+1)%len(ind)]], + p2 = poly[ind[(_i+2)%len(ind)]] + ) + // if it is not a convex vertex, try the next one + _is_cw2(p0,p1,p2,eps) ? _get_ear(poly,ind,eps, _i=_i+1) : + let( // vertex p1 is convex; check if the triangle contains any other point + to_tst = select(ind,_i+3, _i-1), + pt2tst = select(poly,to_tst), // points other than p0, p1 and p2 + q = [(p0-p2).y, (p2-p0).x], // orthogonal to ray [p0,p2] pointing right + q0 = q*p0, + atleft = [for(p=pt2tst) if(p*q<=q0) p ] + ) + atleft==[] ? _i : // no point inside -> an ear + let( + q = [(p2-p1).y, (p1-p2).x], // orthogonal to ray [p1,p2] pointing right + q0 = q*p2, + atleft = [for(p=atleft) if(p*q<=q0) p ] + ) + atleft==[] ? _i : // no point inside -> an ear + let( + q = [(p1-p0).y, (p0-p1).x], // orthogonal to ray [p1,p0] pointing right + q0 = q*p1, + atleft = [for(p=atleft) if(p*q<=q0) p ] + ) + atleft==[] ? _i : // no point inside -> an ear + // check the next ear candidate + _get_ear(poly, ind, eps, _i=_i+1); + +function _is_cw2(a,b,c,eps=EPSILON) = cross(a-c,b-c)= len(points) || !points_on_plane([points[i]],plane))? i : + (i >= len(points) || !are_points_on_plane([points[i]],plane))? i : _find_first_noncoplanar(plane, points, i+1); diff --git a/paths.scad b/paths.scad index 00748c3..a0e7c52 100644 --- a/paths.scad +++ b/paths.scad @@ -6,9 +6,6 @@ ////////////////////////////////////////////////////////////////////// -include - - // Section: Functions @@ -121,7 +118,7 @@ function simplify_path(path, eps=EPSILON) = indices = [ 0, for (i=[1:1:len(path)-2]) - if (!collinear(path[i-1], path[i], path[i+1], eps=eps)) i, + if (!is_collinear(path[i-1], path[i], path[i+1], eps=eps)) i, len(path)-1 ] ) [for (i=indices) path[i]]; @@ -148,7 +145,7 @@ function simplify_path_indexed(points, indices, eps=EPSILON) = i1 = indices[i-1], i2 = indices[i], i3 = indices[i+1] - ) if (!collinear(points[i1], points[i2], points[i3], eps=eps)) + ) if (!is_collinear(points[i1], points[i2], points[i3], eps=eps)) indices[i] ], indices[len(indices)-1] @@ -604,38 +601,11 @@ function path_add_jitter(path, dist=1/512, closed=true) = path[0], for (i=idx(path,s=1,e=closed?-1:-2)) let( n = line_normal([path[i-1],path[i]]) - ) path[i] + n * (collinear(select(path,i-1,i+1))? (dist * ((i%2)*2-1)) : 0), + ) path[i] + n * (is_collinear(select(path,i-1,i+1))? (dist * ((i%2)*2-1)) : 0), if (!closed) last(path) ]; -// Function: path3d_spiral() -// Description: -// Returns a 3D spiral path. -// Usage: -// path3d_spiral(turns, h, n, r|d, [cp], [scale]); -// Arguments: -// h = Height of spiral. -// turns = Number of turns in spiral. -// n = Number of spiral sides. -// r = Radius of spiral. -// d = Radius of spiral. -// cp = Centerpoint of spiral. Default: `[0,0]` -// scale = [X,Y] scaling factors for each axis. Default: `[1,1]` -// Example(3D): -// trace_path(path3d_spiral(turns=2.5, h=100, n=24, r=50), N=1, showpts=true); -function path3d_spiral(turns=3, h=100, n=12, r, d, cp=[0,0], scale=[1,1]) = let( - rr=get_radius(r=r, d=d, dflt=100), - cnt=floor(turns*n), - dz=h/cnt - ) [ - for (i=[0:1:cnt]) [ - rr * cos(i*360/n) * scale.x + cp.x, - rr * sin(i*360/n) * scale.y + cp.y, - i*dz - ] - ]; - // Function: path_self_intersections() // Usage: @@ -1004,7 +974,7 @@ function _path_cuts_normals(path, cuts, dirs, closed=false) = // to define the plane of the path. function _path_plane(path, ind, i,closed) = i<(closed?-1:0) ? undef : - !collinear(path[ind],path[ind-1], select(path,i))? + !is_collinear(path[ind],path[ind-1], select(path,i))? [select(path,i)-path[ind-1],path[ind]-path[ind-1]] : _path_plane(path, ind, i-1); diff --git a/rounding.scad b/rounding.scad index 27ff5ce..45555da 100644 --- a/rounding.scad +++ b/rounding.scad @@ -1865,16 +1865,16 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b assert(jsvecok || jssingleok, str("Argument joint_sides is invalid. All entries must be nonnegative, and it must be a number, 2-vector, or a length ",N," list those.")) assert(is_num(k_sides) || is_vector(k_sides,N), str("Curvature parameter k_sides must be a number or length ",N," vector")) - assert(coplanar(bottom)) - assert(coplanar(top)) + assert(is_coplanar(bottom)) + assert(is_coplanar(top)) assert(!is_num(k_sides) || (k_sides>=0 && k_sides<=1), "Curvature parameter k_sides must be in interval [0,1]") let( - non_coplanar=[for(i=[0:N-1]) if (!coplanar(concat(select(top,i,i+1), select(bottom,i,i+1)))) [i,(i+1)%N]], + non_coplanar=[for(i=[0:N-1]) if (!is_coplanar(concat(select(top,i,i+1), select(bottom,i,i+1)))) [i,(i+1)%N]], k_sides_vec = is_num(k_sides) ? repeat(k_sides, N) : k_sides, kbad = [for(i=[0:N-1]) if (k_sides_vec[i]<0 || k_sides_vec[i]>1) i], joint_sides_vec = jssingleok ? repeat(joint_sides,N) : joint_sides, - top_collinear = [for(i=[0:N-1]) if (collinear(select(top,i-1,i+1))) i], - bot_collinear = [for(i=[0:N-1]) if (collinear(select(bottom,i-1,i+1))) i] + top_collinear = [for(i=[0:N-1]) if (is_collinear(select(top,i-1,i+1))) i], + bot_collinear = [for(i=[0:N-1]) if (is_collinear(select(bottom,i-1,i+1))) i] ) assert(non_coplanar==[], str("Side faces are non-coplanar at edges: ",non_coplanar)) assert(top_collinear==[], str("Top has collinear or duplicated points at indices: ",top_collinear)) @@ -1940,14 +1940,14 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b vline = concat(select(subindex(top_patch[i],j),2,4), select(subindex(bot_patch[i],j),2,4)) ) - if (!collinear(vline)) [i,j]], + if (!is_collinear(vline)) [i,j]], //verify horiz edges verify_horiz=[for(i=[0:N-1], j=[0:4]) let( hline_top = concat(select(top_patch[i][j],2,4), select(select(top_patch, i+1)[j],0,2)), hline_bot = concat(select(bot_patch[i][j],2,4), select(select(bot_patch, i+1)[j],0,2)) ) - if (!collinear(hline_top) || !collinear(hline_bot)) [i,j]] + if (!is_collinear(hline_top) || !is_collinear(hline_bot)) [i,j]] ) assert(debug || top_intersections==[], "Roundovers interfere with each other on top face: either input is self intersecting or top joint length is too large") @@ -1958,7 +1958,8 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b vnf = vnf_merge([ each subindex(top_samples,0), each subindex(bot_samples,0), for(pts=edge_points) vnf_vertex_array(pts), - vnf_triangulate(vnf_add_faces(EMPTY_VNF,faces)) + debug ? vnf_add_faces(EMPTY_VNF,faces) + : vnf_triangulate(vnf_add_faces(EMPTY_VNF,faces)) ]) ) debug ? [concat(top_patch, bot_patch), vnf] : vnf; diff --git a/shapes2d.scad b/shapes2d.scad index 378f82e..51b70e2 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1,10 +1,6 @@ ////////////////////////////////////////////////////////////////////// // LibFile: shapes2d.scad -// This file includes stroke(), which converts a path into a -// geometric object, like drawing with a pen. It even works on -// three-dimensional paths. You can make a dashed line or add arrow -// heads. The turtle() function provides a turtle graphics style -// approach for producing paths. You can create regular polygons +// This file lets you create regular polygons // with optional rounded corners and alignment features not // available with circle(). The file also provides teardrop2d, // which is useful for 3d printable holes. Lastly you can use the @@ -16,897 +12,6 @@ // include ////////////////////////////////////////////////////////////////////// -// Section: Line Drawing - -// 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]); -// Topics: Paths (2D), Paths (3D), Drawing Tools -// Description: -// Draws a 2D or 3D path with a given line width. Endcaps can be specified for each end individually. -// Figure(Med,NoAxes,2D,VPR=[0,0,0],VPD=250): Endcap Types -// cap_pairs = [ -// ["butt", "chisel" ], -// ["round", "square" ], -// ["line", "cross" ], -// ["x", "diamond"], -// ["dot", "block" ], -// ["tail", "arrow" ], -// ["tail2", "arrow2" ] -// ]; -// for (i = idx(cap_pairs)) { -// fwd((i-len(cap_pairs)/2+0.5)*13) { -// stroke([[-20,0], [20,0]], width=3, endcap1=cap_pairs[i][0], endcap2=cap_pairs[i][1]); -// color("black") { -// stroke([[-20,0], [20,0]], width=0.25, endcaps=false); -// left(28) text(text=cap_pairs[i][0], size=5, halign="right", valign="center"); -// right(28) text(text=cap_pairs[i][1], size=5, halign="left", valign="center"); -// } -// } -// } -// Arguments: -// path = The 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. -// plots = Specifies the plot point shape for every point of the line. If a 2D path is given, use that to draw custom plot points. -// joints = Specifies the joint shape for each joint of the line. If a 2D path is given, use that to draw custom joints. -// 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. -// plot_width = Some plot point shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. -// joint_width = Some joint shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. -// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width. -// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width. -// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width. -// plot_length = Length of plot point shape, in multiples of the line width. -// joint_length = Length of joint shape, in multiples of the line width. -// endcap_length = Length of endcaps, in multiples of the line width. -// endcap_length1 = Length of starting endcap, in multiples of the line width. -// endcap_length2 = Length of ending endcap, in multiples of the line width. -// plot_extent = Extents length of plot point shape, in multiples of the line width. -// joint_extent = Extents length of joint shape, in multiples of the line width. -// endcap_extent = Extents length of endcaps, in multiples of the line width. -// endcap_extent1 = Extents length of starting endcap, in multiples of the line width. -// endcap_extent2 = Extents length of ending endcap, in multiples of the line width. -// plot_angle = Extra rotation given to plot point shapes, in degrees. If not given, the shapes are fully spun. -// joint_angle = Extra rotation given to joint shapes, in degrees. If not given, the shapes are fully spun. -// endcap_angle = Extra rotation given to endcaps, in degrees. If not given, the endcaps are fully spun. -// endcap_angle1 = Extra rotation given to a starting endcap, in degrees. If not given, the endcap is fully spun. -// endcap_angle2 = Extra rotation given to a ending endcap, in degrees. If not given, the endcap is fully spun. -// 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): Plotting Points -// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; -// stroke(path, width=3, joints="diamond", endcaps="arrow2", plot_angle=0, plot_width=5); -// Example(2D): Joints and Endcaps -// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; -// stroke(path, width=3, joints="dot", endcaps="arrow2", joint_angle=0); -// 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); -// Example: 3D Path with Joints and Endcaps -// path = [for (i=[0:10:360]) [(i-180)/2,20*cos(3*i),20*sin(3*i)]]; -// stroke(path, width=2, joints="dot", endcap1="round", endcap2="arrow2", joint_width=2.0, endcap_width2=3, $fn=18); -function stroke( - path, width=1, closed=false, - endcaps, endcap1, endcap2, joints, plots, - endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, - endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, - endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, - endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, - trim, trim1, trim2, - convexity=10, hull=true -) = no_function("stroke"); -module stroke( - path, width=1, closed=false, - endcaps, endcap1, endcap2, joints, plots, - endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, - endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, - endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, - endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, - trim, trim1, trim2, - convexity=10, hull=true -) { - function _shape_defaults(cap) = - cap==undef? [1.00, 0.00, 0.00] : - cap==false? [1.00, 0.00, 0.00] : - cap==true? [1.00, 1.00, 0.00] : - cap=="butt"? [1.00, 0.00, 0.00] : - cap=="round"? [1.00, 1.00, 0.00] : - cap=="chisel"? [1.00, 1.00, 0.00] : - cap=="square"? [1.00, 1.00, 0.00] : - cap=="block"? [3.00, 1.00, 0.00] : - cap=="diamond"? [3.50, 1.00, 0.00] : - cap=="dot"? [3.00, 1.00, 0.00] : - cap=="x"? [3.50, 0.40, 0.00] : - cap=="cross"? [4.50, 0.22, 0.00] : - cap=="line"? [4.50, 0.22, 0.00] : - cap=="arrow"? [3.50, 0.40, 0.50] : - cap=="arrow2"? [3.50, 1.00, 0.14] : - cap=="tail"? [3.50, 0.47, 0.50] : - cap=="tail2"? [3.50, 0.28, 0.50] : - is_path(cap)? [0.00, 0.00, 0.00] : - assert(false, str("Invalid cap or joint: ",cap)); - - function _shape_path(cap,linewidth,w,l,l2) = ( - (cap=="butt" || cap==false || cap==undef)? [] : - (cap=="round" || cap==true)? scale([w,l], p=circle(d=1, $fn=max(8, segs(w/2)))) : - cap=="chisel"? scale([w,l], p=circle(d=1,$fn=4)) : - cap=="diamond"? circle(d=w,$fn=4) : - cap=="square"? scale([w,l], p=square(1,center=true)) : - cap=="block"? scale([w,l], p=square(1,center=true)) : - cap=="dot"? circle(d=w, $fn=max(12, segs(w*3/2))) : - cap=="x"? [for (a=[0:90:270]) each rot(a,p=[[w+l/2,w-l/2]/2, [w-l/2,w+l/2]/2, [0,l/2]]) ] : - cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[l,w]/2, [-l,w]/2, [-l,l]/2]) ] : - cap=="line"? scale([w,l], p=square(1,center=true)) : - 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 : - assert(false, str("Invalid endcap: ",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; - assert(all([for (w=width) w>0])); - - endcap1 = first_defined([endcap1, endcaps, if(!closed) plots, "round"]); - endcap2 = first_defined([endcap2, endcaps, plots, "round"]); - joints = first_defined([joints, plots, "round"]); - assert(is_bool(endcap1) || is_string(endcap1) || is_path(endcap1)); - assert(is_bool(endcap2) || is_string(endcap2) || is_path(endcap2)); - assert(is_bool(joints) || is_string(joints) || is_path(joints)); - - endcap1_dflts = _shape_defaults(endcap1); - endcap2_dflts = _shape_defaults(endcap2); - joint_dflts = _shape_defaults(joints); - - endcap_width1 = first_defined([endcap_width1, endcap_width, plot_width, endcap1_dflts[0]]); - endcap_width2 = first_defined([endcap_width2, endcap_width, plot_width, endcap2_dflts[0]]); - joint_width = first_defined([joint_width, plot_width, joint_dflts[0]]); - assert(is_num(endcap_width1)); - assert(is_num(endcap_width2)); - assert(is_num(joint_width)); - - endcap_length1 = first_defined([endcap_length1, endcap_length, plot_length, endcap1_dflts[1]*endcap_width1]); - endcap_length2 = first_defined([endcap_length2, endcap_length, plot_length, endcap2_dflts[1]*endcap_width2]); - joint_length = first_defined([joint_length, plot_length, joint_dflts[1]*joint_width]); - assert(is_num(endcap_length1)); - assert(is_num(endcap_length2)); - assert(is_num(joint_length)); - - endcap_extent1 = first_defined([endcap_extent1, endcap_extent, plot_extent, endcap1_dflts[2]*endcap_width1]); - endcap_extent2 = first_defined([endcap_extent2, endcap_extent, plot_extent, endcap2_dflts[2]*endcap_width2]); - joint_extent = first_defined([joint_extent, plot_extent, joint_dflts[2]*joint_width]); - assert(is_num(endcap_extent1)); - assert(is_num(endcap_extent2)); - assert(is_num(joint_extent)); - - endcap_angle1 = first_defined([endcap_angle1, endcap_angle, plot_angle]); - endcap_angle2 = first_defined([endcap_angle2, endcap_angle, plot_angle]); - joint_angle = first_defined([joint_angle, plot_angle]); - assert(is_undef(endcap_angle1)||is_num(endcap_angle1)); - assert(is_undef(endcap_angle2)||is_num(endcap_angle2)); - assert(is_undef(joint_angle)||is_num(joint_angle)); - - endcap_shape1 = _shape_path(endcap1, width[0], endcap_width1, endcap_length1, endcap_extent1); - endcap_shape2 = _shape_path(endcap2, last(width), endcap_width2, endcap_length2, endcap_extent2); - - trim1 = width[0] * first_defined([ - trim1, trim, - (endcap1=="arrow")? endcap_length1-0.01 : - (endcap1=="arrow2")? endcap_length1*3/4 : - 0 - ]); - assert(is_num(trim1)); - - trim2 = last(width) * 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 = path[0] - path[1]; - end_vec = last(path) - select(path,-2); - - if (len(path[0]) == 2) { - // Straight segments - for (i = idx(path2,e=-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); - translate(path2[i]) { - if (joints != undef) { - joint_shape = _shape_path( - joints, width[i], - joint_width, - joint_length, - joint_extent - ); - v1 = unit(path2[i] - path2[i-1]); - v2 = unit(path2[i+1] - path2[i]); - vec = unit((v1+v2)/2); - mat = is_undef(joint_angle) - ? rot(from=BACK,to=v1) - : zrot(joint_angle); - multmatrix(mat) polygon(joint_shape); - } else if (hull) { - hull() { - 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 { - 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]) { - mat = is_undef(endcap_angle1)? rot(from=BACK,to=start_vec) : - zrot(endcap_angle1); - multmatrix(mat) polygon(endcap_shape1); - } - - // Endcap2 - translate(last(path)) { - mat = is_undef(endcap_angle2)? rot(from=BACK,to=end_vec) : - zrot(endcap_angle2); - multmatrix(mat) polygon(endcap_shape2); - } - } else { - quatsums = q_cumulative([ - for (i = idx(path2,e=-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,e=-2)) - quantup(segs(max(widths[i],widths[i+1])/2),4) - ]; - - // Straight segments - for (i = idx(path2,e=-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 (joints != undef) { - joint_shape = _shape_path( - joints, width[i], - joint_width, - joint_length, - joint_extent - ); - multmatrix(rotmats[i] * xrot(180)) { - $fn = sides[i]; - if (is_undef(joint_angle)) { - rotate_extrude(convexity=convexity) { - right_half(planar=true) { - polygon(joint_shape); - } - } - } else { - rotate([90,0,joint_angle]) { - linear_extrude(height=max(widths[i],0.001), center=true, convexity=convexity) { - polygon(joint_shape); - } - } - } - } - } else if (hull) { - hull(){ - multmatrix(rotmats[i]) { - sphere(d=widths[i],style="aligned"); - } - multmatrix(rotmats[i-1]) { - sphere(d=widths[i],style="aligned"); - } - } - } else { - multmatrix(rotmats[i]) { - sphere(d=widths[i],style="aligned"); - } - multmatrix(rotmats[i-1]) { - sphere(d=widths[i],style="aligned"); - } - } - } - } - - // 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=max(widths[0],0.001), center=true, convexity=convexity) { - polygon(endcap_shape1); - } - } - } - } - } - - // Endcap2 - translate(last(path)) { - multmatrix(last(rotmats)) { - $fn = last(sides); - 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=max(last(widths),0.001), center=true, convexity=convexity) { - polygon(endcap_shape2); - } - } - } - } - } - } - } -} - - -// Function&Module: dashed_stroke() -// Usage: As a Module -// dashed_stroke(path, dashpat, [closed=]); -// Usage: As a Function -// dashes = dashed_stroke(path, dashpat, width=, [closed=]); -// Topics: Paths, Drawing Tools -// See Also: stroke(), path_cut() -// Description: -// Given a path and a dash pattern, creates a dashed line that follows that -// path with the given dash pattern. -// - When called as a function, returns a list of dash sub-paths. -// - When called as a module, draws all those subpaths using `stroke()`. -// Arguments: -// path = The path to subdivide into dashes. -// dashpat = A list of alternating dash lengths and space lengths for the dash pattern. This will be scaled by the width of the line. -// --- -// width = The width of the dashed line to draw. Module only. Default: 1 -// closed = If true, treat path as a closed polygon. Default: false -// Example(2D): Open Path -// path = [for (a=[-180:10:180]) [a/3,20*sin(a)]]; -// dashed_stroke(path, [3,2], width=1); -// Example(2D): Closed Polygon -// path = circle(d=100,$fn=72); -// dashpat = [10,2,3,2,3,2]; -// dashed_stroke(path, dashpat, width=1, closed=true); -// Example(FlatSpin,VPD=250): 3D Dashed Path -// path = [for (a=[-180:5:180]) [a/3, 20*cos(3*a), 20*sin(3*a)]]; -// dashed_stroke(path, [3,2], width=1); -function dashed_stroke(path, dashpat=[3,3], closed=false) = - let( - path = closed? close_path(path) : path, - dashpat = len(dashpat)%2==0? dashpat : concat(dashpat,[0]), - plen = path_length(path), - dlen = sum(dashpat), - doff = cumsum(dashpat), - reps = floor(plen / dlen), - step = plen / reps, - cuts = [ - for (i=[0:1:reps-1], off=doff) - let (st=i*step, x=st+off) - if (x>0 && x=2), "Number of points must be an integer 2 or larger") - // 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(is_def(r) && r>0, "Arc radius invalid") - let( - N = is_def(N) ? N : max(3, ceil(segs(r)*abs(angle)/360)), - 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( - plane = [is_def(cp) ? cp : points[2], points[0], points[1]], - center2d = is_def(cp) ? project_plane(plane,cp) : undef, - points2d = project_plane(plane, points) - ) - lift_plane(plane,arc(N,cp=center2d,points=points2d,wedge=wedge,long=long)) - ) : 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], [full_state=], [repeat=]) -// Topics: Shapes (2D), Path Generators (2D), Mini-Language -// See Also: turtle3d() -// 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, the current default angle, -// and the current arcsteps setting. -// . -// 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)); -module turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) {no_module();} -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, default arcsteps] - -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 = last(state[path]) - ) - 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], list_tail(arcpath)), - 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], list_tail(arcpath)), - rot(delta_angle,p=state[step],planar=true) - ] - ) : - assert(false,str("Unknown turtle command \"",command,"\" at index",index)) - []; - - // Section: 2D Primitives @@ -1556,14 +661,16 @@ function star(n, r, ir, d, or, od, id, step, realign=false, align_tip, align_pit assert(is_undef(align_tip) || is_vector(align_tip)) assert(is_undef(align_pit) || is_vector(align_pit)) assert(is_undef(align_tip) || is_undef(align_pit), "Can only specify one of align_tip and align_pit") + assert(is_def(n), "Must specify number of points, n") let( r = get_radius(r1=or, d1=od, r=r, d=d), count = num_defined([ir,id,step]), stepOK = is_undef(step) || (step>1 && step include include include -include +include include +include include include include diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 8deb425..844b807 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -6,8 +6,8 @@ include <../std.scad> -test_point_on_segment(); -test_collinear(); +test_is_point_on_line(); +test_is_collinear(); test_point_line_distance(); test_segment_distance(); test_line_normal(); @@ -33,9 +33,9 @@ test_plane_line_angle(); test_plane_line_intersection(); test_polygon_line_intersection(); test_plane_intersection(); -test_coplanar(); -test_points_on_plane(); -test_above_plane(); +test_is_coplanar(); +test_are_points_on_plane(); +test_is_above_plane(); test_circle_2tangents(); test_circle_3points(); test_circle_point_tangents(); @@ -44,7 +44,6 @@ test_noncollinear_triple(); test_polygon_area(); test_is_polygon_convex(); test_polygon_shift(); -test_polygon_shift_to_closest_point(); test_reindex_polygon(); test_align_polygon(); test_centroid(); @@ -53,10 +52,8 @@ test_is_polygon_clockwise(); test_clockwise_polygon(); test_ccw_polygon(); test_reverse_polygon(); -//test_polygon_normal(); -//test_split_polygons_at_each_x(); -//test_split_polygons_at_each_y(); -//test_split_polygons_at_each_z(); + +test_polygon_normal(); //tests to migrate to other files test_is_path(); @@ -225,7 +222,7 @@ module test__general_plane_line_intersection() { *test__general_plane_line_intersection(); -module test_points_on_plane() { +module test_are_points_on_plane() { pts = [for(i=[0:40]) rands(-1,1,3) ]; dir = rands(-10,10,3); normal0 = [1,2,3]; @@ -234,10 +231,10 @@ module test_points_on_plane() { plane = [each normal, normal*dir]; prj_pts = plane_closest_point(plane,pts); info = info_str([["pts = ",pts],["dir = ",dir],["ang = ",ang]]); - assert(points_on_plane(prj_pts,plane),info); - assert(!points_on_plane(concat(pts,[normal-dir]),plane),info); + assert(are_points_on_plane(prj_pts,plane),info); + assert(!are_points_on_plane(concat(pts,[normal-dir]),plane),info); } -*test_points_on_plane(); +*test_are_points_on_plane(); module test_plane_closest_point(){ ang = rands(0,360,1)[0]; @@ -268,35 +265,43 @@ module test_line_from_points() { } *test_line_from_points(); -module test_point_on_segment() { - assert(point_on_segment([-15,0], [[-10,0], [10,0]]) == false); - assert(point_on_segment([-10,0], [[-10,0], [10,0]]) == true); - assert(point_on_segment([-5,0], [[-10,0], [10,0]]) == true); - assert(point_on_segment([0,0], [[-10,0], [10,0]]) == true); - assert(point_on_segment([3,3], [[-10,0], [10,0]]) == false); - assert(point_on_segment([5,0], [[-10,0], [10,0]]) == true); - assert(point_on_segment([10,0], [[-10,0], [10,0]]) == true); - assert(point_on_segment([15,0], [[-10,0], [10,0]]) == false); +module test_is_point_on_line() { + assert(is_point_on_line([-15,0], [[-10,0], [10,0]],SEGMENT) == false); + assert(is_point_on_line([-10,0], [[-10,0], [10,0]],SEGMENT) == true); + assert(is_point_on_line([-5,0], [[-10,0], [10,0]],SEGMENT) == true); + assert(is_point_on_line([0,0], [[-10,0], [10,0]],SEGMENT) == true); + assert(is_point_on_line([3,3], [[-10,0], [10,0]],SEGMENT) == false); + assert(is_point_on_line([5,0], [[-10,0], [10,0]],SEGMENT) == true); + assert(is_point_on_line([10,0], [[-10,0], [10,0]],SEGMENT) == true); + assert(is_point_on_line([15,0], [[-10,0], [10,0]],SEGMENT) == false); - assert(point_on_segment([0,-15], [[0,-10], [0,10]]) == false); - assert(point_on_segment([0,-10], [[0,-10], [0,10]]) == true); - assert(point_on_segment([0, -5], [[0,-10], [0,10]]) == true); - assert(point_on_segment([0, 0], [[0,-10], [0,10]]) == true); - assert(point_on_segment([3, 3], [[0,-10], [0,10]]) == false); - assert(point_on_segment([0, 5], [[0,-10], [0,10]]) == true); - assert(point_on_segment([0, 10], [[0,-10], [0,10]]) == true); - assert(point_on_segment([0, 15], [[0,-10], [0,10]]) == false); + assert(is_point_on_line([0,-15], [[0,-10], [0,10]],SEGMENT) == false); + assert(is_point_on_line([0,-10], [[0,-10], [0,10]],SEGMENT) == true); + assert(is_point_on_line([0, -5], [[0,-10], [0,10]],SEGMENT) == true); + assert(is_point_on_line([0, 0], [[0,-10], [0,10]],SEGMENT) == true); + assert(is_point_on_line([3, 3], [[0,-10], [0,10]],SEGMENT) == false); + assert(is_point_on_line([0, 5], [[0,-10], [0,10]],SEGMENT) == true); + assert(is_point_on_line([0, 10], [[0,-10], [0,10]],SEGMENT) == true); + assert(is_point_on_line([0, 15], [[0,-10], [0,10]],SEGMENT) == false); - assert(point_on_segment([-15,-15], [[-10,-10], [10,10]]) == false); - assert(point_on_segment([-10,-10], [[-10,-10], [10,10]]) == true); - assert(point_on_segment([ -5, -5], [[-10,-10], [10,10]]) == true); - assert(point_on_segment([ 0, 0], [[-10,-10], [10,10]]) == true); - assert(point_on_segment([ 0, 3], [[-10,-10], [10,10]]) == false); - assert(point_on_segment([ 5, 5], [[-10,-10], [10,10]]) == true); - assert(point_on_segment([ 10, 10], [[-10,-10], [10,10]]) == true); - assert(point_on_segment([ 15, 15], [[-10,-10], [10,10]]) == false); + assert(is_point_on_line([-15,-15], [[-10,-10], [10,10]],SEGMENT) == false); + assert(is_point_on_line([-10,-10], [[-10,-10], [10,10]],SEGMENT) == true); + assert(is_point_on_line([ -5, -5], [[-10,-10], [10,10]],SEGMENT) == true); + assert(is_point_on_line([ 0, 0], [[-10,-10], [10,10]],SEGMENT) == true); + assert(is_point_on_line([ 0, 3], [[-10,-10], [10,10]],SEGMENT) == false); + assert(is_point_on_line([ 5, 5], [[-10,-10], [10,10]],SEGMENT) == true); + assert(is_point_on_line([ 10, 10], [[-10,-10], [10,10]],SEGMENT) == true); + assert(is_point_on_line([ 15, 15], [[-10,-10], [10,10]],SEGMENT) == false); + + assert(is_point_on_line([10,10], [[0,0],[5,5]]) == true); + assert(is_point_on_line([4,4], [[0,0],[5,5]]) == true); + assert(is_point_on_line([-2,-2], [[0,0],[5,5]]) == true); + assert(is_point_on_line([5,5], [[0,0],[5,5]]) == true); + assert(is_point_on_line([10,10], [[0,0],[5,5]],RAY) == true); + assert(is_point_on_line([0,0], [[0,0],[5,5]],RAY) == true); + assert(is_point_on_line([3,3], [[0,0],[5,5]],RAY) == true); } -*test_point_on_segment(); +*test_is_point_on_line(); module test__point_left_of_line2d() { @@ -306,18 +311,18 @@ module test__point_left_of_line2d() { } test__point_left_of_line2d(); -module test_collinear() { - assert(collinear([-10,-10], [-15, -16], [10,10]) == false); - assert(collinear([[-10,-10], [-15, -16], [10,10]]) == false); - assert(collinear([-10,-10], [-15, -15], [10,10]) == true); - assert(collinear([[-10,-10], [-15, -15], [10,10]]) == true); - assert(collinear([-10,-10], [ -3, 0], [10,10]) == false); - assert(collinear([-10,-10], [ 0, 0], [10,10]) == true); - assert(collinear([-10,-10], [ 3, 0], [10,10]) == false); - assert(collinear([-10,-10], [ 15, 15], [10,10]) == true); - assert(collinear([-10,-10], [ 15, 16], [10,10]) == false); +module test_is_collinear() { + assert(is_collinear([-10,-10], [-15, -16], [10,10]) == false); + assert(is_collinear([[-10,-10], [-15, -16], [10,10]]) == false); + assert(is_collinear([-10,-10], [-15, -15], [10,10]) == true); + assert(is_collinear([[-10,-10], [-15, -15], [10,10]]) == true); + assert(is_collinear([-10,-10], [ -3, 0], [10,10]) == false); + assert(is_collinear([-10,-10], [ 0, 0], [10,10]) == true); + assert(is_collinear([-10,-10], [ 3, 0], [10,10]) == false); + assert(is_collinear([-10,-10], [ 15, 15], [10,10]) == true); + assert(is_collinear([-10,-10], [ 15, 16], [10,10]) == false); } -*test_collinear(); +*test_is_collinear(); module test_point_line_distance() { @@ -593,6 +598,16 @@ module test_plane_from_points() { *test_plane_from_points(); +module test_polygon_normal() { + circ = path3d(circle($fn=37, r=3)); + + assert_approx(polygon_normal(circ), UP); + assert_approx(polygon_normal(rot(from=UP,to=[1,2,3],p=circ)), unit([1,2,3])); + assert_approx(polygon_normal(rot(from=UP,to=[4,-2,3],p=reverse(circ))), -unit([4,-2,3])); + assert_approx(polygon_normal(path3d([[0,0], [10,10], [11,10], [0,-1], [-1,1]])), UP); +} +*test_polygon_normal(); + module test_plane_normal() { assert_approx(plane_normal(plane3pt([0,0,20], [0,10,10], [0,0,0])), [1,0,0]); assert_approx(plane_normal(plane3pt([2,0,20], [2,10,10], [2,0,0])), [1,0,0]); @@ -650,30 +665,90 @@ module test_polygon_line_intersection() { undef, info); assert_approx(polygon_line_intersection(polygnr,linegnr,bounded=[false,false]), trnsl, info); + + sq = path3d(square(10)); + pentagram = 10*path3d(turtle(["move",10,"left",144], repeat=4)); + for (tran = [ident(4), skew(sxy=1.2)*scale([.9,1,1.2])*yrot(14)*zrot(37)*xrot(9)]) + { + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[5,5,-1], [5,5,10]])), apply(tran, [5,5,0])); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[5,5,1], [5,5,10]])), apply(tran, [5,5,0])); + assert(undef==polygon_line_intersection(apply(tran,sq),apply(tran,[[5,5,1], [5,5,10]]),RAY)); + assert(undef==polygon_line_intersection(apply(tran,sq),apply(tran,[[11,11,-1],[11,11,1]]))); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[5,0,-10], [5,0,10]])), apply(tran, [5,0,0])); + assert_equal(polygon_line_intersection(apply(tran,sq),apply(tran,[[5,0,1], [5,0,10]]),RAY), undef); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[10,0,1],[10,0,10]])), apply(tran, [10,0,0])); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[1,5,0],[9,6,0]])), apply(tran, [[[0,4.875,0],[10,6.125,0]]])); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[1,5,0],[9,6,0]]),SEGMENT), apply(tran, [[[1,5,0],[9,6,0]]])); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[-1,-1,0],[8,8,0]])), apply(tran, [[[0,0,0],[10,10,0]]])); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[-1,-1,0],[8,8,0]]),SEGMENT), apply(tran, [[[0,0,0],[8,8,0]]])); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[-1,-1,0],[8,8,0]]),RAY), apply(tran, [[[0,0,0],[10,10,0]]])); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[-2,4,0], [12,11,0]]),RAY), apply(tran, [[[0,5,0],[10,10,0]]])); + assert_equal(polygon_line_intersection(apply(tran,sq),apply(tran,[[-20,0,0],[20,40,0]]),RAY), undef); + assert_approx(polygon_line_intersection(apply(tran,sq),apply(tran,[[-1,0,0],[11,0,0]])), apply(tran, [[[0,0,0],[10,0,0]]])); + } + assert_approx(polygon_line_intersection(path2d(sq),[[1,5],[9,6]],SEGMENT), [[[1,5],[9,6]]]); + assert_approx(polygon_line_intersection(path2d(sq),[[1,5],[9,6]],LINE), [[[0,4.875],[10,6.125]]]); + assert_approx(polygon_line_intersection(pentagram,[[50,10,-4],[54,12,4]], nonzero=true), [52,11,0]); + assert_equal(polygon_line_intersection(pentagram,[[50,10,-4],[54,12,4]], nonzero=false), undef); + assert_approx(polygon_line_intersection(pentagram,[[50,-10,-4],[54,-12,4]], nonzero=true), [52,-11,0]); + assert_approx(polygon_line_intersection(pentagram,[[50,-10,-4],[54,-12,4]], nonzero=false), [52,-11,0]); + assert_approx(polygon_line_intersection(star(8,step=3,od=10), [[-5,3], [5,3]]), + [[[-3.31370849898, 3], [-2.24264068712, 3]], + [[-0.828427124746, 3], [0.828427124746, 3]], + [[2.24264068712, 3], [3.31370849898, 3]]]); + + tran = skew(sxy=1.2)*scale([.9,1,1.2])*yrot(14)*zrot(37)*xrot(9); + + // assemble multiple edges into one edge + assert_approx(polygon_line_intersection(star(r=15,n=8,step=2), [[20,-5],[-5,20]]), [[[15,0],[0,15]]]); + assert_approx(polygon_line_intersection(apply(tran,path3d(star(r=15,n=8,step=2))), apply(tran,[[20,-5,0],[-5,20,0]])), apply(tran,[[[15,0,0],[0,15,0]]])); + // line going the other direction + assert_approx(polygon_line_intersection(star(r=15,n=8,step=2), [[-5,20],[20,-5]]), [[[0,15],[15,0]]]); + assert_approx(polygon_line_intersection(apply(tran,path3d(star(r=15,n=8,step=2))), apply(tran,[[-5,20,0],[20,-5,0]])),apply(tran, [[[0,15,0],[15,0,0]]])); + // single point + assert_approx(polygon_line_intersection(hexagon(r=15), [[15,-10],[15,13]], RAY), [[[15,0]]]); + assert_approx(polygon_line_intersection(apply(tran,path3d(hexagon(r=15))), apply(tran,[[15,-10,0],[15,13,0]]), RAY), + [[apply(tran,[15,0,0])]]); + // two points + assert_approx(polygon_line_intersection(star(r=15,n=8,step=3), rot(22.5,p=[[15,-10],[15,20]],cp=[15,0])), + [[[15,0]], [[10.6066017178, 10.6066017178]]]); + assert_approx(polygon_line_intersection(apply(tran,path3d(star(r=15,n=8,step=3))), apply(tran,rot(22.5,p=[[15,-10,0],[15,20,0]],cp=[15,0,0]))), + [[apply(tran,[15,0,0])], [apply(tran,[10.6066017178, 10.6066017178,0])]]); + // two segments and one point + star7 = star(r=25,ir=9,n=7); + assert_approx(polygon_line_intersection(star7, [left(10,p=star7[8]), right(50,p=star7[8])]), + [[[-22.5242216976, 10.8470934779]], + [[-5.60077322195, 10.8470934779], [0.997372374838, 10.8470934779]], + [[4.61675816681, 10.8470934779], [11.4280421589, 10.8470934779]]]); + assert_approx(polygon_line_intersection(apply(tran,path3d(star7)), + apply(tran, path3d([left(10,p=star7[8]), right(50,p=star7[8])]))), + [[apply(tran,[-22.5242216976, 10.8470934779,0])], + apply(tran,[[-5.60077322195, 10.8470934779,0], [0.997372374838, 10.8470934779,0]]), + apply(tran,[[4.61675816681, 10.8470934779,0], [11.4280421589, 10.8470934779,0]])]); } *test_polygon_line_intersection(); -module test_coplanar() { - assert(coplanar([ [5,5,1],[0,0,1],[-1,-1,1] ]) == false); - assert(coplanar([ [5,5,1],[0,0,0],[-1,-1,1] ]) == true); - assert(coplanar([ [0,0,0],[1,0,1],[1,1,1], [0,1,2] ]) == false); - assert(coplanar([ [0,0,0],[1,0,1],[1,1,2], [0,1,1] ]) == true); +module test_is_coplanar() { + assert(is_coplanar([ [5,5,1],[0,0,1],[-1,-1,1] ]) == false); + assert(is_coplanar([ [5,5,1],[0,0,0],[-1,-1,1] ]) == true); + assert(is_coplanar([ [0,0,0],[1,0,1],[1,1,1], [0,1,2] ]) == false); + assert(is_coplanar([ [0,0,0],[1,0,1],[1,1,2], [0,1,1] ]) == true); } -*test_coplanar(); +*test_is_coplanar(); -module test_above_plane() { +module test_is_above_plane() { plane = plane3pt([0,0,0], [0,10,10], [10,0,10]); - assert(above_plane(plane, [5,5,10]) == false); - assert(above_plane(plane, [-5,0,0]) == true); - assert(above_plane(plane, [5,0,0]) == false); - assert(above_plane(plane, [0,-5,0]) == true); - assert(above_plane(plane, [0,5,0]) == false); - assert(above_plane(plane, [0,0,5]) == true); - assert(above_plane(plane, [0,0,-5]) == false); + assert(is_above_plane(plane, [5,5,10]) == false); + assert(is_above_plane(plane, [-5,0,0]) == true); + assert(is_above_plane(plane, [5,0,0]) == false); + assert(is_above_plane(plane, [0,-5,0]) == true); + assert(is_above_plane(plane, [0,5,0]) == false); + assert(is_above_plane(plane, [0,0,5]) == true); + assert(is_above_plane(plane, [0,0,-5]) == false); } -*test_above_plane(); +*test_is_above_plane(); module test_is_path() { @@ -744,15 +819,6 @@ module test_polygon_shift() { *test_polygon_shift(); -module test_polygon_shift_to_closest_point() { - path = [[1,1],[-1,1],[-1,-1],[1,-1]]; - assert(polygon_shift_to_closest_point(path,[1.1,1.1]) == [[1,1],[-1,1],[-1,-1],[1,-1]]); - assert(polygon_shift_to_closest_point(path,[-1.1,1.1]) == [[-1,1],[-1,-1],[1,-1],[1,1]]); - assert(polygon_shift_to_closest_point(path,[-1.1,-1.1]) == [[-1,-1],[1,-1],[1,1],[-1,1]]); - assert(polygon_shift_to_closest_point(path,[1.1,-1.1]) == [[1,-1],[1,1],[-1,1],[-1,-1]]); -} -*test_polygon_shift_to_closest_point(); - module test_reindex_polygon() { pent = subdivide_path([for(i=[0:4])[sin(72*i),cos(72*i)]],5); diff --git a/tests/test_shapes2d.scad b/tests/test_shapes2d.scad index 4c69e25..d535e71 100644 --- a/tests/test_shapes2d.scad +++ b/tests/test_shapes2d.scad @@ -1,55 +1,6 @@ include <../std.scad> -module test_turtle() { - assert_approx( - turtle([ - "move", 10, - "ymove", 5, - "xmove", 5, - "xymove", [10,15], - "left", 135, - "untilx", 0, - "turn", 90, - "untily", 0, - "right", 135, - "arcsteps", 5, - "arcright", 15, 30, - "arcleft", 15, 30, - "arcsteps", 0, - "arcrightto", 15, 90, - "arcleftto", 15, 180, - "jump", [10,10], - "xjump", 15, - "yjump", 15, - "angle", 30, - "length", 15, - "right", - "move", - "scale", 2, - "left", - "move", - "addlength", 5, - "repeat", 3, ["move"], - ], $fn=24), - [[0,0],[10,0],[10,5],[15,5],[25,20],[-3.5527136788e-15,45],[-45,0],[-44.8716729206,1.9578928833],[-44.4888873943,3.88228567654],[-43.8581929877,5.74025148548],[-42.9903810568,7.5],[-42.1225691259,9.25974851452],[-41.4918747192,11.1177143235],[-41.1090891929,13.0421071167],[-40.9807621135,15],[-41.0157305757,16.0236362005],[-41.120472923,17.0424997364],[-41.2945007983,18.0518401958],[-41.5370028033,19.0469515674],[-41.8468482818,20.0231941826],[-42.222592591,20.9760163477],[-42.6624838375,21.900975566],[-43.1644710453,22.7937592505],[-43.7262137184,23.6502048317],[-44.345092753,24.4663191649],[-45.0182226494,25.2382971483],[-45.7424649653,25.9625394642],[-46.5144429486,26.6356693606],[-47.3305572818,27.2545483952],[-48.187002863,27.8162910682],[-49.0797865476,28.318278276],[-50.0047457658,28.7581695226],[-50.957567931,29.1339138318],[-51.9338105462,29.4437593102],[-52.9289219177,29.6862613152],[-53.9382623771,29.8602891905],[-54.9571259131,29.9650315379],[-55.9807621135,30],[10,10],[15,10],[15,15],[2.00961894323,22.5],[-27.9903810568,22.5],[-62.9903810568,22.5],[-97.9903810568,22.5],[-132.990381057,22.5]] - ); -} -test_turtle(); - - -module test_arc() { - assert_approx(arc(N=8, d=100, angle=135, cp=[10,10]), [[60,10],[57.1941665154,26.5139530978],[49.0915741234,41.1744900929],[36.6016038258,52.3362099614],[21.1260466978,58.7463956091],[4.40177619483,59.6856104947],[-11.6941869559,55.0484433951],[-25.3553390593,45.3553390593]]); - assert_approx(arc(N=8, d=100, angle=135, cp=[10,10],endpoint=false), [[60,10],[57.8470167866,24.5142338627],[51.5734806151,37.778511651],[41.7196642082,48.6505226681],[29.1341716183,56.1939766256],[14.9008570165,59.7592363336],[0.245483899194,59.0392640202],[-13.5698368413,54.0960632174]]); - assert_approx(arc(N=8, d=100, angle=[45,225], cp=[10,10]), [[45.3553390593,45.3553390593],[26.5139530978,57.1941665154],[4.40177619483,59.6856104947],[-16.6016038258,52.3362099614],[-32.3362099614,36.6016038258],[-39.6856104947,15.5982238052],[-37.1941665154,-6.51395309776],[-25.3553390593,-25.3553390593]]); - assert_approx(arc(N=8, d=100, start=45, angle=135, cp=[10,10]), [[45.3553390593,45.3553390593],[31.6941869559,55.0484433951],[15.5982238052,59.6856104947],[-1.12604669782,58.7463956091],[-16.6016038258,52.3362099614],[-29.0915741234,41.1744900929],[-37.1941665154,26.5139530978],[-40,10]]); - assert_approx(arc(N=8, d=100, start=45, angle=-90, cp=[10,10]), [[45.3553390593,45.3553390593],[52.3362099614,36.6016038258],[57.1941665154,26.5139530978],[59.6856104947,15.5982238052],[59.6856104947,4.40177619483],[57.1941665154,-6.51395309776],[52.3362099614,-16.6016038258],[45.3553390593,-25.3553390593]]); - assert_approx(arc(N=8, width=100, thickness=30), [[50,-3.5527136788e-15],[39.5300788555,13.9348601124],[25.3202618476,24.0284558904],[8.71492362453,29.3258437015],[-8.71492362453,29.3258437015],[-25.3202618476,24.0284558904],[-39.5300788555,13.9348601124],[-50,-1.42108547152e-14]]); - assert_approx(arc(N=8, cp=[10,10], points=[[45,45],[-25,45]]), [[45,45],[36.3342442379,51.9107096148],[26.3479795075,56.7198412457],[15.5419588213,59.1862449514],[4.45804117867,59.1862449514],[-6.34797950747,56.7198412457],[-16.3342442379,51.9107096148],[-25,45]]); - assert_approx(arc(N=24, cp=[10,10], points=[[45,45],[-25,45]], long=true), [[45,45],[51.3889035257,37.146982612],[56.0464336973,28.1583574081],[58.7777575294,18.4101349813],[59.4686187624,8.31010126292],[58.0901174104,-1.71924090789],[54.6999187001,-11.2583458482],[49.4398408296,-19.9081753929],[42.5299224539,-27.3068913894],[34.2592180667,-33.1449920477],[24.9737063235,-37.1782589647],[15.0618171232,-39.2379732261],[4.93818287676,-39.2379732261],[-4.97370632349,-37.1782589647],[-14.2592180667,-33.1449920477],[-22.5299224539,-27.3068913894],[-29.4398408296,-19.9081753929],[-34.6999187001,-11.2583458482],[-38.0901174104,-1.71924090789],[-39.4686187624,8.31010126292],[-38.7777575294,18.4101349813],[-36.0464336973,28.1583574081],[-31.3889035257,37.146982612],[-25,45]]); - assert_approx(arc($fn=24, cp=[10,10], points=[[45,45],[-25,45]], long=true), [[45,45],[53.2421021636,34.0856928585],[58.1827254512,21.3324740498],[59.4446596304,7.71403542491],[56.9315576496,-5.72987274525],[50.8352916125,-17.9728253654],[41.6213035891,-28.0800887515],[29.9930697126,-35.2799863457],[16.8383906815,-39.0228152281],[3.16160931847,-39.0228152281],[-9.9930697126,-35.2799863457],[-21.6213035891,-28.0800887515],[-30.8352916125,-17.9728253654],[-36.9315576496,-5.72987274525],[-39.4446596304,7.71403542491],[-38.1827254512,21.3324740498],[-33.2421021636,34.0856928585],[-25,45]]); -} -test_arc(); module test_rect() { diff --git a/tests/test_shapes.scad b/tests/test_shapes3d.scad similarity index 100% rename from tests/test_shapes.scad rename to tests/test_shapes3d.scad diff --git a/transforms.scad b/transforms.scad index c14e0ad..339388a 100644 --- a/transforms.scad +++ b/transforms.scad @@ -605,7 +605,7 @@ function zrot(a=0, p, cp) = rot(a, cp=cp, p=p); ////////////////////////////////////////////////////////////////////// -// Section: Scaling and Mirroring +// Section: Scaling ////////////////////////////////////////////////////////////////////// diff --git a/triangulation.scad b/triangulation.scad deleted file mode 100644 index 24f69cb..0000000 --- a/triangulation.scad +++ /dev/null @@ -1,194 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// LibFile: triangulation.scad -// Functions to triangulate polyhedron faces. -// Includes: -// include -// include -////////////////////////////////////////////////////////////////////// - - -// Section: Functions - - -// Function: face_normal() -// Description: -// Given an array of vertices (`points`), and a list of indexes into the -// vertex array (`face`), returns the normal vector of the face. -// Arguments: -// points = Array of vertices for the polyhedron. -// face = The face, given as a list of indices into the vertex array `points`. -function face_normal(points, face) = - let(count=len(face)) - unit( - sum( - [ - for(i=[0:1:count-1]) cross( - points[face[(i+1)%count]]-points[face[0]], - points[face[(i+2)%count]]-points[face[(i+1)%count]] - ) - ] - ) - ) -; - - -// Function: find_convex_vertex() -// Description: -// Returns the index of a convex point on the given face. -// Arguments: -// points = Array of vertices for the polyhedron. -// face = The face, given as a list of indices into the vertex array `points`. -// facenorm = The normal vector of the face. -function find_convex_vertex(points, face, facenorm, i=0) = - let(count=len(face), - p0=points[face[i]], - p1=points[face[(i+1)%count]], - p2=points[face[(i+2)%count]] - ) - (len(face)>i)? ( - (cross(p1-p0, p2-p1)*facenorm>0)? (i+1)%count : - find_convex_vertex(points, face, facenorm, i+1) - ) : //This should never happen since there is at least 1 convex vertex. - undef -; - - -// Function: point_in_ear() -// Description: Determine if a point is in a clipable convex ear. -// Arguments: -// points = Array of vertices for the polyhedron. -// face = The face, given as a list of indices into the vertex array `points`. -function point_in_ear(points, face, tests, i=0) = - (iprev[0])? [test, i] : prev - : - [_check_point_in_ear(points[face[i]], tests), i] -; - - -// Internal non-exposed function. -function _check_point_in_ear(point, tests) = - let( - result=[ - (point*tests[0][0])-tests[0][1], - (point*tests[1][0])-tests[1][1], - (point*tests[2][0])-tests[2][1] - ] - ) - (result[0]>0 && result[1]>0 && result[2]>0)? result[0] : -1 -; - - -// Function: normalize_vertex_perimeter() -// Description: Removes the last item in an array if it is the same as the first item. -// Arguments: -// v = The array to normalize. -function normalize_vertex_perimeter(v) = - let(lv = len(v)) - (lv < 2)? v : - (v[lv-1] != v[0])? v : - [for (i=[0:1:lv-2]) v[i]] -; - - -// Function: is_only_noncolinear_vertex() -// Description: -// Given a face in a polyhedron, and a vertex in that face, returns true -// if that vertex is the only non-colinear vertex in the face. -// Arguments: -// points = Array of vertices for the polyhedron. -// facelist = The face, given as a list of indices into the vertex array `points`. -// vertex = The index into `facelist`, of the vertex to test. -function is_only_noncolinear_vertex(points, facelist, vertex) = - let( - face=select(facelist, vertex+1, vertex-1), - count=len(face) - ) - 0==sum( - [ - for(i=[0:1:count-1]) norm( - cross( - points[face[(i+1)%count]]-points[face[0]], - points[face[(i+2)%count]]-points[face[(i+1)%count]] - ) - ) - ] - ) -; - - -// Function: triangulate_face() -// Description: -// Given a face in a polyhedron, subdivides the face into triangular faces. -// Returns an array of faces, where each face is a list of three vertex indices. -// Arguments: -// points = Array of vertices for the polyhedron. -// face = The face, given as a list of indices into the vertex array `points`. -function triangulate_face(points, face) = - let( - points = path3d(points), - face = deduplicate_indexed(points,face), - count = len(face) - ) - (count < 3)? [] : - (count == 3)? [face] : - let( - facenorm=face_normal(points, face), - cv=find_convex_vertex(points, face, facenorm) - ) - assert(!is_undef(cv), "Cannot triangulate self-crossing face perimeters.") - let( - pv=(count+cv-1)%count, - nv=(cv+1)%count, - p0=points[face[pv]], - p1=points[face[cv]], - p2=points[face[nv]], - tests=[ - [cross(facenorm, p0-p2), cross(facenorm, p0-p2)*p0], - [cross(facenorm, p1-p0), cross(facenorm, p1-p0)*p1], - [cross(facenorm, p2-p1), cross(facenorm, p2-p1)*p2] - ], - ear_test=point_in_ear(points, face, tests), - clipable_ear=(ear_test[0]<0), - diagonal_point=ear_test[1] - ) - (clipable_ear)? // There is no point inside the ear. - is_only_noncolinear_vertex(points, face, cv)? - // In the point&line degeneracy clip to somewhere in the middle of the line. - concat( - triangulate_face(points, select(face, cv, (cv+2)%count)), - triangulate_face(points, select(face, (cv+2)%count, cv)) - ) - : - // Otherwise the ear is safe to clip. - [ - select(face, pv, nv), - each triangulate_face(points, select(face, nv, pv)) - ] - : // If there is a point inside the ear, make a diagonal and clip along that. - concat( - triangulate_face(points, select(face, cv, diagonal_point)), - triangulate_face(points, select(face, diagonal_point, cv)) - ); - - -// Function: triangulate_faces() -// Description: -// Subdivides all faces for the given polyhedron that have more than three vertices. -// Returns an array of faces where each face is a list of three vertex array indices. -// Arguments: -// points = Array of vertices for the polyhedron. -// faces = Array of faces for the polyhedron. Each face is a list of 3 or more indices into the `points` array. -function triangulate_faces(points, faces) = - [ - for (face=faces) each - len(face)==3? [face] : - triangulate_face(points, normalize_vertex_perimeter(face)) - ]; - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/vnf.scad b/vnf.scad index 6dba96f..58afed8 100644 --- a/vnf.scad +++ b/vnf.scad @@ -1,22 +1,23 @@ ////////////////////////////////////////////////////////////////////// // LibFile: vnf.scad -// VNF structures, holding Vertices 'N' Faces for use with `polyhedron().` +// The Vertices'N'Faces structure (VNF) holds the data used by polyhedron() to construct objects: a vertex +// list and a list of faces. This library makes it easier to construct polyhedra by providing +// functions to construct, merge, and modify VNF data, while avoiding common pitfalls such as +// reversed faces. // Includes: // include ////////////////////////////////////////////////////////////////////// -include +// Creating Polyhedrons with VNF Structures - -// Section: Creating Polyhedrons with VNF Structures +// Section: VNF Testing and Access // VNF stands for "Vertices'N'Faces". VNF structures are 2-item lists, `[VERTICES,FACES]` where the // first item is a list of vertex points, and the second is a list of face indices into the vertex // list. Each VNF is self contained, with face indices referring only to its own vertex list. // You can construct a `polyhedron()` in parts by describing each part in a self-contained VNF, then // merge the various VNFs to get the completed polyhedron vertex list and faces. - EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. @@ -49,18 +50,6 @@ function vnf_vertices(vnf) = vnf[0]; function vnf_faces(vnf) = vnf[1]; -// Function: vnf_quantize() -// Usage: -// vnf2 = vnf_quantize(vnf,[q]); -// Description: -// Quantizes the vertex coordinates of the VNF to the given quanta `q`. -// Arguments: -// vnf = The VNF to quantize. -// q = The quanta to quantize the VNF coordinates to. -function vnf_quantize(vnf,q=pow(2,-12)) = - [[for (pt = vnf[0]) quant(pt,q)], vnf[1]]; - - // Function: vnf_get_vertex() // Usage: // vvnf = vnf_get_vertex(vnf, p); @@ -89,130 +78,7 @@ function vnf_get_vertex(vnf=EMPTY_VNF, p) = ]; -// Function: vnf_add_face() -// Usage: -// vnf_add_face(vnf, pts); -// Description: -// Given a VNF structure and a list of face vertex points, adds the face to the VNF structure. -// Returns the modified VNF structure `[VERTICES, FACES]`. It is up to the caller to make -// sure that the points are in the correct order to make the face normal point outwards. -// Arguments: -// vnf = The VNF structure to add a face to. -// pts = The vertex points for the face. -function vnf_add_face(vnf=EMPTY_VNF, pts) = - assert(is_vnf(vnf)) - assert(is_path(pts)) - let( - res = set_union(vnf[0], pts, get_indices=true), - face = deduplicate(res[0], closed=true) - ) [ - res[1], - concat(vnf[1], len(face)>2? [face] : []) - ]; - - -// Function: vnf_add_faces() -// Usage: -// vnf_add_faces(vnf, faces); -// Description: -// Given a VNF structure and a list of faces, where each face is given as a list of vertex points, -// adds the faces to the VNF structure. Returns the modified VNF structure `[VERTICES, FACES]`. -// It is up to the caller to make sure that the points are in the correct order to make the face -// normals point outwards. -// Arguments: -// vnf = The VNF structure to add a face to. -// faces = The list of faces, where each face is given as a list of vertex points. -function vnf_add_faces(vnf=EMPTY_VNF, faces) = - assert(is_vnf(vnf)) - assert(is_list(faces)) - let( - res = set_union(vnf[0], flatten(faces), get_indices=true), - idxs = res[0], - nverts = res[1], - offs = cumsum([0, for (face=faces) len(face)]), - ifaces = [ - for (i=idx(faces)) [ - for (j=idx(faces[i])) - idxs[offs[i]+j] - ] - ] - ) [ - nverts, - concat(vnf[1],ifaces) - ]; - - -// Function: vnf_merge() -// Usage: -// vnf = vnf_merge([VNF, VNF, VNF, ...], [cleanup],[eps]); -// Description: -// Given a list of VNF structures, merges them all into a single VNF structure. -// When cleanup=true, it consolidates all duplicate vertices with a tolerance `eps`, -// drops unreferenced vertices and any final face with less than 3 vertices. -// Unreferenced vertices of the input VNFs that doesn't duplicate any other vertex -// are not dropped. -// Arguments: -// vnfs - a list of the VNFs to merge in one VNF. -// cleanup - when true, consolidates the duplicate vertices of the merge. Default: false -// eps - the tolerance in finding duplicates when cleanup=true. Default: EPSILON -function vnf_merge(vnfs, cleanup=false, eps=EPSILON) = - is_vnf(vnfs) ? vnf_merge([vnfs], cleanup, eps) : - assert( is_vnf_list(vnfs) , "Improper vnf or vnf list")   - let ( - offs = cumsum([ 0, for (vnf = vnfs) len(vnf[0]) ]), - verts = [for (vnf=vnfs) each vnf[0]], - faces = - [ for (i = idx(vnfs)) - let( faces = vnfs[i][1] ) - for (face = faces) - if ( len(face) >= 3 ) - [ for (j = face) - assert( j>=0 && j= 3) dface - ] - ) - [nverts, nfaces]; - - -// Function: vnf_reverse_faces() -// Usage: -// rvnf = vnf_reverse_faces(vnf); -// Description: -// Reverses the facing of all the faces in the given VNF. -function vnf_reverse_faces(vnf) = - [vnf[0], [for (face=vnf[1]) reverse(face)]]; - - -// Function: vnf_triangulate() -// Usage: -// vnf2 = vnf_triangulate(vnf); -// Description: -// Forces triangulation of faces in the VNF that have more than 3 vertices. -function vnf_triangulate(vnf) = - let( - vnf = is_vnf_list(vnf)? vnf_merge(vnf) : vnf, - verts = vnf[0] - ) [verts, triangulate_faces(verts, vnf[1])]; - +// Section: Constructing VNFs // Function: vnf_vertex_array() // Usage: @@ -468,6 +334,152 @@ function vnf_tri_array(points, row_wrap=false, reverse=false, vnf=EMPTY_VNF) = vnf_merge(cleanup=true, [vnf, [flatten(points), faces]]); +// Function: vnf_add_face() +// Usage: +// vnf_add_face(vnf, pts); +// Description: +// Given a VNF structure and a list of face vertex points, adds the face to the VNF structure. +// Returns the modified VNF structure `[VERTICES, FACES]`. It is up to the caller to make +// sure that the points are in the correct order to make the face normal point outwards. +// Arguments: +// vnf = The VNF structure to add a face to. +// pts = The vertex points for the face. +function vnf_add_face(vnf=EMPTY_VNF, pts) = + assert(is_vnf(vnf)) + assert(is_path(pts)) + let( + res = set_union(vnf[0], pts, get_indices=true), + face = deduplicate(res[0], closed=true) + ) [ + res[1], + concat(vnf[1], len(face)>2? [face] : []) + ]; + + +// Function: vnf_add_faces() +// Usage: +// vnf_add_faces(vnf, faces); +// Description: +// Given a VNF structure and a list of faces, where each face is given as a list of vertex points, +// adds the faces to the VNF structure. Returns the modified VNF structure `[VERTICES, FACES]`. +// It is up to the caller to make sure that the points are in the correct order to make the face +// normals point outwards. +// Arguments: +// vnf = The VNF structure to add a face to. +// faces = The list of faces, where each face is given as a list of vertex points. +function vnf_add_faces(vnf=EMPTY_VNF, faces) = + assert(is_vnf(vnf)) + assert(is_list(faces)) + let( + res = set_union(vnf[0], flatten(faces), get_indices=true), + idxs = res[0], + nverts = res[1], + offs = cumsum([0, for (face=faces) len(face)]), + ifaces = [ + for (i=idx(faces)) [ + for (j=idx(faces[i])) + idxs[offs[i]+j] + ] + ] + ) [ + nverts, + concat(vnf[1],ifaces) + ]; + + +// Function: vnf_merge() +// Usage: +// vnf = vnf_merge([VNF, VNF, VNF, ...], [cleanup],[eps]); +// Description: +// Given a list of VNF structures, merges them all into a single VNF structure. +// When cleanup=true, it consolidates all duplicate vertices with a tolerance `eps`, +// drops unreferenced vertices and any final face with less than 3 vertices. +// Unreferenced vertices of the input VNFs that doesn't duplicate any other vertex +// are not dropped. +// Arguments: +// vnfs - a list of the VNFs to merge in one VNF. +// cleanup - when true, consolidates the duplicate vertices of the merge. Default: false +// eps - the tolerance in finding duplicates when cleanup=true. Default: EPSILON +function vnf_merge(vnfs, cleanup=false, eps=EPSILON) = + is_vnf(vnfs) ? vnf_merge([vnfs], cleanup, eps) : + assert( is_vnf_list(vnfs) , "Improper vnf or vnf list")   + let ( + offs = cumsum([ 0, for (vnf = vnfs) len(vnf[0]) ]), + verts = [for (vnf=vnfs) each vnf[0]], + faces = + [ for (i = idx(vnfs)) + let( faces = vnfs[i][1] ) + for (face = faces) + if ( len(face) >= 3 ) + [ for (j = face) + assert( j>=0 && j= 3) dface + ] + ) + [nverts, nfaces]; + + + +// Section: Altering the VNF Internals + + +// Function: vnf_reverse_faces() +// Usage: +// rvnf = vnf_reverse_faces(vnf); +// Description: +// Reverses the facing of all the faces in the given VNF. +function vnf_reverse_faces(vnf) = + [vnf[0], [for (face=vnf[1]) reverse(face)]]; + + +// Function: vnf_quantize() +// Usage: +// vnf2 = vnf_quantize(vnf,[q]); +// Description: +// Quantizes the vertex coordinates of the VNF to the given quanta `q`. +// Arguments: +// vnf = The VNF to quantize. +// q = The quanta to quantize the VNF coordinates to. +function vnf_quantize(vnf,q=pow(2,-12)) = + [[for (pt = vnf[0]) quant(pt,q)], vnf[1]]; + + +// Function: vnf_triangulate() +// Usage: +// vnf2 = vnf_triangulate(vnf); +// Description: +// Triangulates faces in the VNF that have more than 3 vertices. +function vnf_triangulate(vnf) = + let( + vnf = is_vnf_list(vnf)? vnf_merge(vnf) : vnf, + verts = vnf[0], + faces = [for (face=vnf[1]) each len(face)==3 ? [face] : + polygon_triangulate(verts, face)] + ) [verts, faces]; + + + +// Section: Turning a VNF into geometry + // Module: vnf_polyhedron() // Usage: @@ -493,7 +505,6 @@ module vnf_polyhedron(vnf, convexity=2, extent=true, cp=[0,0,0], anchor="origin" } - // Module: vnf_wireframe() // Usage: // vnf_wireframe(vnf, ); @@ -528,6 +539,8 @@ module vnf_wireframe(vnf, r, d) } +// Section: Operations on VNFs + // Function: vnf_volume() // Usage: // vol = vnf_volume(vnf); @@ -545,6 +558,16 @@ function vnf_volume(vnf) = ])/6; +// Function: vnf_area() +// Usage: +// area = vnf_area(vnf); +// Description: +// Returns the surface area in any VNF by adding up the area of all its faces. The VNF need not be a manifold. +function vnf_area(vnf) = + let(verts=vnf[0]) + sum([for(face=vnf[1]) polygon_area(select(verts,face))]); + + // Function: vnf_centroid() // Usage: // vol = vnf_centroid(vnf); @@ -573,6 +596,115 @@ function vnf_centroid(vnf) = pos[1]/pos[0]/4; +// Function: vnf_halfspace() +// Usage: +// newvnf = vnf_halfspace(plane, vnf, [closed]); +// Description: +// Returns the intersection of the vnf with a half space. The half space is defined by +// plane = [A,B,C,D], taking the side where the normal [A,B,C] points: Ax+By+Cz≥D. +// If closed is set to false then the cut face is not included in the vnf. This could +// allow further extension of the vnf by merging with other vnfs. +// Arguments: +// plane = plane defining the boundary of the half space +// vnf = vnf to cut +// closed = if false do not return include cut face(s). Default: true +// Example: +// vnf = cube(10,center=true); +// cutvnf = vnf_halfspace([-1,1,-1,0], vnf); +// vnf_polyhedron(cutvnf); +// Example: Cut face has 2 components +// vnf = path_sweep(circle(r=4, $fn=16), +// circle(r=20, $fn=64),closed=true); +// cutvnf = vnf_halfspace([-1,1,-4,0], vnf); +// vnf_polyhedron(cutvnf); +// Example: Cut face is not simply connected +// vnf = path_sweep(circle(r=4, $fn=16), +// circle(r=20, $fn=64),closed=true); +// cutvnf = vnf_halfspace([0,0.7,-4,0], vnf); +// vnf_polyhedron(cutvnf); +// Example: Cut object has multiple components +// function knot(a,b,t) = // rolling knot +// [ a * cos (3 * t) / (1 - b* sin (2 *t)), +// a * sin( 3 * t) / (1 - b* sin (2 *t)), +// 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))]; +// a = 0.8; b = sqrt (1 - a * a); +// ksteps = 400; +// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)]; +// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; +// knot=path_sweep(ushape, knot_path, closed=true, method="incremental"); +// cut_knot = vnf_halfspace([1,0,0,0], knot); +// vnf_polyhedron(cut_knot); +function vnf_halfspace(plane, vnf, closed=true) = + let( + inside = [for(x=vnf[0]) plane*[each x,-1] >= 0 ? 1 : 0], + vertexmap = [0,each cumsum(inside)], + faces_edges_vertices = _vnfcut(plane, vnf[0],vertexmap,inside, vnf[1], last(vertexmap)), + newvert = concat(bselect(vnf[0],inside), faces_edges_vertices[2]) + ) + closed==false ? [newvert, faces_edges_vertices[0]] : + let( + allpaths = _assemble_paths(newvert, faces_edges_vertices[1]), + newpaths = [for(p=allpaths) if (len(p)>=3) p + else assert(approx(p[0],p[1]),"Orphan edge found when assembling cut edges.") + ] + ) + len(newpaths)<=1 ? [newvert, concat(faces_edges_vertices[0], newpaths)] + : + let( + faceregion = project_plane(plane, newpaths), + facevnf = region_faces(faceregion,reverse=true) + ) + vnf_merge([[newvert, faces_edges_vertices[0]], lift_plane(plane, facevnf)]); + + +function _assemble_paths(vertices, edges, paths=[],i=0) = + i==len(edges) ? paths : + norm(vertices[edges[i][0]]-vertices[edges[i][1]])3 + ? _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount+1, + concat(newfaces, [list_head(newface)]), newedges,concat(newvertices,[newvert[0]]),i+1) + : + _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount,newfaces, newedges, newvert, i+1); + + + function _triangulate_planar_convex_polygons(polys) = polys==[]? [] : let( @@ -817,6 +949,8 @@ function _split_polygons_at_each_y(polys, ys, _i=0) = +// Section: Debugging VNFs + // Function&Module: vnf_validate() // Usage: As Function // fails = vnf_validate(vnf); @@ -1071,7 +1205,7 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) = faceverts = [for (k=face) varr[k]] ) if (is_num(area) && abs(area) > EPSILON) - if (!coplanar(faceverts)) + if (!is_coplanar(faceverts)) _vnf_validate_err("NONPLANAR", faceverts) ]), issues = concat(issues, nonplanars) @@ -1144,114 +1278,5 @@ module vnf_validate(vnf, size=1, show_warns=true, check_isects=false) { } -// Section: VNF Transformations - -// Function: vnf_halfspace() -// Usage: -// newvnf = vnf_halfspace(plane, vnf, [closed]); -// Description: -// Returns the intersection of the vnf with a half space. The half space is defined by -// plane = [A,B,C,D], taking the side where the normal [A,B,C] points: Ax+By+Cz≥D. -// If closed is set to false then the cut face is not included in the vnf. This could -// allow further extension of the vnf by merging with other vnfs. -// Arguments: -// plane = plane defining the boundary of the half space -// vnf = vnf to cut -// closed = if false do not return include cut face(s). Default: true -// Example: -// vnf = cube(10,center=true); -// cutvnf = vnf_halfspace([-1,1,-1,0], vnf); -// vnf_polyhedron(cutvnf); -// Example: Cut face has 2 components -// vnf = path_sweep(circle(r=4, $fn=16), -// circle(r=20, $fn=64),closed=true); -// cutvnf = vnf_halfspace([-1,1,-4,0], vnf); -// vnf_polyhedron(cutvnf); -// Example: Cut face is not simply connected -// vnf = path_sweep(circle(r=4, $fn=16), -// circle(r=20, $fn=64),closed=true); -// cutvnf = vnf_halfspace([0,0.7,-4,0], vnf); -// vnf_polyhedron(cutvnf); -// Example: Cut object has multiple components -// function knot(a,b,t) = // rolling knot -// [ a * cos (3 * t) / (1 - b* sin (2 *t)), -// a * sin( 3 * t) / (1 - b* sin (2 *t)), -// 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))]; -// a = 0.8; b = sqrt (1 - a * a); -// ksteps = 400; -// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)]; -// ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; -// knot=path_sweep(ushape, knot_path, closed=true, method="incremental"); -// cut_knot = vnf_halfspace([1,0,0,0], knot); -// vnf_polyhedron(cut_knot); -function vnf_halfspace(plane, vnf, closed=true) = - let( - inside = [for(x=vnf[0]) plane*[each x,-1] >= 0 ? 1 : 0], - vertexmap = [0,each cumsum(inside)], - faces_edges_vertices = _vnfcut(plane, vnf[0],vertexmap,inside, vnf[1], last(vertexmap)), - newvert = concat(bselect(vnf[0],inside), faces_edges_vertices[2]) - ) - closed==false ? [newvert, faces_edges_vertices[0]] : - let( - allpaths = _assemble_paths(newvert, faces_edges_vertices[1]), - newpaths = [for(p=allpaths) if (len(p)>=3) p - else assert(approx(p[0],p[1]),"Orphan edge found when assembling cut edges.") - ] - ) - len(newpaths)<=1 ? [newvert, concat(faces_edges_vertices[0], newpaths)] - : - let( - faceregion = project_plane(plane, newpaths), - facevnf = region_faces(faceregion,reverse=true) - ) - vnf_merge([[newvert, faces_edges_vertices[0]], lift_plane(plane, facevnf)]); - - -function _assemble_paths(vertices, edges, paths=[],i=0) = - i==len(edges) ? paths : - norm(vertices[edges[i][0]]-vertices[edges[i][1]])3 - ? _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount+1, - concat(newfaces, [list_head(newface)]), newedges,concat(newvertices,[newvert[0]]),i+1) - : - _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount,newfaces, newedges, newvert, i+1); - // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap