Merge pull request #649 from adrianVmariano/master

polygon line intersection & triangulation
This commit is contained in:
Revar Desmera 2021-09-15 20:32:15 -07:00 committed by GitHub
commit 4935849ae3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1934 additions and 1833 deletions

View file

@ -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()

View file

@ -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

View file

@ -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

934
drawing.scad Normal file
View file

@ -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 <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
// 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<plen) x
],
dashes = path_cut(path, cuts, closed=false),
evens = [for (i=idx(dashes)) if (i%2==0) dashes[i]]
) evens;
module dashed_stroke(path, dashpat=[3,3], width=1, closed=false) {
segs = dashed_stroke(path, dashpat=dashpat*width, closed=closed);
for (seg = segs)
stroke(seg, width=width, endcaps=false);
}
// Section: Computing paths
// Function&Module: arc()
// Usage: 2D arc from 0º to `angle` degrees.
// arc(N, r|d=, angle);
// Usage: 2D arc from START to END degrees.
// arc(N, r|d=, angle=[START,END])
// Usage: 2D arc from `start` to `start+angle` degrees.
// arc(N, r|d=, start=, angle=)
// Usage: 2D circle segment by `width` and `thickness`, starting and ending on the X axis.
// arc(N, width=, thickness=)
// Usage: Shortest 2D or 3D arc around centerpoint `cp`, starting at P0 and ending on the vector pointing from `cp` to `P1`.
// arc(N, cp=, points=[P0,P1], [long=], [cw=], [ccw=])
// Usage: 2D or 3D arc, starting at `P0`, passing through `P1` and ending at `P2`.
// arc(N, points=[P0,P1,P2])
// Topics: Paths (2D), Paths (3D), Shapes (2D), Path Generators
// Description:
// If called as a function, returns a 2D or 3D path forming an arc.
// If called as a module, creates a 2D arc polygon or pie slice shape.
// Arguments:
// N = Number of vertices to form the arc curve from.
// r = Radius of the arc.
// angle = If a scalar, specifies the end angle in degrees (relative to start parameter). If a vector of two scalars, specifies start and end angles.
// ---
// d = Diameter of the arc.
// cp = Centerpoint of arc.
// points = Points on the arc.
// long = if given with cp and points takes the long arc instead of the default short arc. Default: false
// cw = if given with cp and 2 points takes the arc in the clockwise direction. Default: false
// ccw = if given with cp and 2 points takes the arc in the counter-clockwise direction. Default: false
// width = If given with `thickness`, arc starts and ends on X axis, to make a circle segment.
// thickness = If given with `width`, arc starts and ends on X axis, to make a circle segment.
// start = Start angle of arc.
// wedge = If true, include centerpoint `cp` in output to form pie slice shape.
// endpoint = If false exclude the last point (function only). Default: true
// Examples(2D):
// arc(N=4, r=30, angle=30, wedge=true);
// arc(r=30, angle=30, wedge=true);
// arc(d=60, angle=30, wedge=true);
// arc(d=60, angle=120);
// arc(d=60, angle=120, wedge=true);
// arc(r=30, angle=[75,135], wedge=true);
// arc(r=30, start=45, angle=75, wedge=true);
// arc(width=60, thickness=20);
// arc(cp=[-10,5], points=[[20,10],[0,35]], wedge=true);
// arc(points=[[30,-5],[20,10],[-10,20]], wedge=true);
// arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
// Example(2D):
// path = arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
// stroke(closed=true, path);
// Example(FlatSpin,VPD=175):
// path = arc(points=[[0,30,0],[0,0,30],[30,0,0]]);
// trace_path(path, showpts=true, color="cyan");
function arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false, long=false, cw=false, ccw=false, endpoint=true) =
assert(is_bool(endpoint))
!endpoint ? assert(!wedge, "endpoint cannot be false if wedge is true")
list_head(arc(N+1,r,angle,d,cp,points,width,thickness,start,wedge,long,cw,ccw,true)) :
assert(is_undef(N) || (is_integer(N) && N>=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))
[];

View file

@ -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 <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
@ -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)<eps;
point_line_distance(point, line, bounded)<eps;
//Internal - distance from point `d` to the line passing through the origin with unit direction n
@ -60,9 +66,9 @@ function _point_left_of_line2d(point, line) =
cross(line[0]-point, line[1]-line[0]);
// Function: collinear()
// Function: is_collinear()
// Usage:
// test = collinear(a, [b, c], [eps]);
// test = is_collinear(a, [b, c], [eps]);
// Topics: Geometry, Points, Collinearity
// Description:
// Returns true if the points `a`, `b` and `c` are co-linear or if the list of points `a` is collinear.
@ -71,7 +77,7 @@ function _point_left_of_line2d(point, line) =
// b = Second point or undef; it should be undef if `c` is undef
// c = Third point or undef.
// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9)
function collinear(a, b, c, eps=EPSILON) =
function is_collinear(a, b, c, eps=EPSILON) =
assert( is_path([a,b,c],dim=undef)
|| ( is_undef(b) && is_undef(c) && is_path(a,dim=undef) ),
"Input should be 3 points or a list of points with same dimension.")
@ -330,7 +336,7 @@ function line_from_points(points, fast=false, eps=EPSILON) =
assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." )
let( pb = furthest_point(points[0],points) )
norm(points[pb]-points[0])<eps*max(norm(points[pb]),norm(points[0])) ? undef :
fast || collinear(points)
fast || is_collinear(points)
? [points[pb], points[0]]
: undef;
@ -339,16 +345,16 @@ function line_from_points(points, fast=false, eps=EPSILON) =
// Section: Planes
// Function: coplanar()
// Function: is_coplanar()
// Usage:
// test = coplanar(points,[eps]);
// test = is_coplanar(points,[eps]);
// Topics: Geometry, Coplanarity
// Description:
// Returns true if the given 3D points are non-collinear and are on a plane.
// Arguments:
// points = The points to test.
// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9)
function coplanar(points, eps=EPSILON) =
function is_coplanar(points, eps=EPSILON) =
assert( is_path(points,dim=3) , "Input should be a list of 3D points." )
assert( is_finite(eps) && eps>=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,<r|d>,[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,<r|d>,[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)<EPSILON ? undef : -unit(area_vec);
unit(-area_vec, error=undef);
// Function: point_in_polygon()
// Usage:
// test = point_in_polygon(point, poly, [eps])
// test = point_in_polygon(point, poly, [nonzero], [eps])
// Topics: Geometry, Polygons
// Description:
// This function tests whether the given 2D point is inside, outside or on the boundary of
@ -1356,7 +1485,7 @@ function polygon_normal(poly) =
// Arguments:
// point = The 2D point to check
// poly = The list of 2D points forming the perimeter of the polygon.
// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd" (Default: true )
// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd". Default: false (Even-Odd)
// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9)
// Example(2D): With nonzero set to true, we get this result. Green dots are inside the polygon and red are outside:
// a=20*2/3;
@ -1390,7 +1519,7 @@ function polygon_normal(poly) =
// color(point_in_polygon(p,path,nonzero=false)==1 ? "green" : "red")
// move(p)circle(r=1, $fn=12);
// }
function point_in_polygon(point, poly, nonzero=true, eps=EPSILON) =
function point_in_polygon(point, poly, nonzero=false, eps=EPSILON) =
// Original algorithms from http://geomalgorithms.com/a03-_inclusion.html
assert( is_vector(point,2) && is_path(poly,dim=2) && len(poly)>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<BOSL2/polyhedra.scad>
// 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(poly) ),
"Improper or out of bounds list of indices")
let( ind = deduplicate_indexed(poly,is_undef(ind) ? count(len(poly)) : ind) )
len(ind) == 3 ? [ind] :
len(ind) < 3 ? [] :
len(poly[ind[0]]) == 3
? // represents the polygon projection on its plane as a 2d polygon
let(
pts = select(poly,ind),
nrm = polygon_normal(pts)
)
// here, instead of an error, it might return [] or undef
assert( nrm!=undef,
"The polygon has self-intersections or its vertices are collinear or non coplanar.")
let(
imax = max_index([for(p=pts) norm(p-pts[0]) ]),
v1 = unit( pts[imax] - pts[0] ),
v2 = cross(v1,nrm),
prpts = pts*transpose([v1,v2])
)
[for(tri=_triangulate(prpts, count(len(ind)), eps)) select(ind,tri) ]
: let( cw = is_polygon_clockwise(select(poly, ind)) )
cw
? [for(tri=_triangulate( poly, reverse(ind), eps )) reverse(tri) ]
: _triangulate( poly, ind, eps );
// requires ccw 2d polygons
// returns ccw triangles
function _triangulate(poly, ind, eps=EPSILON, tris=[]) =
len(ind)==3 ? concat(tris,[ind]) :
let( ear = _get_ear(poly,ind,eps) )
assert( ear!=undef,
"The polygon has self-intersections or its vertices are collinear or non coplanar.")
let(
ear_tri = select(ind,ear,ear+2),
indr = select(ind,ear+2, ear) // indices of the remaining points
)
_triangulate(poly, indr, eps, concat(tris,[ear_tri]));
// search a valid ear from the remaining polygon
function _get_ear(poly, ind, eps, _i=0) =
_i>=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)<eps*norm(a-c)*norm(b-c);
// Function: is_polygon_clockwise()
// Usage:
// test = is_polygon_clockwise(poly);
@ -1507,6 +1754,7 @@ function reverse_polygon(poly) =
// Topics: Geometry, Polygons
// Description:
// Given a polygon `poly`, rotates the point ordering so that the first point in the polygon path is the one at index `i`.
// This is identical to `list_rotate` except that it checks for doubled endpoints and removed them if present.
// Arguments:
// poly = The list of points in the polygon path.
// i = The index of the point to shift to the front of the path.
@ -1517,24 +1765,6 @@ function polygon_shift(poly, i) =
list_rotate(cleanup_path(poly), i);
// Function: polygon_shift_to_closest_point()
// Usage:
// newpoly = polygon_shift_to_closest_point(path, pt);
// Topics: Geometry, Polygons
// Description:
// Given a polygon `poly`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`.
// Arguments:
// poly = The list of points in the polygon path.
// pt = The reference point.
function polygon_shift_to_closest_point(poly, pt) =
assert(is_vector(pt), "Invalid point." )
assert(is_path(poly,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." )
let(
poly = cleanup_path(poly),
dists = [for (p=poly) norm(p-pt)],
closest = min_index(dists)
) select(poly,closest,closest+len(poly)-1);
// Function: reindex_polygon()
// Usage:
@ -1544,7 +1774,7 @@ function polygon_shift_to_closest_point(poly, pt) =
// Rotates and possibly reverses the point order of a 2d or 3d polygon path to optimize its pairwise point
// association with a reference polygon. The two polygons must have the same number of vertices and be the same dimension.
// The optimization is done by computing the distance, norm(reference[i]-poly[i]), between
// corresponding pairs of vertices of the two polygons and choosing the polygon point order that
// corresponding pairs of vertices of the two polygons and choosing the polygon point index rotation that
// makes the total sum over all pairs as small as possible. Returns the reindexed polygon. Note
// that the geometry of the polygon is not changed by this operation, just the labeling of its
// vertices. If the input polygon is 2d and is oriented opposite the reference then its point order is

View file

@ -186,7 +186,7 @@ function hull3d_faces(points) =
remaining = [for (i = [0:1:len(points)-1]) if (i!=a && i!=b && i!=c && i!=d) i],
// Build an initial tetrahedron.
// Swap b, c if d is in front of triangle t.
ifop = above_plane(plane, points[d]),
ifop = is_above_plane(plane, points[d]),
bc = ifop? [c,b] : [b,c],
b = bc[0],
c = bc[1],
@ -245,7 +245,7 @@ function _remove_internal_edges(halfedges) = [
];
function _find_first_noncoplanar(plane, points, i=0) =
(i >= 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);

View file

@ -6,9 +6,6 @@
//////////////////////////////////////////////////////////////////////
include <triangulation.scad>
// 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);

View file

@ -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;

View file

@ -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 <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
// 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<plen) x
],
dashes = path_cut(path, cuts, closed=false),
evens = [for (i=idx(dashes)) if (i%2==0) dashes[i]]
) evens;
module dashed_stroke(path, dashpat=[3,3], width=1, closed=false) {
segs = dashed_stroke(path, dashpat=dashpat*width, closed=closed);
for (seg = segs)
stroke(seg, width=width, endcaps=false);
}
// Function&Module: arc()
// Usage: 2D arc from 0º to `angle` degrees.
// arc(N, r|d=, angle);
// Usage: 2D arc from START to END degrees.
// arc(N, r|d=, angle=[START,END])
// Usage: 2D arc from `start` to `start+angle` degrees.
// arc(N, r|d=, start=, angle=)
// Usage: 2D circle segment by `width` and `thickness`, starting and ending on the X axis.
// arc(N, width=, thickness=)
// Usage: Shortest 2D or 3D arc around centerpoint `cp`, starting at P0 and ending on the vector pointing from `cp` to `P1`.
// arc(N, cp=, points=[P0,P1], [long=], [cw=], [ccw=])
// Usage: 2D or 3D arc, starting at `P0`, passing through `P1` and ending at `P2`.
// arc(N, points=[P0,P1,P2])
// Topics: Paths (2D), Paths (3D), Shapes (2D), Path Generators
// Description:
// If called as a function, returns a 2D or 3D path forming an arc.
// If called as a module, creates a 2D arc polygon or pie slice shape.
// Arguments:
// N = Number of vertices to form the arc curve from.
// r = Radius of the arc.
// angle = If a scalar, specifies the end angle in degrees (relative to start parameter). If a vector of two scalars, specifies start and end angles.
// ---
// d = Diameter of the arc.
// cp = Centerpoint of arc.
// points = Points on the arc.
// long = if given with cp and points takes the long arc instead of the default short arc. Default: false
// cw = if given with cp and 2 points takes the arc in the clockwise direction. Default: false
// ccw = if given with cp and 2 points takes the arc in the counter-clockwise direction. Default: false
// width = If given with `thickness`, arc starts and ends on X axis, to make a circle segment.
// thickness = If given with `width`, arc starts and ends on X axis, to make a circle segment.
// start = Start angle of arc.
// wedge = If true, include centerpoint `cp` in output to form pie slice shape.
// endpoint = If false exclude the last point (function only). Default: true
// Examples(2D):
// arc(N=4, r=30, angle=30, wedge=true);
// arc(r=30, angle=30, wedge=true);
// arc(d=60, angle=30, wedge=true);
// arc(d=60, angle=120);
// arc(d=60, angle=120, wedge=true);
// arc(r=30, angle=[75,135], wedge=true);
// arc(r=30, start=45, angle=75, wedge=true);
// arc(width=60, thickness=20);
// arc(cp=[-10,5], points=[[20,10],[0,35]], wedge=true);
// arc(points=[[30,-5],[20,10],[-10,20]], wedge=true);
// arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
// Example(2D):
// path = arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
// stroke(closed=true, path);
// Example(FlatSpin,VPD=175):
// path = arc(points=[[0,30,0],[0,0,30],[30,0,0]]);
// trace_path(path, showpts=true, color="cyan");
function arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false, long=false, cw=false, ccw=false, endpoint=true) =
assert(is_bool(endpoint))
!endpoint ? assert(!wedge, "endpoint cannot be false if wedge is true")
list_head(arc(N+1,r,angle,d,cp,points,width,thickness,start,wedge,long,cw,ccw,true)) :
assert(is_undef(N) || (is_integer(N) && N>=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<n/2)
)
assert(is_def(n), "Must specify number of points, n")
assert(count==1, "Must specify exactly one of ir, id, step")
assert(stepOK, str("Parameter 'step' must be between 2 and ",floor(n/2)," for ",n," point star"))
assert(stepOK, n==4 ? "Parameter 'step' not allowed for 4 point stars"
: n==5 || n==6 ? str("Parameter 'step' must be 2 for ",n," point stars")
: str("Parameter 'step' must be between 2 and ",floor(n/2-1/2)," for ",n," point stars"))
let(
mat = !is_undef(_mat) ? _mat :
( realign? rot(-180/n, planar=true) : affine2d_identity() ) * (

View file

@ -15,8 +15,9 @@ include <distributors.scad>
include <mutators.scad>
include <attachments.scad>
include <primitives.scad>
include <shapes.scad>
include <shapes3d.scad>
include <shapes2d.scad>
include <drawing.scad>
include <masks.scad>
include <paths.scad>
include <edges.scad>

View file

@ -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);

View file

@ -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() {

View file

@ -605,7 +605,7 @@ function zrot(a=0, p, cp) = rot(a, cp=cp, p=p);
//////////////////////////////////////////////////////////////////////
// Section: Scaling and Mirroring
// Section: Scaling
//////////////////////////////////////////////////////////////////////

View file

@ -1,194 +0,0 @@
//////////////////////////////////////////////////////////////////////
// LibFile: triangulation.scad
// Functions to triangulate polyhedron faces.
// Includes:
// include <BOSL2/std.scad>
// include <BOSL2/triangulation.scad>
//////////////////////////////////////////////////////////////////////
// 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) =
(i<len(face)-1)?
let(
prev=point_in_ear(points, face, tests, i+1),
test=_check_point_in_ear(points[face[i]], tests)
)
(test>prev[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

529
vnf.scad
View file

@ -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 <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
include <triangulation.scad>
// 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<len(vnfs[i][0]),
str("VNF number ", i, " has a face indexing an nonexistent vertex") )
offs[i] + j ]
]
)
! cleanup ? [verts, faces] :
let(
dedup = vector_search(verts,eps,verts), // collect vertex duplicates
map = [for(i=idx(verts)) min(dedup[i]) ], // remap duplic vertices
offset = cumsum([for(i=idx(verts)) map[i]==i ? 0 : 1 ]), // remaping face vertex offsets
map2 = list(idx(verts))-offset, // map old vertex indices to new indices
nverts = [for(i=idx(verts)) if(map[i]==i) verts[i] ], // eliminates all unreferenced vertices
nfaces =
[ for(face=faces)
let(
nface = [ for(vi=face) map2[map[vi]] ],
dface = [for (i=idx(nface))
if( nface[i]!=nface[(i+1)%len(nface)])
nface[i] ]
)
if(len(dface) >= 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<len(vnfs[i][0]),
str("VNF number ", i, " has a face indexing an nonexistent vertex") )
offs[i] + j ]
]
)
! cleanup ? [verts, faces] :
let(
dedup = vector_search(verts,eps,verts), // collect vertex duplicates
map = [for(i=idx(verts)) min(dedup[i]) ], // remap duplic vertices
offset = cumsum([for(i=idx(verts)) map[i]==i ? 0 : 1 ]), // remaping face vertex offsets
map2 = list(idx(verts))-offset, // map old vertex indices to new indices
nverts = [for(i=idx(verts)) if(map[i]==i) verts[i] ], // eliminates all unreferenced vertices
nfaces =
[ for(face=faces)
let(
nface = [ for(vi=face) map2[map[vi]] ],
dface = [for (i=idx(nface))
if( nface[i]!=nface[(i+1)%len(nface)])
nface[i] ]
)
if(len(dface) >= 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, <r|d>);
@ -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+CzD.
// 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]])<EPSILON ? echo(degen=i)_assemble_paths(vertices,edges,paths,i+1) :
let( // Find paths that connects on left side and right side of the edges (if one exists)
left = [for(j=idx(paths)) if (approx(vertices[last(paths[j])],vertices[edges[i][0]])) j],
right = [for(j=idx(paths)) if (approx(vertices[edges[i][1]],vertices[paths[j][0]])) j]
)
assert(len(left)<=1 && len(right)<=1)
let(
keep_path = list_remove(paths,concat(left,right)),
update_path = left==[] && right==[] ? edges[i]
: left==[] ? concat([edges[i][0]],paths[right[0]])
: right==[] ? concat(paths[left[0]],[edges[i][1]])
: left != right ? concat(paths[left[0]], paths[right[0]])
: paths[left[0]]
)
_assemble_paths(vertices, edges, concat(keep_path, [update_path]), i+1);
function _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount, newfaces=[], newedges=[], newvertices=[], i=0) =
i==len(faces) ? [newfaces, newedges, newvertices] :
let(
pts_inside = select(inside,faces[i])
)
all(pts_inside) ? _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount,
concat(newfaces, [select(vertexmap,faces[i])]), newedges, newvertices, i+1):
!any(pts_inside) ? _vnfcut(plane, vertices, vertexmap,inside, faces, vertcount, newfaces, newedges, newvertices, i+1):
let(
first = search([[1,0]],pair(pts_inside,wrap=true),0)[0],
second = search([[0,1]],pair(pts_inside,wrap=true),0)[0]
)
assert(len(first)==1 && len(second)==1, "Found concave face in VNF. Run vnf_triangulate first to ensure convex faces.")
let(
newface = [each select(vertexmap,select(faces[i],second[0]+1,first[0])),vertcount, vertcount+1],
newvert = [plane_line_intersection(plane, select(vertices,select(faces[i],first[0],first[0]+1)),eps=0),
plane_line_intersection(plane, select(vertices,select(faces[i],second[0],second[0]+1)),eps=0)]
)
true //!approx(newvert[0],newvert[1])
? _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount+2,
concat(newfaces, [newface]), concat(newedges,[[vertcount+1,vertcount]]),concat(newvertices,newvert),i+1)
:len(newface)>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+CzD.
// 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]])<EPSILON ? echo(degen=i)_assemble_paths(vertices,edges,paths,i+1) :
let( // Find paths that connects on left side and right side of the edges (if one exists)
left = [for(j=idx(paths)) if (approx(vertices[last(paths[j])],vertices[edges[i][0]])) j],
right = [for(j=idx(paths)) if (approx(vertices[edges[i][1]],vertices[paths[j][0]])) j]
)
assert(len(left)<=1 && len(right)<=1)
let(
keep_path = list_remove(paths,concat(left,right)),
update_path = left==[] && right==[] ? edges[i]
: left==[] ? concat([edges[i][0]],paths[right[0]])
: right==[] ? concat(paths[left[0]],[edges[i][1]])
: left != right ? concat(paths[left[0]], paths[right[0]])
: paths[left[0]]
)
_assemble_paths(vertices, edges, concat(keep_path, [update_path]), i+1);
function _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount, newfaces=[], newedges=[], newvertices=[], i=0) =
i==len(faces) ? [newfaces, newedges, newvertices] :
let(
pts_inside = select(inside,faces[i])
)
all(pts_inside) ? _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount,
concat(newfaces, [select(vertexmap,faces[i])]), newedges, newvertices, i+1):
!any(pts_inside) ? _vnfcut(plane, vertices, vertexmap,inside, faces, vertcount, newfaces, newedges, newvertices, i+1):
let(
first = search([[1,0]],pair(pts_inside,wrap=true),0)[0],
second = search([[0,1]],pair(pts_inside,wrap=true),0)[0]
)
assert(len(first)==1 && len(second)==1, "Found concave face in VNF. Run vnf_triangulate first to ensure convex faces.")
let(
newface = [each select(vertexmap,select(faces[i],second[0]+1,first[0])),vertcount, vertcount+1],
newvert = [plane_line_intersection(plane, select(vertices,select(faces[i],first[0],first[0]+1)),eps=0),
plane_line_intersection(plane, select(vertices,select(faces[i],second[0],second[0]+1)),eps=0)]
)
true //!approx(newvert[0],newvert[1])
? _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount+2,
concat(newfaces, [newface]), concat(newedges,[[vertcount+1,vertcount]]),concat(newvertices,newvert),i+1)
:len(newface)>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