Merge remote-tracking branch 'upstream/master'

This commit is contained in:
RonaldoCMP 2021-11-04 10:56:17 +00:00
commit 10604cd20b
23 changed files with 992 additions and 568 deletions

View file

@ -6,7 +6,7 @@
//////////////////////////////////////////////////////////////////////
// Section: Comparing lists to zero
// Section: List comparison operations
// Function: approx()
// Usage:
@ -34,7 +34,7 @@ function approx(a,b,eps=EPSILON) =
// x = all_zero(x, [eps]);
// Description:
// Returns true if the finite number passed to it is approximately zero, to within `eps`.
// If passed a list, recursively checks if all items in the list are approximately zero.
// If passed a list returns true if all its entries are approximately zero.
// Otherwise, returns false.
// Arguments:
// x = The value to check.
@ -45,17 +45,16 @@ function approx(a,b,eps=EPSILON) =
// c = all_zero([0,0,0]); // Returns: true.
// d = all_zero([0,0,1e-3]); // Returns: false.
function all_zero(x, eps=EPSILON) =
is_finite(x)? approx(x,eps) :
is_list(x)? (x != [] && [for (xx=x) if(!all_zero(xx,eps=eps)) 1] == []) :
false;
is_finite(x)? abs(x)<eps :
is_vector(x) && [for (xx=x) if(abs(xx)>eps) 1] == [];
// Function: all_nonzero()
// Usage:
// test = all_nonzero(x, [eps]);
// Description:
// Returns true if the finite number passed to it is not almost zero, to within `eps`.
// If passed a list, recursively checks if all items in the list are not almost zero.
// Returns true if the finite number passed to it is different from zero by `eps`.
// If passed a list returns true if all the entries of the list are different from zero by `eps`.
// Otherwise, returns false.
// Arguments:
// x = The value to check.
@ -67,20 +66,20 @@ function all_zero(x, eps=EPSILON) =
// d = all_nonzero([0,0,1e-3]); // Returns: false.
// e = all_nonzero([1e-3,1e-3,1e-3]); // Returns: true.
function all_nonzero(x, eps=EPSILON) =
is_finite(x)? !approx(x,eps) :
is_list(x)? (x != [] && [for (xx=x) if(!all_nonzero(xx,eps=eps)) 1] == []) :
false;
is_finite(x)? abs(x)>eps :
is_vector(x) && [for (xx=x) if(abs(xx)<eps) 1] == [];
// Function: all_positive()
// Usage:
// test = all_positive(x);
// test = all_positive(x,[eps]);
// Description:
// Returns true if the finite number passed to it is greater than zero.
// If passed a list, recursively checks if all items in the list are positive.
// If passed a list returns true if all the entries are positive.
// Otherwise, returns false.
// Arguments:
// x = The value to check.
// eps = Tolerance. Default: 0
// Example:
// a = all_positive(-2); // Returns: false.
// b = all_positive(0); // Returns: false.
@ -89,21 +88,21 @@ function all_nonzero(x, eps=EPSILON) =
// e = all_positive([0,1,2]); // Returns: false.
// f = all_positive([3,1,2]); // Returns: true.
// g = all_positive([3,-1,2]); // Returns: false.
function all_positive(x) =
is_num(x)? x>0 :
is_list(x)? (x != [] && [for (xx=x) if(!all_positive(xx)) 1] == []) :
false;
function all_positive(x,eps=0) =
is_num(x)? x>eps :
is_vector(x) && [for (xx=x) if(xx<=0) 1] == [];
// Function: all_negative()
// Usage:
// test = all_negative(x);
// test = all_negative(x, [eps]);
// Description:
// Returns true if the finite number passed to it is less than zero.
// If passed a list, recursively checks if all items in the list are negative.
// Otherwise, returns false.
// Arguments:
// x = The value to check.
// eps = tolerance. Default: 0
// Example:
// a = all_negative(-2); // Returns: true.
// b = all_negative(0); // Returns: false.
@ -113,21 +112,21 @@ function all_positive(x) =
// f = all_negative([3,1,2]); // Returns: false.
// g = all_negative([3,-1,2]); // Returns: false.
// h = all_negative([-3,-1,-2]); // Returns: true.
function all_negative(x) =
is_num(x)? x<0 :
is_list(x)? (x != [] && [for (xx=x) if(!all_negative(xx)) 1] == []) :
false;
function all_negative(x, eps=0) =
is_num(x)? x<-eps :
is_vector(x) && [for (xx=x) if(xx>=-eps) 1] == [];
// Function: all_nonpositive()
// Usage:
// all_nonpositive(x);
// all_nonpositive(x, [eps]);
// Description:
// Returns true if the finite number passed to it is less than or equal to zero.
// If passed a list, recursively checks if all items in the list are nonpositive.
// Otherwise, returns false.
// Otherwise, returns false.
// Arguments:
// x = The value to check.
// eps = tolerance. Default: 0
// Example:
// a = all_nonpositive(-2); // Returns: true.
// b = all_nonpositive(0); // Returns: true.
@ -137,21 +136,21 @@ function all_negative(x) =
// f = all_nonpositive([3,1,2]); // Returns: false.
// g = all_nonpositive([3,-1,2]); // Returns: false.
// h = all_nonpositive([-3,-1,-2]); // Returns: true.
function all_nonpositive(x) =
is_num(x)? x<=0 :
is_list(x)? (x != [] && [for (xx=x) if(!all_nonpositive(xx)) 1] == []) :
false;
function all_nonpositive(x,eps=0) =
is_num(x)? x<=eps :
is_vector(x) && [for (xx=x) if(xx>eps) 1] == [];
// Function: all_nonnegative()
// Usage:
// all_nonnegative(x);
// all_nonnegative(x, [eps]);
// Description:
// Returns true if the finite number passed to it is greater than or equal to zero.
// If passed a list, recursively checks if all items in the list are nonnegative.
// Otherwise, returns false.
// Arguments:
// x = The value to check.
// eps = tolerance. Default: 0
// Example:
// a = all_nonnegative(-2); // Returns: false.
// b = all_nonnegative(0); // Returns: true.
@ -162,10 +161,9 @@ function all_nonpositive(x) =
// g = all_nonnegative([3,1,2]); // Returns: true.
// h = all_nonnegative([3,-1,2]); // Returns: false.
// i = all_nonnegative([-3,-1,-2]); // Returns: false.
function all_nonnegative(x) =
is_num(x)? x>=0 :
is_list(x)? (x != [] && [for (xx=x) if(!all_nonnegative(xx)) 1] == []) :
false;
function all_nonnegative(x,eps=0) =
is_num(x)? x>=-eps :
is_vector(x) && [for (xx=x) if(xx<-eps) 1] == [];
// Function: all_equal()
@ -280,30 +278,6 @@ function compare_lists(a, b) =
cmps==[]? (len(a)-len(b)) : cmps[0];
// Function: list_smallest()
// Usage:
// small = list_smallest(list, k)
// Description:
// Returns a set of the k smallest items in list in arbitrary order. The items must be
// mutually comparable with native OpenSCAD comparison operations. You will get "undefined operation"
// errors if you provide invalid input.
// Arguments:
// list = list to process
// k = number of items to return
function list_smallest(list, k) =
assert(is_list(list))
assert(is_finite(k) && k>=0, "k must be nonnegative")
let(
v = list[rand_int(0,len(list)-1,1)[0]],
smaller = [for(li=list) if(li<v) li ],
equal = [for(li=list) if(li==v) li ]
)
len(smaller) == k ? smaller :
len(smaller)<k && len(smaller)+len(equal) >= k ? [ each smaller, for(i=[1:k-len(smaller)]) v ] :
len(smaller) > k ? list_smallest(smaller, k) :
let( bigger = [for(li=list) if(li>v) li ] )
concat(smaller, equal, list_smallest(bigger, k-len(smaller) -len(equal)));
// Section: Dealing with duplicate list entries
@ -780,3 +754,27 @@ function group_data(groups, values) =
];
// Function: list_smallest()
// Usage:
// small = list_smallest(list, k)
// Description:
// Returns a set of the k smallest items in list in arbitrary order. The items must be
// mutually comparable with native OpenSCAD comparison operations. You will get "undefined operation"
// errors if you provide invalid input.
// Arguments:
// list = list to process
// k = number of items to return
function list_smallest(list, k) =
assert(is_list(list))
assert(is_finite(k) && k>=0, "k must be nonnegative")
let(
v = list[rand_int(0,len(list)-1,1)[0]],
smaller = [for(li=list) if(li<v) li ],
equal = [for(li=list) if(li==v) li ]
)
len(smaller) == k ? smaller :
len(smaller)<k && len(smaller)+len(equal) >= k ? [ each smaller, for(i=[1:k-len(smaller)]) v ] :
len(smaller) > k ? list_smallest(smaller, k) :
let( bigger = [for(li=list) if(li>v) li ] )
concat(smaller, equal, list_smallest(bigger, k-len(smaller) -len(equal)));

View file

@ -554,6 +554,7 @@ function dashed_stroke(path, dashpat=[3,3], closed=false) =
module dashed_stroke(path, dashpat=[3,3], width=1, closed=false) {
no_children($children);
segs = dashed_stroke(path, dashpat=dashpat*width, closed=closed);
for (seg = segs)
stroke(seg, width=width, endcaps=false);
@ -731,6 +732,7 @@ module arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false)
// stroke(helix(turns=-2.5, h=100, r=50), dots=true, dots_color="blue");
// Example(3D): Flat helix (note points are still 3d)
// stroke(helix(h=0,r1=50,r2=25,l=0, turns=4));
module helix(l,h,turns,angle, r, r1, r2, d, d1, d2) {no_module();}
function helix(l,h,turns,angle, r, r1, r2, d, d1, d2)=
let(
r1=get_radius(r=r,r1=r1,d=d,d1=d1,dflt=1),

View file

@ -310,8 +310,8 @@ function line_intersection(line1, line2, bounded1, bounded2, bounded, eps=EPSILO
// pt = line_closest_point(line, pt, [bounded]);
// Topics: Geometry, Lines, Distance
// Description:
// Returns the point on the given 2D or 3D line, segment or ray that is closest to the given point `pt`.
// The inputs `line` and `pt` args should either both be 2D or both 3D. The parameter bounded indicates
// Returns the point on the given line, segment or ray that is closest to the given point `pt`.
// The inputs `line` and `pt` args should be of the same dimension. The parameter bounded indicates
// whether the points of `line` should be treated as endpoints.
// Arguments:
// line = A list of two points that are on the unbounded line.
@ -1496,7 +1496,7 @@ function _region_centroid(region,eps=EPSILON) =
total[0]/total[1];
/// Function: _polygon_centroid()
/// Internal Function: _polygon_centroid()
/// Usage:
/// cpt = _polygon_centroid(poly);
/// Topics: Geometry, Polygons, Centroid
@ -1640,6 +1640,7 @@ function _point_above_below_segment(point, edge) =
? (edge[1].y > 0 && cross(edge[0], edge[1]-edge[0]) > 0) ? 1 : 0
: (edge[1].y <= 0 && cross(edge[0], edge[1]-edge[0]) < 0) ? -1 : 0;
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,
@ -1654,39 +1655,44 @@ function point_in_polygon(point, poly, nonzero=false, eps=EPSILON) =
:
// Does the point lie on any edges? If so return 0.
let(
on_brd = [
for (i = [0:1:len(poly)-1])
let( seg = select(poly,i,i+1) )
if (!approx(seg[0],seg[1],eps) )
_is_point_on_line(point, seg, SEGMENT, eps=eps)? 1:0
]
segs = pair(poly,true),
on_border = [for (seg=segs)
if (norm(seg[0]-seg[1])>eps && _is_point_on_line(point, seg, SEGMENT, eps=eps)) 1]
)
sum(on_brd) > 0? 0 :
nonzero
? // Compute winding number and return 1 for interior, -1 for exterior
let(
windchk = [
for(i=[0:1:len(poly)-1])
let( seg=select(poly,i,i+1) )
if (!approx(seg[0],seg[1],eps=eps))
_point_above_below_segment(point, seg)
on_border != [] ? 0 :
nonzero // Compute winding number and return 1 for interior, -1 for exterior
? let(
winding = [
for(seg=segs)
let(
p0=seg[0]-point,
p1=seg[1]-point
)
if (norm(p0-p1)>eps)
p0.y <=0
? p1.y > 0 && cross(p0,p1-p0)>0 ? 1 : 0
: p1.y <=0 && cross(p0,p1-p0)<0 ? -1: 0
]
) sum(windchk) != 0 ? 1 : -1
)
sum(winding) != 0 ? 1 : -1
: // or compute the crossings with the ray [point, point+[1,0]]
let(
n = len(poly),
cross = [
for(i=[0:n-1])
let(
p0 = poly[i]-point,
p1 = poly[(i+1)%n]-point
)
if (
( (p1.y>eps && p0.y<=eps) || (p1.y<=eps && p0.y>eps) )
&& -eps < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y)
) 1
for(seg=segs)
let(
p0 = seg[0]-point,
p1 = seg[1]-point
)
if (
( (p1.y>eps && p0.y<=eps) || (p1.y<=eps && p0.y>eps) )
&& -eps < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y)
)
1
]
) 2*(len(cross)%2)-1;
)
2*(len(cross)%2)-1;
// Function: polygon_triangulate()
@ -1912,7 +1918,7 @@ function is_polygon_clockwise(poly) =
// poly = The list of 2D path points for the perimeter of the polygon.
function clockwise_polygon(poly) =
assert(is_path(poly,dim=2), "Input should be a 2d polygon")
polygon_area(poly, signed=true)<0 ? poly : reverse_polygon(poly);
is_polygon_clockwise(poly) ? poly : reverse_polygon(poly);
// Function: ccw_polygon()
@ -1926,7 +1932,7 @@ function clockwise_polygon(poly) =
// poly = The list of 2D path points for the perimeter of the polygon.
function ccw_polygon(poly) =
assert(is_path(poly,dim=2), "Input should be a 2d polygon")
polygon_area(poly, signed=true)<0 ? reverse_polygon(poly) : poly;
is_polygon_clockwise(poly) ? reverse_polygon(poly) : poly;
// Function: reverse_polygon()

View file

@ -955,11 +955,13 @@ function enumerate(l,idx=undef) =
function pair(list, wrap=false) =
assert(is_list(list)||is_string(list), "Invalid input." )
assert(is_bool(wrap))
let(
ll = len(list)
) wrap
? [for (i=[0:1:ll-1]) [list[i], list[(i+1) % ll]]]
: [for (i=[0:1:ll-2]) [list[i], list[i+1]]];
let( L = len(list)-1)
L<1 ? [] :
[
for (i=[0:1:L-1]) [list[i], list[i+1]],
if(wrap) [list[L], list[0]]
];
// Function: triplet()
@ -970,9 +972,17 @@ function pair(list, wrap=false) =
// See Also: idx(), enumerate(), pair(), combinations(), permutations()
// Description:
// Takes a list, and returns a list of adjacent triplets from it, optionally wrapping back to the front.
// If you set `wrap` to true then the first triplet is the one centered on the first list element, so it includes
// the last element and the first two elements. If the list has fewer than three elements then the empty list is returned.
// Arguments:
// list = list to produce triplets from
// wrap = if true, wrap triplets around the list. Default: false
// Example:
// l = ["A","B","C","D","E"];
// echo([for (p=triplet(l)) str(p.z,p.y,p.x)]); // Outputs: ["CBA", "DCB", "EDC"]
// list = [0,1,2,3,4];
// a = triplet(list); // Returns [[0,1,2],[1,2,3],[2,3,4]]
// b = triplet(list,wrap=true); // Returns [[4,0,1],[0,1,2],[1,2,3],[2,3,4],[3,4,0]]
// letters = ["A","B","C","D","E"];
// [for (p=triplet(letters)) str(p.z,p.y,p.x)]; // Returns: ["CBA", "DCB", "EDC"]
// Example(2D):
// path = [for (i=[0:24]) polar_to_xy(i*2, i*360/12)];
// for (t = triplet(path)) {
@ -984,11 +994,14 @@ function pair(list, wrap=false) =
function triplet(list, wrap=false) =
assert(is_list(list)||is_string(list), "Invalid input." )
assert(is_bool(wrap))
let(
ll = len(list)
) wrap
? [for (i=[0:1:ll-1]) [ list[i], list[(i+1)%ll], list[(i+2)%ll] ]]
: [for (i=[0:1:ll-3]) [ list[i], list[i+1], list[i+2] ]];
let(L=len(list))
L<3 ? [] :
[
if(wrap) [list[L-1], list[0], list[1]],
for (i=[0:1:L-3]) [list[i],list[i+1],list[i+2]],
if(wrap) [list[L-2], list[L-1], list[0]]
];
// Function: combinations()

View file

@ -192,7 +192,7 @@ function lerp(a,b,u) =
// l = lerpn(0,1,6); // Returns: [0, 0.2, 0.4, 0.6, 0.8, 1]
// l = lerpn(0,1,5,false); // Returns: [0, 0.2, 0.4, 0.6, 0.8]
function lerpn(a,b,n,endpoint=true) =
assert(same_shape(a,b), "Bad or inconsistent inputs to lerp")
assert(same_shape(a,b), "Bad or inconsistent inputs to lerpn")
assert(is_int(n))
assert(is_bool(endpoint))
let( d = n - (endpoint? 1 : 0) )

View file

@ -475,12 +475,15 @@ module chain_hull()
// Module: path_extrude2d()
// Usage:
// path_extrude2d(path, [caps]) {...}
// path_extrude2d(path, [caps], [closed]) {...}
// Description:
// Extrudes 2D children along the given 2D path, with optional rounded endcaps.
// Extrudes 2D children along the given 2D path, with optional rounded endcaps. This module works properly in general only if the given
// children are convex and symmetric across the Y axis. It works by constructing flat sections corresponding to each segment of the path and
// inserting rounded joints at each corner.
// Arguments:
// path = The 2D path to extrude the geometry along.
// caps = If true, caps each end of the path with a `rotate_extrude()`d copy of the children. This may interact oddly when given asymmetric profile children.
// caps = If true, caps each end of the path with a `rotate_extrude()`d copy of the children. This may interact oddly when given asymmetric profile children. Default: false
// closed = If true, connect the starting point of the path to the ending point. Default: false
// Example:
// path = [
// each right(50, p=arc(d=100,angle=[90,180])),
@ -491,7 +494,7 @@ module chain_hull()
// fwd(6) square([10,5],center=true);
// }
// Example:
// path_extrude2d(arc(d=100,angle=[180,270]))
// path_extrude2d(arc(d=100,angle=[180,270]),caps=true)
// trapezoid(w1=10, w2=5, h=10, anchor=BACK);
// Example:
// include <BOSL2/beziers.scad>
@ -500,39 +503,77 @@ module chain_hull()
// ]);
// path_extrude2d(path, caps=false)
// trapezoid(w1=10, w2=1, h=5, anchor=BACK);
module path_extrude2d(path, caps=true) {
thin = 0.01;
module path_extrude2d(path, caps=false, closed=false) {
extra_ang = 0.1; // Extra angle for overlap of joints
assert(caps==false || closed==false, "Cannot have caps on a closed extrusion");
path = deduplicate(path);
for (p=pair(path)) {
delt = p[1]-p[0];
translate(p[0]) {
rot(from=BACK,to=delt) {
minkowski() {
cube([thin,norm(delt),thin], anchor=FRONT);
rotate([90,0,0]) linear_extrude(height=thin,center=true) children();
}
}
}
}
for (t=triplet(path)) {
ang = v_theta(t[2]-t[1]) - v_theta(t[1]-t[0]);
delt = t[2] - t[1];
translate(t[1]) {
minkowski() {
cube(thin,center=true);
if (ang >= 0) {
rotate(90-ang)
rot(from=LEFT,to=delt)
rotate_extrude(angle=ang+0.01)
for (p=pair(path,wrap=closed))
extrude_from_to(p[0],p[1]) xflip()rot(-90)children();
for (t=triplet(path,wrap=closed)) {
ang = -(180-vector_angle(t)) * sign(_point_left_of_line2d(t[2],[t[0],t[1]]));
delt = point3d(t[2] - t[1]);
if (ang!=0)
translate(t[1]) {
frame_map(y=delt, z=UP)
rotate(-sign(ang)*extra_ang/2)
rotate_extrude(angle=ang+sign(ang)*extra_ang)
if (ang<0)
right_half(planar=true) children();
} else {
rotate(-90)
rot(from=RIGHT,to=delt)
rotate_extrude(angle=-ang+0.01)
left_half(planar=true) children();
}
else
left_half(planar=true) children();
}
}
}
if (caps) {
move_copies([path[0],last(path)])
rotate_extrude()
right_half(planar=true) children();
}
}
module new_path_extrude2d(path, caps=false, closed=false) {
extra_ang = 0.1; // Extra angle for overlap of joints
assert(caps==false || closed==false, "Cannot have caps on a closed extrusion");
path = deduplicate(path);
for (i=[0:1:len(path)-(closed?1:2)]){
// for (i=[0:1:1]){
difference(){
extrude_from_to(path[i],select(path,i+1)) xflip()rot(-90)children();
# for(t = [select(path,i-1,i+1)]){ //, select(path,i,i+2)]){
ang = -(180-vector_angle(t)) * sign(_point_left_of_line2d(t[2],[t[0],t[1]]));
echo(ang=ang);
delt = point3d(t[2] - t[1]);
if (ang!=0)
translate(t[1]) {
frame_map(y=delt, z=UP)
rotate(-sign(ang)*extra_ang/2)
rotate_extrude(angle=ang+sign(ang)*extra_ang)
if (ang<0)
left_half(planar=true) children();
else
right_half(planar=true) children();
}
}
}
}
for (t=triplet(path,wrap=closed)) {
ang = -(180-vector_angle(t)) * sign(_point_left_of_line2d(t[2],[t[0],t[1]]));
echo(oang=ang);
delt = point3d(t[2] - t[1]);
if (ang!=0)
translate(t[1]) {
frame_map(y=delt, z=UP)
rotate(-sign(ang)*extra_ang/2)
rotate_extrude(angle=ang+sign(ang)*extra_ang)
if (ang<0)
right_half(planar=true) children();
else
left_half(planar=true) children();
}
}
if (caps) {
move_copies([path[0],last(path)])

View file

@ -1,14 +1,18 @@
//////////////////////////////////////////////////////////////////////
// LibFile: paths.scad
// Support for polygons and paths.
// A `path` is a list of points of the same dimensions, usually 2D or 3D, that can
// be connected together to form a sequence of line segments or a polygon.
// The functions in this file work on paths and also 1-regions, which are regions
// that include exactly one path. When you pass a 1-region to a function, the default
// value for `closed` is always `true` because regions represent polygons.
// Capabilities include computing length of paths, computing
// path tangents and normals, resampling of paths, and cutting paths up into smaller paths.
// Includes:
// include <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
// Section: Utility Functions
// Function: is_path()
// Usage:
// is_path(list, [dim], [fast])
@ -16,7 +20,8 @@
// Returns true if `list` is a path. A path is a list of two or more numeric vectors (AKA points).
// All vectors must of the same size, and may only contain numbers that are not inf or nan.
// By default the vectors in a path must be 2d or 3d. Set the `dim` parameter to specify a list
// of allowed dimensions, or set it to `undef` to allow any dimension.
// of allowed dimensions, or set it to `undef` to allow any dimension. (Note that this function
// returns `false` on 1-regions.)
// Example:
// bool1 = is_path([[3,4],[5,6]]); // Returns true
// bool2 = is_path([[3,4]]); // Returns false
@ -45,30 +50,29 @@ function is_path(list, dim=[2,3], fast=false) =
&& len(list[0])>0
&& (is_undef(dim) || in_list(len(list[0]), force_list(dim)));
// Function: is_path_region()
// Function: is_1region()
// Usage:
// bool = is_path_region(path, [name])
// bool = is_1region(path, [name])
// Description:
// If `path` is a region with one component then return true. If path is a region with more components
// If `path` is a region with one component (a 1-region) then return true. If path is a region with more components
// then display an error message about the parameter `name` requiring a path or a single component region. If the input
// is not a region then return false. This function helps accept singleton regions in functions that
// operate on a path.
// is not a region then return false. This function helps path functions accept 1-regions.
// Arguments:
// path = input to process
// name = name of parameter to use in error message. Default: "path"
function is_path_region(path, name="path") =
function is_1region(path, name="path") =
!is_region(path)? false
:assert(len(path)==1,str("Parameter \"",name,"\" must be a path or singleton region, but is a multicomponent region"))
true;
// Function: force_path()
// Usage:
// outpath = force_path(path, [name])
// Description:
// If `path` is a region with one component then return that component as a path. If path is a region with more components
// If `path` is a region with one component (a 1-region) then return that component as a path. If path is a region with more components
// then display an error message about the parameter `name` requiring a path or a single component region. If the input
// is not a region then return the input without any checks. This function helps accept singleton regions in functions that
// operate on a path.
// is not a region then return the input without any checks. This function helps path functions accept 1-regions.
// Arguments:
// path = input to process
// name = name of parameter to use in error message. Default: "path"
@ -142,11 +146,11 @@ function _path_select(path, s1, u1, s2, u2, closed=false) =
// Usage:
// path_merge_collinear(path, [eps])
// Arguments:
// path = A list of path points of any dimension.
// path = A path of any dimension or a 1-region
// closed = treat as closed polygon. Default: false
// eps = Largest positional variance allowed. Default: `EPSILON` (1-e9)
function path_merge_collinear(path, closed, eps=EPSILON) =
is_path_region(path) ? path_merge_collinear(path[0], default(closed,true), eps) :
is_1region(path) ? path_merge_collinear(path[0], default(closed,true), eps) :
let(closed=default(closed,false))
assert(is_bool(closed))
assert( is_path(path), "Invalid path in path_merge_collinear." )
@ -172,13 +176,13 @@ function path_merge_collinear(path, closed, eps=EPSILON) =
// Description:
// Returns the length of the path.
// Arguments:
// path = The list of points of the path to measure.
// path = Path of any dimension or 1-region.
// closed = true if the path is closed. Default: false
// Example:
// path = [[0,0], [5,35], [60,-25], [80,0]];
// echo(path_length(path));
function path_length(path,closed) =
is_path_region(path) ? path_length(path[0], default(closed,true)) :
is_1region(path) ? path_length(path[0], default(closed,true)) :
assert(is_path(path), "Invalid path in path_length")
let(closed=default(closed,false))
assert(is_bool(closed))
@ -192,10 +196,10 @@ function path_length(path,closed) =
// Description:
// Returns list of the length of each segment in a path
// Arguments:
// path = path to measure
// path = path in any dimension or 1-region
// closed = true if the path is closed. Default: false
function path_segment_lengths(path, closed) =
is_path_region(path) ? path_segment_lengths(path[0], default(closed,true)) :
is_1region(path) ? path_segment_lengths(path[0], default(closed,true)) :
let(closed=default(closed,false))
assert(is_path(path),"Invalid path in path_segment_lengths.")
assert(is_bool(closed))
@ -214,22 +218,22 @@ function path_segment_lengths(path, closed) =
// will have one extra point because of the final connecting segment that connects the last
// point of the path to the first point.
// Arguments:
// path = path to operate on
// path = path in any dimension or a 1-region
// closed = set to true if path is closed. Default: false
function path_length_fractions(path, closed) =
is_path_region(path) ? path_length_fractions(path[0], default(closed,true)):
is_1region(path) ? path_length_fractions(path[0], default(closed,true)):
let(closed=default(closed, false))
assert(is_path(path))
assert(is_bool(closed))
let(
lengths = [
0,
for (i=[0:1:len(path)-(closed?1:2)])
norm(select(path,i+1)-path[i])
each path_segment_lengths(path,closed)
],
partial_len = cumsum(lengths),
total_len = last(partial_len)
) partial_len / total_len;
)
partial_len / total_len;
@ -286,10 +290,8 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) =
isect = _general_line_intersection([a1,a2],[b1,b2],eps=eps)
)
if (isect
// && isect[1]> (i==0 && !closed? -eps: 0) // Apparently too strict
&& isect[1]>=-eps
&& isect[1]<= 1+eps
// && isect[2]> 0
&& isect[2]>= -eps
&& isect[2]<= 1+eps)
[isect[0], i, isect[1], j, isect[2]]
@ -337,7 +339,7 @@ function _sum_preserving_round(data, index=0) =
// a closed polygon the total number of points will be sum(N). Note that with an open
// path there is an extra point at the end, so the number of points will be sum(N)+1.
// Arguments:
// path = path to subdivide
// path = path in any dimension or a 1-region
// N = scalar total number of points desired or with `method="segment"` can be a vector requesting `N[i]-1` points on segment i.
// refine = number of points to add each segment.
// closed = set to false if the path is open. Default: True
@ -386,32 +388,28 @@ function subdivide_path(path, N, refine, closed=true, exact=true, method="length
assert((is_num(N) && N>0) || is_vector(N),"Parameter N to subdivide_path must be postive number or vector")
let(
count = len(path) - (closed?0:1),
add_guess = method=="segment"? (
is_list(N)? (
assert(len(N)==count,"Vector parameter N to subdivide_path has the wrong length")
add_scalar(N,-1)
) : repeat((N-len(path)) / count, count)
) : // method=="length"
assert(is_num(N),"Parameter N to subdivide path must be a number when method=\"length\"")
let(
path_lens = concat(
[ for (i = [0:1:len(path)-2]) norm(path[i+1]-path[i]) ],
closed? [norm(path[len(path)-1]-path[0])] : []
),
add_density = (N - len(path)) / sum(path_lens)
)
path_lens * add_density,
add = exact? _sum_preserving_round(add_guess) :
[for (val=add_guess) round(val)]
) concat(
[
for (i=[0:1:count]) each [
for(j=[0:1:add[i]])
lerp(path[i],select(path,i+1), j/(add[i]+1))
]
],
closed? [] : [last(path)]
);
add_guess = method=="segment"?
(
is_list(N)
? assert(len(N)==count,"Vector parameter N to subdivide_path has the wrong length")
add_scalar(N,-1)
: repeat((N-len(path)) / count, count)
)
: // method=="length"
assert(is_num(N),"Parameter N to subdivide path must be a number when method=\"length\"")
let(
path_lens = path_segment_lengths(path,closed),
add_density = (N - len(path)) / sum(path_lens)
)
path_lens * add_density,
add = exact? _sum_preserving_round(add_guess)
: [for (val=add_guess) round(val)]
)
[
for (i=[0:1:count-1])
each lerpn(path[i],select(path,i+1), 1+add[i],endpoint=false),
if (!closed) last(path)
];
@ -423,7 +421,7 @@ function subdivide_path(path, N, refine, closed=true, exact=true, method="length
// Description:
// Evenly subdivides long `path` segments until they are all shorter than `maxlen`.
// Arguments:
// path = The path to subdivide.
// path = path in any dimension or a 1-region
// maxlen = The maximum allowed path segment length.
// ---
// closed = If true, treat path like a closed polygon. Default: true
@ -459,10 +457,33 @@ function subdivide_long_segments(path, maxlen, closed=true) =
// Note that because this function operates on a discrete input path the quality of the output depends on
// the sampling of the input. If you want very accurate output, use a lot of points for the input.
// Arguments:
// path = path to resample
// path = path in any dimension or a 1-region
// N = Number of points in output
// ---
// spacing = Approximate spacing desired
// closed = set to true if path is closed. Default: true
// Example(2D): Subsampling lots of points from a smooth curve
// path = xscale(2,circle($fn=250, r=10));
// sampled = resample_path(path, 16);
// stroke(path);
// color("red")move_copies(sampled) circle($fn=16);
// Example(2D): Specified spacing is rounded to make a uniform sampling
// path = xscale(2,circle($fn=250, r=10));
// sampled = resample_path(path, spacing=17);
// stroke(path);
// color("red")move_copies(sampled) circle($fn=16);
// Example(2D): Notice that the corners are excluded
// path = square(20);
// sampled = resample_path(path, spacing=6);
// stroke(path,closed=true);
// color("red")move_copies(sampled) circle($fn=16);
// Example(2D): Closed set to false
// path = square(20);
// sampled = resample_path(path, spacing=6,closed=false);
// stroke(path);
// color("red")move_copies(sampled) circle($fn=16);
function resample_path(path, N, spacing, closed=true) =
let(path = force_path(path))
assert(is_path(path))
@ -493,11 +514,11 @@ function resample_path(path, N, spacing, closed=true) =
// still be simple.
// If closed is set to true then treat the path as a polygon.
// Arguments:
// path = path to check
// path = 2D path or 1-region
// closed = set to true to treat path as a polygon. Default: false
// eps = Epsilon error value used for determine if points coincide. Default: `EPSILON` (1e-9)
function is_path_simple(path, closed, eps=EPSILON) =
is_path_region(path) ? is_path_simple(path[0], default(closed,true), eps) :
is_1region(path) ? is_path_simple(path[0], default(closed,true), eps) :
let(closed=default(closed,false))
assert(is_path(path, 2),"Must give a 2D path")
assert(is_bool(closed))
@ -519,8 +540,8 @@ function is_path_simple(path, closed, eps=EPSILON) =
// Finds the closest path segment, and point on that segment to the given point.
// Returns `[SEGNUM, POINT]`
// Arguments:
// path = The path to find the closest point on.
// pt = the point to find the closest point to.
// path = path of any dimension or a 1-region
// pt = the point to find the closest point to
// closed =
// Example(2D):
// path = circle(d=100,$fn=6);
@ -531,11 +552,11 @@ function is_path_simple(path, closed, eps=EPSILON) =
// color("red") translate(closest[1]) circle(d=3, $fn=12);
function path_closest_point(path, pt, closed=true) =
let(path = force_path(path))
assert(is_path(path,[2,3]), "Must give 2D or 3D path.")
assert(is_path(path), "Input must be a path")
assert(is_vector(pt, len(path[0])), "Input pt must be a compatible vector")
assert(is_bool(closed))
let(
pts = [for (seg=[0:1:len(path)-(closed?1:2)]) line_closest_point(select(path,seg,seg+1),pt,SEGMENT)],
pts = [for (seg=pair(path,closed)) line_closest_point(seg,pt,SEGMENT)],
dists = [for (p=pts) norm(p-pt)],
min_seg = min_index(dists)
) [min_seg, pts[min_seg]];
@ -551,7 +572,7 @@ function path_closest_point(path, pt, closed=true) =
// assumed to be non-uniform and the derivative is computed with adjustments to produce corrected
// values.
// Arguments:
// path = path to find the tagent vectors for
// path = path of any dimension or a 1-region
// closed = set to true of the path is closed. Default: false
// uniform = set to false to correct for non-uniform sampling. Default: true
// Example(2D): A shape with non-uniform sampling gives distorted derivatives that may be undesirable. Note that derivatives tilt towards the long edges of the rectangle.
@ -569,7 +590,7 @@ function path_closest_point(path, pt, closed=true) =
// for(i=[0:len(tangents)-1])
// stroke([rect[i]-tangents[i], rect[i]+tangents[i]],width=.25, endcap2="arrow2");
function path_tangents(path, closed, uniform=true) =
is_path_region(path) ? path_tangents(path[0], default(closed,true), uniform) :
is_1region(path) ? path_tangents(path[0], default(closed,true), uniform) :
let(closed=default(closed,false))
assert(is_bool(closed))
assert(is_path(path))
@ -592,11 +613,11 @@ function path_tangents(path, closed, uniform=true) =
// For 2d paths the plane is always defined so the normal fails to exist only
// when the derivative is zero (in the case of repeated points).
// Arguments:
// path = path to compute the normals to
// path = 2D or 3D path or a 1-region
// tangents = path tangents optionally supplied
// closed = if true path is treated as a polygon. Default: false
function path_normals(path, tangents, closed) =
is_path_region(path) ? path_normals(path[0], tangents, default(closed,true)) :
is_1region(path) ? path_normals(path[0], tangents, default(closed,true)) :
let(closed=default(closed,false))
assert(is_path(path,[2,3]))
assert(is_bool(closed))
@ -623,9 +644,12 @@ function path_normals(path, tangents, closed) =
// Usage:
// curvs = path_curvature(path, [closed]);
// Description:
// Numerically estimate the curvature of the path (in any dimension).
// Numerically estimate the curvature of the path (in any dimension).
// Arguments:
// path = path in any dimension or a 1-region
// closed = if true then treat the path as a polygon. Default: false
function path_curvature(path, closed) =
is_path_region(path) ? path_curvature(path[0], default(closed,true)) :
is_1region(path) ? path_curvature(path[0], default(closed,true)) :
let(closed=default(closed,false))
assert(is_bool(closed))
assert(is_path(path))
@ -643,9 +667,12 @@ function path_curvature(path, closed) =
// Function: path_torsion()
// Usage:
// tortions = path_torsion(path, [closed]);
// torsions = path_torsion(path, [closed]);
// Description:
// Numerically estimate the torsion of a 3d path.
// Numerically estimate the torsion of a 3d path.
// Arguments:
// path = 3D path
// closed = if true then treat path as a polygon. Default: false
function path_torsion(path, closed=false) =
assert(is_path(path,3), "Input path must be a 3d path")
assert(is_bool(closed))
@ -939,19 +966,19 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) =
// Topics: Paths
// See Also: split_path_at_self_crossings()
// Usage:
// path_list = path_cut(path, cutdist, [closed=]);
// path_list = path_cut(path, cutdist, [closed=]);
// Description:
// Given a list of distances in `cutdist`, cut the path into
// subpaths at those lengths, returning a list of paths.
// If the input path is closed then the final path will include the
// original starting point. The list of cut distances must be
// in ascending order and should not include the endpoints: 0
// or len(path). If you repeat a distance you will get an
// empty list in that position in the output. If you give an
// empty cutdist array you will get the input path as output
// (without the final vertex doubled in the case of a closed path).
// Given a list of distances in `cutdist`, cut the path into
// subpaths at those lengths, returning a list of paths.
// If the input path is closed then the final path will include the
// original starting point. The list of cut distances must be
// in ascending order and should not include the endpoints: 0
// or len(path). If you repeat a distance you will get an
// empty list in that position in the output. If you give an
// empty cutdist array you will get the input path as output
// (without the final vertex doubled in the case of a closed path).
// Arguments:
// path = The original path to split.
// path = path of any dimension or a 1-region
// cutdist = Distance or list of distances where path is cut
// closed = If true, treat the path as a closed polygon. Default: false
// Example(2D,NoAxes):
@ -960,7 +987,7 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) =
// rainbow(segs) stroke($item, endcaps="butt", width=3);
function path_cut(path,cutdist,closed) =
is_num(cutdist) ? path_cut(path,[cutdist],closed) :
is_path_region(path) ? path_cut(path[0], cutdist, default(closed,true)):
is_1region(path) ? path_cut(path[0], cutdist, default(closed,true)):
let(closed=default(closed,false))
assert(is_bool(closed))
assert(is_vector(cutdist))
@ -1017,10 +1044,11 @@ function _cut_to_seg_u_form(pathcut, path, closed) =
// Usage:
// paths = split_path_at_self_crossings(path, [closed], [eps]);
// Description:
// Splits a path into sub-paths wherever the original path crosses itself.
// Splits a 2D path into sub-paths wherever the original path crosses itself.
// Splits may occur mid-segment, so new vertices will be created at the intersection points.
// Returns a list of the resulting subpaths.
// Arguments:
// path = The path to split up.
// path = A 2D path or a 1-region.
// closed = If true, treat path as a closed polygon. Default: true
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
// Example(2D,NoAxes):
@ -1081,22 +1109,22 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
// Function: polygon_parts()
// Usage:
// splitpaths = polygon_parts(path, [nonzero], [eps]);
// splitpolys = polygon_parts(poly, [nonzero], [eps]);
// Description:
// Given a possibly self-intersecting polygon, constructs a representation of the original polygon as a list of
// non-intersecting simple polygons. If nonzero is set to true then it uses the nonzero method for defining polygon membership, which
// means it will produce the outer perimeter.
// Given a possibly self-intersecting 2d polygon, constructs a representation of the original polygon as a list of
// non-intersecting simple polygons. If nonzero is set to true then it uses the nonzero method for defining polygon membership.
// For simple cases, such as the pentagram, this will produce the outer perimeter of a self-intersecting polygon.
// Arguments:
// path = The path to split up.
// poly = a 2D polygon or 1-region
// nonzero = If true use the nonzero method for checking if a point is in a polygon. Otherwise use the even-odd method. Default: false
// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9)
// Example(2D,NoAxes): This cross-crossing polygon breaks up into its 3 components (regardless of the value of nonzero).
// path = [
// poly = [
// [-100,100], [0,-50], [100,100],
// [100,-100], [0,50], [-100,-100]
// ];
// splitpaths = polygon_parts(path);
// rainbow(splitpaths) stroke($item, closed=true, width=3);
// splitpolys = polygon_parts(poly);
// rainbow(splitpolys) stroke($item, closed=true, width=3);
// Example(2D,NoAxes): With nonzero=false you get even-odd mode which matches OpenSCAD, so the pentagram breaks apart into its five points.
// pentagram = turtle(["move",100,"left",144], repeat=4);
// left(100)polygon(pentagram);
@ -1112,39 +1140,39 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
// N=12;
// ang=360/N;
// sr=10;
// path = turtle(["angle", 90+ang/2,
// poly = turtle(["angle", 90+ang/2,
// "move", sr, "left",
// "move", 2*sr*sin(ang/2), "left",
// "repeat", 4,
// ["move", 2*sr, "left",
// "move", 2*sr*sin(ang/2), "left"],
// "move", sr]);
// stroke(path, width=.3);
// right(20)rainbow(polygon_parts(path)) polygon($item);
// Example(2D,NoAxes): overlapping path segments disappear
// path = [[0,0], [10,0], [10,10], [0,10],[0,20], [20,10],[10,10], [0,10],[0,0]];
// stroke(path,width=0.3);
// right(22)stroke(polygon_parts(path)[0], width=0.3, closed=true);
// Example(2D,NoAxes): Path segments disappear outside as well
// path = turtle(["repeat", 3, ["move", 17, "left", "move", 10, "left", "move", 7, "left", "move", 10, "left"]]);
// back(2)stroke(path,width=.5);
// fwd(12)rainbow(polygon_parts(path)) stroke($item, closed=true, width=0.5);
// stroke(poly, width=.3);
// right(20)rainbow(polygon_parts(poly)) polygon($item);
// Example(2D,NoAxes): overlapping poly segments disappear
// poly = [[0,0], [10,0], [10,10], [0,10],[0,20], [20,10],[10,10], [0,10],[0,0]];
// stroke(poly,width=0.3);
// right(22)stroke(polygon_parts(poly)[0], width=0.3, closed=true);
// Example(2D,NoAxes): Poly segments disappear outside as well
// poly = turtle(["repeat", 3, ["move", 17, "left", "move", 10, "left", "move", 7, "left", "move", 10, "left"]]);
// back(2)stroke(poly,width=.5);
// fwd(12)rainbow(polygon_parts(poly)) stroke($item, closed=true, width=0.5);
// Example(2D,NoAxes): This shape has six components
// path = turtle(["repeat", 3, ["move", 15, "left", "move", 7, "left", "move", 10, "left", "move", 17, "left"]]);
// polygon(path);
// right(22)rainbow(polygon_parts(path)) polygon($item);
// poly = turtle(["repeat", 3, ["move", 15, "left", "move", 7, "left", "move", 10, "left", "move", 17, "left"]]);
// polygon(poly);
// right(22)rainbow(polygon_parts(poly)) polygon($item);
// Example(2D,NoAxes): When the loops of the shape overlap then nonzero gives a different result than the even-odd method.
// path = turtle(["repeat", 3, ["move", 15, "left", "move", 7, "left", "move", 10, "left", "move", 10, "left"]]);
// polygon(path);
// right(27)rainbow(polygon_parts(path)) polygon($item);
// move([16,-14])rainbow(polygon_parts(path,nonzero=true)) polygon($item);
function polygon_parts(path, nonzero=false, eps=EPSILON) =
let(path = force_path(path))
assert(is_path(path,2), "Must give 2D path")
// poly = turtle(["repeat", 3, ["move", 15, "left", "move", 7, "left", "move", 10, "left", "move", 10, "left"]]);
// polygon(poly);
// right(27)rainbow(polygon_parts(poly)) polygon($item);
// move([16,-14])rainbow(polygon_parts(poly,nonzero=true)) polygon($item);
function polygon_parts(poly, nonzero=false, eps=EPSILON) =
let(poly = force_path(poly))
assert(is_path(poly,2), "Must give 2D polygon")
assert(is_bool(nonzero))
let(
path = cleanup_path(path, eps=eps),
tagged = _tag_self_crossing_subpaths(path, nonzero=nonzero, closed=true, eps=eps),
poly = cleanup_path(poly, eps=eps),
tagged = _tag_self_crossing_subpaths(poly, nonzero=nonzero, closed=true, eps=eps),
kept = [for (sub = tagged) if(sub[0] == "O") sub[1]],
outregion = _assemble_path_fragments(kept, eps=eps)
) outregion;

View file

@ -3,8 +3,7 @@
// This file provides 2D boolean geometry operations on paths, where you can
// compute the intersection or union of the shape defined by point lists, producing
// a new point list. Of course, boolean operations may produce shapes with multiple
// components. To handle that, we use "regions" which are defined by sets of
// multiple paths.
// components. To handle that, we use "regions" which are defined as lists of paths.
// Includes:
// include <BOSL2/std.scad>
//////////////////////////////////////////////////////////////////////
@ -23,29 +22,100 @@
// Checking that the polygons on a list are simple and non-crossing can be a time consuming test,
// so it is not done automatically. It is your responsibility to ensure that your regions are
// compliant. You can construct regions by making a list of polygons, or by using
// boolean function operations such as union() or difference(). And if you must you
// can clean up an ill-formed region using sanitize_region().
// boolean function operations such as union() or difference(), which all except paths, as
// well as regions, as their inputs. And if you must you
// can clean up an ill-formed region using make_region().
// Function: is_region()
// Usage:
// is_region(x);
// Description:
// Returns true if the given item looks like a region. A region is defined as a list of zero or more paths.
// Returns true if the given item looks like a region. A region is a list of non-crossing simple paths. This test just checks
// that the argument is a list whose first entry is a path.
function is_region(x) = is_list(x) && is_path(x.x);
// Function: force_region()
// Function: is_valid_region()
// Usage:
// region = force_region(path)
// bool = is_valid_region(region, [eps]);
// Description:
// If the input is a path then return it as a region. Otherwise return it unaltered.
function force_region(path) = is_path(path) ? [path] : path;
// Returns true if the input is a valid region, meaning that it is a list of simple paths whose segments do not cross each other.
// This test can be time consuming with regions that contain many points.
// It differs from `is_region()` which simply checks that the object appears to be a list of paths
// because it searches all the region paths for any self-intersections or intersections with each other.
// Will also return true if given a single simple path. Use {{make_region()}} to convert sets of self-intersecting polygons into
// a region.
// Arguments:
// region = region to check
// eps = tolerance for geometric comparisons. Default: `EPSILON` = 1e-9
// Example(2D,noaxes): Nested squares form a region
// region = [for(i=[3:2:10]) square(i,center=true)];
// rainbow(region)stroke($item, width=.1,closed=true);
// back(6)text(is_valid_region(region) ? "region" : "non-region", size=2,halign="center");
// Example(2D,noaxes): Two non-intersecting squares make a valid region:
// region = [square(10), right(11,square(8))];
// rainbow(region)stroke($item, width=.1,closed=true);
// back(12)text(is_valid_region(region) ? "region" : "non-region", size=2);
// Example(2D,noaxes): Not a region due to a self-intersecting (non-simple) hourglass path
// object = [move([-2,-2],square(14)), [[0,0],[10,0],[0,10],[10,10]]];
// rainbow(object)stroke($item, width=.1,closed=true);
// move([-1.5,13])text(is_valid_region(object) ? "region" : "non-region", size=2);
// Example(2D,noaxes): Breaking hourglass in half fixes it. Now it's a region:
// region = [move([-2,-2],square(14)), [[0,0],[10,0],[5,5]], [[5,5],[0,10],[10,10]]];
// rainbow(region)stroke($item, width=.1,closed=true);
// move([1,13])text(is_valid_region(region) ? "region" : "non-region", size=2);
// Example(2D,noaxes): As with the "broken" hourglass, Touching at corners is OK. This is a region.
// region = [square(10), move([10,10], square(8))];
// rainbow(region)stroke($item, width=.1,closed=true);
// back(12)text(is_valid_region(region) ? "region" : "non-region", size=2);
// Example(2D,noaxes): The squares cross each other, so not a region
// object = [square(10), move([8,8], square(8))];
// rainbow(object)stroke($item, width=.1,closed=true);
// back(17)text(is_valid_region(object) ? "region" : "non-region", size=2);
// Example(2D,noaxes): A union is one way to fix the above example and get a region. (Note that union is run here on two simple paths, which are valid regions themselves and hence acceptable inputs to union.
// region = union([square(10), move([8,8], square(8))]);
// rainbow(region)stroke($item, width=.1,closed=true);
// back(12)text(is_valid_region(region) ? "region" : "non-region", size=2);
// Example(2D,noaxes): These two squares share part of an edge, hence not a region
// object = [square(10), move([10,2], square(7))];
// stroke(object[0], width=0.1,closed=true);
// color("red")dashed_stroke(object[1], width=0.1,closed=true);
// back(12)text(is_valid_region(object) ? "region" : "non-region", size=2);
// Example(2D,noaxes): These two squares share a full edge, hence not a region
// object = [square(10), right(10, square(10))];
// stroke(object[0], width=0.1,closed=true);
// color("red")dashed_stroke(object[1], width=0.1,closed=true);
// back(12)text(is_valid_region(object) ? "region" : "non-region", size=2);
// Example(2D,noaxes): Sharing on edge on the inside, also not a regionn
// object = [square(10), [[0,0], [2,2],[2,8],[0,10]]];
// stroke(object[0], width=0.1,closed=true);
// color("red")dashed_stroke(object[1], width=0.1,closed=true);
// back(12)text(is_valid_region(object) ? "region" : "non-region", size=2);
function is_valid_region(region, eps=EPSILON) =
let(region=force_region(region))
assert(is_region(region), "Input is not a region")
[for(p=region) if (!is_path_simple(p,closed=true,eps=eps)) 1] == []
&&
[for(i=[0:1:len(region)-2])
let( isect = _region_region_intersections([region[i]], list_tail(region,i+1), eps=eps))
each [
// check for intersection points not at the end of a segment
for(pts=flatten(isect[0])) if (pts[2]!=0 && pts[2]!=1) 1,
// check for full segment
for(seg=pair(flatten(isect[0])))
if (seg[0][0]==seg[1][0] // same path
&& seg[0][1]==seg[1][1] // same segment
&& seg[0][2]==0 && seg[1][2]==1) // both ends
1]
] ==[];
// Function: sanitize_region()
// Function: make_region()
// Usage:
// r_fixed = sanitize_region(r, [nonzero], [eps]);
// r_fixed = make_region(r, [nonzero], [eps]);
// Description:
// Takes a malformed input region that contains self-intersecting polygons or polygons
// that cross each other and converts it into a properly defined region without
@ -56,7 +126,7 @@ function force_region(path) = is_path(path) ? [path] : path;
// eps = Epsilon for geometric comparisons. Default: `EPSILON` (1e-9)
// Examples:
//
function sanitize_region(r,nonzero=false,eps=EPSILON) =
function make_region(r,nonzero=false,eps=EPSILON) =
let(r=force_region(r))
assert(is_region(r), "Input is not a region")
exclusive_or(
@ -64,6 +134,17 @@ function sanitize_region(r,nonzero=false,eps=EPSILON) =
eps=eps);
// Function: force_region()
// Usage:
// region = force_region(path)
// Description:
// If the input is a path then return it as a region. Otherwise return it unaltered.
function force_region(path) = is_path(path) ? [path] : path;
// Section: Turning a region into geometry
// Module: region()
// Usage:
// region(r);
@ -93,6 +174,8 @@ module region(r)
// Section: Gometrical calculations with region
// Function: point_in_region()
// Usage:
// check = point_in_region(point, region, [eps]);
@ -128,23 +211,6 @@ function region_area(region) =
-sum([for(R=parts, poly=R) polygon_area(poly,signed=true)]);
// Function: is_region_simple()
// Usage:
// bool = is_region_simple(region, [eps]);
// Description:
// Returns true if the region is entirely non-self-intersecting, meaning that it is
// formed from a list of simple polygons that do not intersect each other.
// Arguments:
// region = region to check
// eps = tolerance for geometric comparisons. Default: `EPSILON` = 1e-9
function is_region_simple(region, eps=EPSILON) =
let(region=force_region(region))
assert(is_region(region), "Input is not a region")
[for(p=region) if (!is_path_simple(p,closed=true,eps)) 1] == []
&&
[for(i=[0:1:len(region)-2])
if (_region_region_intersections([region[i]], list_tail(region,i+1), eps=eps)[0][0] != []) 1
] ==[];
function _clockwise_region(r) = [for(p=r) clockwise_polygon(p)];
@ -182,7 +248,7 @@ function __are_regions_equal(region1, region2, i) =
/// Returns a pair of sorted lists such that risect[0] is a list of intersection
/// points for every path in region1, and similarly risect[1] is a list of intersection
/// points for the paths in region2. For each path the intersection list is
/// a sorted list of the form [SEGMENT, U]. You can specify that the paths in either
/// a sorted list of the form [PATHIND, SEGMENT, U]. You can specify that the paths in either
/// region be regarded as open paths if desired. Default is to treat them as
/// regions and hence the paths as closed polygons.
/// .
@ -252,6 +318,9 @@ function _region_region_intersections(region1, region2, closed1=true,closed2=tru
[for(i=[0:1]) [for(j=counts[i]) _sort_vectors(select(risect[i],pathind[i][j]))]];
// Section: Breaking up regions into subregions
// Function: split_region_at_region_crossings()
// Usage:
// split_region = split_region_at_region_crossings(region1, region2, [closed1], [closed2], [eps])

View file

@ -602,7 +602,7 @@ module octagon(r, d, or, od, ir, id, side, rounding=0, realign=false, align_tip,
regular_ngon(n=8, r=r, d=d, or=or, od=od, ir=ir, id=id, side=side, rounding=rounding, realign=realign, align_tip=align_tip, align_side=align_side, anchor=anchor, spin=spin) children();
// Function&Module right_triangle()
// Function&Module: right_triangle()
// Usage: As Module
// right_triangle(size, [center], ...);
// Usage: With Attachments

View file

@ -1,37 +1,6 @@
include <../std.scad>
module test_ident() {
assert(ident(3) == [[1,0,0],[0,1,0],[0,0,1]]);
assert(ident(4) == [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]);
}
test_ident();
module test_is_2d_transform() {
assert(!is_2d_transform(affine2d_identity()));
assert(!is_2d_transform(affine2d_translate([5,8])));
assert(!is_2d_transform(affine2d_scale([3,4])));
assert(!is_2d_transform(affine2d_zrot(30)));
assert(!is_2d_transform(affine2d_mirror([-1,1])));
assert(!is_2d_transform(affine2d_skew(30,15)));
assert(is_2d_transform(affine3d_identity()));
assert(is_2d_transform(affine3d_translate([30,40,0])));
assert(!is_2d_transform(affine3d_translate([30,40,50])));
assert(is_2d_transform(affine3d_scale([3,4,1])));
assert(!is_2d_transform(affine3d_xrot(30)));
assert(!is_2d_transform(affine3d_yrot(30)));
assert(is_2d_transform(affine3d_zrot(30)));
assert(is_2d_transform(affine3d_skew(sxy=2)));
assert(is_2d_transform(affine3d_skew(syx=2)));
assert(!is_2d_transform(affine3d_skew(szx=2)));
assert(!is_2d_transform(affine3d_skew(szy=2)));
}
test_is_2d_transform();
// 2D
module test_affine2d_identity() {
@ -197,64 +166,5 @@ test_affine3d_skew_yz();
////////////////////////////
module test_apply() {
assert(approx(apply(affine3d_xrot(90),2*UP),2*FRONT));
assert(approx(apply(affine3d_yrot(90),2*UP),2*RIGHT));
assert(approx(apply(affine3d_zrot(90),2*UP),2*UP));
assert(approx(apply(affine3d_zrot(90),2*RIGHT),2*BACK));
assert(approx(apply(affine3d_zrot(90),2*BACK+2*RIGHT),2*BACK+2*LEFT));
assert(approx(apply(affine3d_xrot(135),2*BACK+2*UP),2*sqrt(2)*FWD));
assert(approx(apply(affine3d_yrot(135),2*RIGHT+2*UP),2*sqrt(2)*DOWN));
assert(approx(apply(affine3d_zrot(45),2*BACK+2*RIGHT),2*sqrt(2)*BACK));
module check_path_apply(mat,path)
assert_approx(apply(mat,path),path3d([for (p=path) mat*concat(p,1)]));
check_path_apply(xrot(45), path3d(rect(100,center=true)));
check_path_apply(yrot(45), path3d(rect(100,center=true)));
check_path_apply(zrot(45), path3d(rect(100,center=true)));
check_path_apply(rot([20,30,40])*scale([0.9,1.1,1])*move([10,20,30]), path3d(rect(100,center=true)));
module check_patch_apply(mat,patch)
assert_approx(apply(mat,patch), [for (path=patch) path3d([for (p=path) mat*concat(p,1)])]);
flat = [for (x=[-50:25:50]) [for (y=[-50:25:50]) [x,y,0]]];
check_patch_apply(xrot(45), flat);
check_patch_apply(yrot(45), flat);
check_patch_apply(zrot(45), flat);
check_patch_apply(rot([20,30,40])*scale([0.9,1.1,1])*move([10,20,30]), flat);
}
test_apply();
module test_rot_decode() {
Tlist = [
rot(37),
xrot(49),
yrot(88),
rot(37,v=[1,3,3]),
rot(41,v=[2,-3,4]),
rot(180),
xrot(180),
yrot(180),
rot(180, v=[3,2,-5], cp=[3,5,18]),
rot(0.1, v=[1,2,3]),
rot(-47,v=[3,4,5],cp=[9,3,4]),
rot(197,v=[13,4,5],cp=[9,-3,4]),
move([3,4,5]),
move([3,4,5]) * rot(a=56, v=[5,3,-3], cp=[2,3,4]),
ident(4)
];
errlist = [for(T = Tlist)
let(
parm = rot_decode(T),
restore = move(parm[3])*rot(a=parm[0],v=parm[1],cp=parm[2])
)
norm_fro(restore-T)];
assert(max(errlist)<1e-13);
}
test_rot_decode();
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

View file

@ -145,7 +145,7 @@ test_deduplicate_indexed();
module test_all_zero() {
assert(all_zero(0));
assert(all_zero([0,0,0]));
assert(all_zero([[0,0,0],[0,0]]));
assert(!all_zero([[0,0,0],[0,0]]));
assert(all_zero([EPSILON/2,EPSILON/2,EPSILON/2]));
assert(!all_zero(1e-3));
assert(!all_zero([0,0,1e-3]));
@ -215,7 +215,7 @@ module test_all_negative() {
assert(!all_negative([3,-1,2]));
assert(all_negative([-3,-1,-2]));
assert(!all_negative([-3,1,-2]));
assert(all_negative([[-5,-7],[-3,-1,-2]]));
assert(!all_negative([[-5,-7],[-3,-1,-2]]));
assert(!all_negative([[-5,-7],[-3,1,-2]]));
assert(!all_negative([]));
assert(!all_negative(true));
@ -256,7 +256,7 @@ module test_all_nonnegative() {
assert(!all_nonnegative([[-5,-7],[-3,-1,-2]]));
assert(!all_nonnegative([[-5,-7],[-3,1,-2]]));
assert(!all_nonnegative([[5,7],[3,-1,2]]));
assert(all_nonnegative([[5,7],[3,1,2]]));
assert(!all_nonnegative([[5,7],[3,1,2]]));
assert(!all_nonnegative([]));
assert(!all_nonnegative(true));
assert(!all_nonnegative(false));
@ -280,3 +280,114 @@ module test_approx() {
assert_equal(approx([1,[1,undef]], [1+1e-12,[1,undef]]), true);
}
test_approx();
module test_group_data() {
assert_equal(group_data([1,2,0], ["A","B","C"]), [["C"],["A"],["B"]]);
assert_equal(group_data([1,3,0], ["A","B","C"]), [["C"],["A"],[],["B"]]);
assert_equal(group_data([5,3,1], ["A","B","C"]), [[],["C"],[],["B"],[],["A"]]);
assert_equal(group_data([1,3,1], ["A","B","C"]), [[],["A","C"],[],["B"]]);
}
test_group_data();
module test_compare_vals() {
assert(compare_vals(-10,0) < 0);
assert(compare_vals(10,0) > 0);
assert(compare_vals(10,10) == 0);
assert(compare_vals("abc","abcd") < 0);
assert(compare_vals("abcd","abc") > 0);
assert(compare_vals("abcd","abcd") == 0);
assert(compare_vals(false,false) == 0);
assert(compare_vals(true,false) > 0);
assert(compare_vals(false,true) < 0);
assert(compare_vals(true,true) == 0);
assert(compare_vals([2,3,4], [2,3,4,5]) < 0);
assert(compare_vals([2,3,4,5], [2,3,4,5]) == 0);
assert(compare_vals([2,3,4,5], [2,3,4]) > 0);
assert(compare_vals([2,3,4,5], [2,3,5,5]) < 0);
assert(compare_vals([[2,3,4,5]], [[2,3,5,5]]) < 0);
assert(compare_vals([[2,3,4],[3,4,5]], [[2,3,4], [3,4,5]]) == 0);
assert(compare_vals([[2,3,4],[3,4,5]], [[2,3,4,5], [3,4,5]]) < 0);
assert(compare_vals([[2,3,4],[3,4,5]], [[2,3,4], [3,4,5,6]]) < 0);
assert(compare_vals([[2,3,4,5],[3,4,5]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_vals([[2,3,4],[3,4,5,6]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_vals([[2,3,4],[3,5,5]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_vals([[2,3,4],[3,4,5]], [[2,3,4], [3,5,5]]) < 0);
assert(compare_vals(undef, undef) == 0);
assert(compare_vals(undef, true) < 0);
assert(compare_vals(undef, 0) < 0);
assert(compare_vals(undef, "foo") < 0);
assert(compare_vals(undef, [2,3,4]) < 0);
assert(compare_vals(undef, [0:3]) < 0);
assert(compare_vals(true, undef) > 0);
assert(compare_vals(true, true) == 0);
assert(compare_vals(true, 0) < 0);
assert(compare_vals(true, "foo") < 0);
assert(compare_vals(true, [2,3,4]) < 0);
assert(compare_vals(true, [0:3]) < 0);
assert(compare_vals(0, undef) > 0);
assert(compare_vals(0, true) > 0);
assert(compare_vals(0, 0) == 0);
assert(compare_vals(0, "foo") < 0);
assert(compare_vals(0, [2,3,4]) < 0);
assert(compare_vals(0, [0:3]) < 0);
assert(compare_vals(1, undef) > 0);
assert(compare_vals(1, true) > 0);
assert(compare_vals(1, 1) == 0);
assert(compare_vals(1, "foo") < 0);
assert(compare_vals(1, [2,3,4]) < 0);
assert(compare_vals(1, [0:3]) < 0);
assert(compare_vals("foo", undef) > 0);
assert(compare_vals("foo", true) > 0);
assert(compare_vals("foo", 1) > 0);
assert(compare_vals("foo", "foo") == 0);
assert(compare_vals("foo", [2,3,4]) < 0);
assert(compare_vals("foo", [0:3]) < 0);
assert(compare_vals([2,3,4], undef) > 0);
assert(compare_vals([2,3,4], true) > 0);
assert(compare_vals([2,3,4], 1) > 0);
assert(compare_vals([2,3,4], "foo") > 0);
assert(compare_vals([2,3,4], [2,3,4]) == 0);
assert(compare_vals([2,3,4], [0:3]) < 0);
assert(compare_vals([0:3], undef) > 0);
assert(compare_vals([0:3], true) > 0);
assert(compare_vals([0:3], 1) > 0);
assert(compare_vals([0:3], "foo") > 0);
assert(compare_vals([0:3], [2,3,4]) > 0);
assert(compare_vals([0:3], [0:3]) == 0);
}
test_compare_vals();
module test_compare_lists() {
assert(compare_lists([2,3,4], [2,3,4,5]) < 0);
assert(compare_lists([2,3,4,5], [2,3,4,5]) == 0);
assert(compare_lists([2,3,4,5], [2,3,4]) > 0);
assert(compare_lists([2,3,4,5], [2,3,5,5]) < 0);
assert(compare_lists([[2,3,4],[3,4,5]], [[2,3,4], [3,4,5]]) == 0);
assert(compare_lists([[2,3,4],[3,4,5]], [[2,3,4,5], [3,4,5]]) < 0);
assert(compare_lists([[2,3,4],[3,4,5]], [[2,3,4], [3,4,5,6]]) < 0);
assert(compare_lists([[2,3,4,5],[3,4,5]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_lists([[2,3,4],[3,4,5,6]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_lists([[2,3,4],[3,5,5]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_lists([[2,3,4],[3,4,5]], [[2,3,4], [3,5,5]]) < 0);
assert(compare_lists("cat", "bat") > 0);
assert(compare_lists(["cat"], ["bat"]) > 0);
}
test_compare_lists();

View file

@ -49,3 +49,13 @@ module test_arc() {
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_dashed_stroke() {
segs = dashed_stroke([[0,0],[10,0]], dashpat=[3,2], closed=false);
assert_equal(segs,[[[0,0],[3,0]], [[5,0],[8,0]]]);
}
test_dashed_stroke();

View file

@ -55,6 +55,7 @@ test_ccw_polygon();
test_reverse_polygon();
test_polygon_normal();
test_rot_decode();
//tests to migrate to other files
test_convex_distance();
@ -966,4 +967,37 @@ module test_convex_collision() {
}
*test_convex_distance();
module test_rot_decode() {
Tlist = [
rot(37),
xrot(49),
yrot(88),
rot(37,v=[1,3,3]),
rot(41,v=[2,-3,4]),
rot(180),
xrot(180),
yrot(180),
rot(180, v=[3,2,-5], cp=[3,5,18]),
rot(0.1, v=[1,2,3]),
rot(-47,v=[3,4,5],cp=[9,3,4]),
rot(197,v=[13,4,5],cp=[9,-3,4]),
move([3,4,5]),
move([3,4,5]) * rot(a=56, v=[5,3,-3], cp=[2,3,4]),
ident(4)
];
errlist = [for(T = Tlist)
let(
parm = rot_decode(T),
restore = move(parm[3])*rot(a=parm[0],v=parm[1],cp=parm[2])
)
norm_fro(restore-T)];
assert(max(errlist)<1e-13);
}
*test_rot_decode();
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

View file

@ -1,5 +1,51 @@
include <../std.scad>
module test_is_matrix() {
assert(is_matrix([[2,3,4],[5,6,7],[8,9,10]]));
assert(is_matrix([[2,3],[5,6],[8,9]],3,2));
assert(is_matrix([[2,3],[5,6],[8,9]],m=3,n=2));
assert(is_matrix([[2,3,4],[5,6,7]],m=2,n=3));
assert(is_matrix([[2,3,4],[5,6,7]],2,3));
assert(is_matrix([[2,3,4],[5,6,7]],m=2));
assert(is_matrix([[2,3,4],[5,6,7]],2));
assert(is_matrix([[2,3,4],[5,6,7]],n=3));
assert(!is_matrix([[2,3,4],[5,6,7]],m=4));
assert(!is_matrix([[2,3,4],[5,6,7]],n=5));
assert(!is_matrix([[2,3],[5,6],[8,9]],m=2,n=3));
assert(!is_matrix([[2,3,4],[5,6,7]],m=3,n=2));
assert(!is_matrix([ [2,[3,4]],
[4,[5,6]]]));
assert(!is_matrix([[3,4],[undef,3]]));
assert(!is_matrix([[3,4],[3,"foo"]]));
assert(!is_matrix([[3,4],[3,3,2]]));
assert(!is_matrix([ [3,4],6]));
assert(!is_matrix(undef));
assert(!is_matrix(NAN));
assert(!is_matrix(INF));
assert(!is_matrix(-5));
assert(!is_matrix(0));
assert(!is_matrix(5));
assert(!is_matrix(""));
assert(!is_matrix("foo"));
assert(!is_matrix([3,4,5]));
assert(!is_matrix([]));
}
test_is_matrix();
module test_ident() {
assert(ident(3) == [[1,0,0],[0,1,0],[0,0,1]]);
assert(ident(4) == [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]);
}
test_ident();
module test_qr_factor() {
// Check that R is upper triangular
function is_ut(R) =
@ -136,7 +182,7 @@ module test_null_space(){
function nullcheck(A,dim) =
let(v=null_space(A))
len(v)==dim && all_zero(A*transpose(v),eps=1e-12);
len(v)==dim && all_zero(flatten(A*transpose(v)),eps=1e-12);
A = [[-1, 2, -5, 2],[-3,-1,3,-3],[5,0,5,0],[3,-4,11,-4]];
assert(nullcheck(A,1));

View file

@ -351,6 +351,12 @@ module test_pair() {
assert(pair("ABCD",true) == [["A","B"], ["B","C"], ["C","D"], ["D","A"]]);
assert(pair([3,4,5,6],wrap=true) == [[3,4], [4,5], [5,6], [6,3]]);
assert(pair("ABCD",wrap=true) == [["A","B"], ["B","C"], ["C","D"], ["D","A"]]);
assert_equal(pair([],wrap=true),[]);
assert_equal(pair([],wrap=false),[]);
assert_equal(pair([1],wrap=true),[]);
assert_equal(pair([1],wrap=false),[]);
assert_equal(pair([1,2],wrap=false),[[1,2]]);
assert_equal(pair([1,2],wrap=true),[[1,2],[2,1]]);
}
test_pair();
@ -358,10 +364,17 @@ test_pair();
module test_triplet() {
assert(triplet([3,4,5,6,7]) == [[3,4,5], [4,5,6], [5,6,7]]);
assert(triplet("ABCDE") == [["A","B","C"], ["B","C","D"], ["C","D","E"]]);
assert(triplet([3,4,5,6],true) == [[3,4,5], [4,5,6], [5,6,3], [6,3,4]]);
assert(triplet("ABCD",true) == [["A","B","C"], ["B","C","D"], ["C","D","A"], ["D","A","B"]]);
assert(triplet([3,4,5,6],wrap=true) == [[3,4,5], [4,5,6], [5,6,3], [6,3,4]]);
assert(triplet("ABCD",wrap=true) == [["A","B","C"], ["B","C","D"], ["C","D","A"], ["D","A","B"]]);
assert(triplet([3,4,5,6],true) == [[6,3,4],[3,4,5], [4,5,6], [5,6,3]]);
assert(triplet("ABCD",true) == [["D","A","B"],["A","B","C"], ["B","C","D"], ["C","D","A"]]);
assert(triplet("ABCD",wrap=true) == [["D","A","B"],["A","B","C"], ["B","C","D"], ["C","D","A"]]);
assert_equal(triplet([],wrap=true),[]);
assert_equal(triplet([],wrap=false),[]);
assert_equal(triplet([1],wrap=true),[]);
assert_equal(triplet([1],wrap=false),[]);
assert_equal(triplet([1,2],wrap=true),[]);
assert_equal(triplet([1,2],wrap=false),[]);
assert_equal(triplet([1,2,3],wrap=true),[[3,1,2],[1,2,3],[2,3,1]]);
assert_equal(triplet([1,2,3],wrap=false),[[1,2,3]]);
}
test_triplet();
@ -401,15 +414,6 @@ module test_list_to_matrix() {
test_list_to_matrix();
module test_group_data() {
assert_equal(group_data([1,2,0], ["A","B","C"]), [["C"],["A"],["B"]]);
assert_equal(group_data([1,3,0], ["A","B","C"]), [["C"],["A"],[],["B"]]);
assert_equal(group_data([5,3,1], ["A","B","C"]), [[],["C"],[],["B"],[],["A"]]);
assert_equal(group_data([1,3,1], ["A","B","C"]), [[],["A","C"],[],["B"]]);
}
test_group_data();
module test_flatten() {
assert(flatten([[1,2,3], [4,5,[6,7,8]]]) == [1,2,3,4,5,[6,7,8]]);
assert(flatten([]) == []);

View file

@ -76,40 +76,6 @@ module test_constrain() {
test_constrain();
module test_is_matrix() {
assert(is_matrix([[2,3,4],[5,6,7],[8,9,10]]));
assert(is_matrix([[2,3],[5,6],[8,9]],3,2));
assert(is_matrix([[2,3],[5,6],[8,9]],m=3,n=2));
assert(is_matrix([[2,3,4],[5,6,7]],m=2,n=3));
assert(is_matrix([[2,3,4],[5,6,7]],2,3));
assert(is_matrix([[2,3,4],[5,6,7]],m=2));
assert(is_matrix([[2,3,4],[5,6,7]],2));
assert(is_matrix([[2,3,4],[5,6,7]],n=3));
assert(!is_matrix([[2,3,4],[5,6,7]],m=4));
assert(!is_matrix([[2,3,4],[5,6,7]],n=5));
assert(!is_matrix([[2,3],[5,6],[8,9]],m=2,n=3));
assert(!is_matrix([[2,3,4],[5,6,7]],m=3,n=2));
assert(!is_matrix([ [2,[3,4]],
[4,[5,6]]]));
assert(!is_matrix([[3,4],[undef,3]]));
assert(!is_matrix([[3,4],[3,"foo"]]));
assert(!is_matrix([[3,4],[3,3,2]]));
assert(!is_matrix([ [3,4],6]));
assert(!is_matrix(undef));
assert(!is_matrix(NAN));
assert(!is_matrix(INF));
assert(!is_matrix(-5));
assert(!is_matrix(0));
assert(!is_matrix(5));
assert(!is_matrix(""));
assert(!is_matrix("foo"));
assert(!is_matrix([3,4,5]));
assert(!is_matrix([]));
}
test_is_matrix();
module test_all_integer() {
assert(!all_integer(undef));
@ -133,37 +99,6 @@ test_all_integer();
module test_min_index() {
vals = rands(-100,100,100,seed=75);
minval = min(vals);
minidx = min_index(vals);
assert_equal(vals[minidx], minval);
assert_equal(min_index([3,4,5,6]), 0);
assert_equal(min_index([4,3,5,6]), 1);
assert_equal(min_index([4,5,3,6]), 2);
assert_equal(min_index([4,5,6,3]), 3);
assert_equal(min_index([6,5,4,3]), 3);
assert_equal(min_index([6,3,4,5]), 1);
assert_equal(min_index([-56,72,-874,5]), 2);
}
test_min_index();
module test_max_index() {
vals = rands(-100,100,100,seed=97);
maxval = max(vals);
maxidx = max_index(vals);
assert_equal(vals[maxidx], maxval);
assert_equal(max_index([3,4,5,6]), 3);
assert_equal(max_index([3,4,6,5]), 2);
assert_equal(max_index([3,6,4,5]), 1);
assert_equal(max_index([6,3,4,5]), 0);
assert_equal(max_index([5,6,4,3]), 1);
assert_equal(max_index([-56,72,-874,5]), 1);
}
test_max_index();
module test_posmod() {
assert_equal(posmod(-5,3), 1);
assert_equal(posmod(-4,3), 2);
@ -248,13 +183,6 @@ test_gaussian_rands();
module test_segs() {
assert_equal(segs(50,$fn=8), 8);
assert_equal(segs(50,$fa=2,$fs=2), 158);
}
test_segs();
module test_lerp() {
assert_equal(lerp(-20,20,0), -20);
assert_equal(lerp(-20,20,0.25), -10);
@ -486,105 +414,6 @@ test_convolve();
// Logic
module test_compare_vals() {
assert(compare_vals(-10,0) < 0);
assert(compare_vals(10,0) > 0);
assert(compare_vals(10,10) == 0);
assert(compare_vals("abc","abcd") < 0);
assert(compare_vals("abcd","abc") > 0);
assert(compare_vals("abcd","abcd") == 0);
assert(compare_vals(false,false) == 0);
assert(compare_vals(true,false) > 0);
assert(compare_vals(false,true) < 0);
assert(compare_vals(true,true) == 0);
assert(compare_vals([2,3,4], [2,3,4,5]) < 0);
assert(compare_vals([2,3,4,5], [2,3,4,5]) == 0);
assert(compare_vals([2,3,4,5], [2,3,4]) > 0);
assert(compare_vals([2,3,4,5], [2,3,5,5]) < 0);
assert(compare_vals([[2,3,4,5]], [[2,3,5,5]]) < 0);
assert(compare_vals([[2,3,4],[3,4,5]], [[2,3,4], [3,4,5]]) == 0);
assert(compare_vals([[2,3,4],[3,4,5]], [[2,3,4,5], [3,4,5]]) < 0);
assert(compare_vals([[2,3,4],[3,4,5]], [[2,3,4], [3,4,5,6]]) < 0);
assert(compare_vals([[2,3,4,5],[3,4,5]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_vals([[2,3,4],[3,4,5,6]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_vals([[2,3,4],[3,5,5]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_vals([[2,3,4],[3,4,5]], [[2,3,4], [3,5,5]]) < 0);
assert(compare_vals(undef, undef) == 0);
assert(compare_vals(undef, true) < 0);
assert(compare_vals(undef, 0) < 0);
assert(compare_vals(undef, "foo") < 0);
assert(compare_vals(undef, [2,3,4]) < 0);
assert(compare_vals(undef, [0:3]) < 0);
assert(compare_vals(true, undef) > 0);
assert(compare_vals(true, true) == 0);
assert(compare_vals(true, 0) < 0);
assert(compare_vals(true, "foo") < 0);
assert(compare_vals(true, [2,3,4]) < 0);
assert(compare_vals(true, [0:3]) < 0);
assert(compare_vals(0, undef) > 0);
assert(compare_vals(0, true) > 0);
assert(compare_vals(0, 0) == 0);
assert(compare_vals(0, "foo") < 0);
assert(compare_vals(0, [2,3,4]) < 0);
assert(compare_vals(0, [0:3]) < 0);
assert(compare_vals(1, undef) > 0);
assert(compare_vals(1, true) > 0);
assert(compare_vals(1, 1) == 0);
assert(compare_vals(1, "foo") < 0);
assert(compare_vals(1, [2,3,4]) < 0);
assert(compare_vals(1, [0:3]) < 0);
assert(compare_vals("foo", undef) > 0);
assert(compare_vals("foo", true) > 0);
assert(compare_vals("foo", 1) > 0);
assert(compare_vals("foo", "foo") == 0);
assert(compare_vals("foo", [2,3,4]) < 0);
assert(compare_vals("foo", [0:3]) < 0);
assert(compare_vals([2,3,4], undef) > 0);
assert(compare_vals([2,3,4], true) > 0);
assert(compare_vals([2,3,4], 1) > 0);
assert(compare_vals([2,3,4], "foo") > 0);
assert(compare_vals([2,3,4], [2,3,4]) == 0);
assert(compare_vals([2,3,4], [0:3]) < 0);
assert(compare_vals([0:3], undef) > 0);
assert(compare_vals([0:3], true) > 0);
assert(compare_vals([0:3], 1) > 0);
assert(compare_vals([0:3], "foo") > 0);
assert(compare_vals([0:3], [2,3,4]) > 0);
assert(compare_vals([0:3], [0:3]) == 0);
}
test_compare_vals();
module test_compare_lists() {
assert(compare_lists([2,3,4], [2,3,4,5]) < 0);
assert(compare_lists([2,3,4,5], [2,3,4,5]) == 0);
assert(compare_lists([2,3,4,5], [2,3,4]) > 0);
assert(compare_lists([2,3,4,5], [2,3,5,5]) < 0);
assert(compare_lists([[2,3,4],[3,4,5]], [[2,3,4], [3,4,5]]) == 0);
assert(compare_lists([[2,3,4],[3,4,5]], [[2,3,4,5], [3,4,5]]) < 0);
assert(compare_lists([[2,3,4],[3,4,5]], [[2,3,4], [3,4,5,6]]) < 0);
assert(compare_lists([[2,3,4,5],[3,4,5]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_lists([[2,3,4],[3,4,5,6]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_lists([[2,3,4],[3,5,5]], [[2,3,4], [3,4,5]]) > 0);
assert(compare_lists([[2,3,4],[3,4,5]], [[2,3,4], [3,5,5]]) < 0);
assert(compare_lists("cat", "bat") > 0);
assert(compare_lists(["cat"], ["bat"]) > 0);
}
test_compare_lists();
module test_any() {
assert_equal(any([0,false,undef]), false);

View file

@ -1,5 +1,6 @@
include<../std.scad>
module test_is_path() {
assert(is_path([[1,2,3],[4,5,6]]));
assert(is_path([[1,2,3],[4,5,6],[7,8,9]]));
@ -15,6 +16,23 @@ module test_is_path() {
test_is_path();
module test_is_1region() {
assert(!is_1region([[3,4],[5,6],[7,8]]));
assert(is_1region([[[3,4],[5,6],[7,8]]]));
}
test_is_1region();
module force_path() {
assert_equal(force_path([[3,4],[5,6],[7,8]]), [[3,4],[5,6],[7,8]]);
assert_equal(force_path([[[3,4],[5,6],[7,8]]]), [[3,4],[5,6],[7,8]]);
assert_equal(force_path("abc"), "abc");
assert_equal(force_path(13), 13);
}
test_is_1region();
module test_is_closed_path() {
assert(!is_closed_path([[1,2,3],[4,5,6],[1,8,9]]));
assert(is_closed_path([[1,2,3],[4,5,6],[1,8,9],[1,2,3]]));
@ -39,8 +57,219 @@ test_cleanup_path();
module test_path_merge_collinear() {
path = [[-20,-20], [-10,-20], [0,-10], [10,0], [20,10], [20,20], [15,30]];
assert(path_merge_collinear(path) == [[-20,-20], [-10,-20], [20,10], [20,20], [15,30]]);
assert(path_merge_collinear([path]) == [[-20,-20], [-10,-20], [20,10], [20,20], [15,30]]);
assert(path_merge_collinear([path]) == [[-20,-20], [-10,-20], [20,10], [20,20], [15,30]]);
sq=square(10);
assert_equal(path_merge_collinear(subdivide_path(square(10), refine=25),closed=true), sq);
}
test_path_merge_collinear();
module test_path_length(){
sq = square(10);
assert_equal(path_length(sq),30);
assert_equal(path_length(sq,true),40);
c = circle($fn=1000, r=1);
assert(approx(path_length(c,closed=true), 2*PI,eps=.0001));
}
test_path_length();
module test_path_segment_lengths(){
sq = square(10);
assert_equal(path_segment_lengths(sq), [10,10,10]);
assert_equal(path_segment_lengths(sq,true), [10,10,10,10]);
c = circle($fn=1000, r=1);
assert(approx(path_segment_lengths(c,closed=true), repeat(2*PI/1000,1000),eps=1e-7));
}
test_path_segment_lengths();
module test_path_length_fractions(){
sq = square(10);
assert_approx(path_length_fractions(sq), [0,1/3, 2/3, 1]);
assert_approx(path_length_fractions(sq,true), [0,1/4, 2/4,3/4, 1]);
}
test_path_length_fractions();
module test_subdivide_path(){
assert(approx(subdivide_path(square([2,2],center=true), 12), [[1, -1], [1/3, -1], [-1/3, -1], [-1, -1], [-1, -1/3], [-1, 1/3], [-1, 1], [-1/3, 1], [1/3, 1], [1, 1], [1, 1/3], [1, -1/3]]));
assert_equal(subdivide_path(square([8,2],center=true), 12), [[4, -1], [2, -1], [0, -1], [-2, -1], [-4, -1], [-4, 0], [-4, 1], [-2, 1], [0, 1], [2, 1], [4, 1], [4, 0]]);
assert_approx(subdivide_path(square([8,2],center=true), 12, method="segment"), [[4, -1], [4/3, -1], [-4/3, -1], [-4, -1], [-4, -1/3], [-4, 1/3], [-4, 1], [-4/3, 1], [4/3, 1], [4, 1], [4, 1/3], [4, -1/3]]);
assert_approx(subdivide_path(square([2,2],center=true), 17, closed=false), [[1, -1], [0.6, -1], [0.2, -1], [-0.2, -1], [-0.6, -1], [-1, -1], [-1, -2/3], [-1, -1/3], [-1, 0], [-1, 1/3], [-1, 2/3], [-1, 1], [-0.6, 1], [-0.2, 1], [0.2, 1], [0.6, 1], [1, 1]]);
assert_approx(subdivide_path(hexagon(side=2), [2,3,4,5,6,7], method="segment"),
[[2, 0], [1.5, -0.866025403784], [1, -1.73205080757],
[0.333333333333, -1.73205080757], [-0.333333333333,
-1.73205080757], [-1, -1.73205080757], [-1.25,
-1.29903810568], [-1.5, -0.866025403784], [-1.75,
-0.433012701892], [-2, 0], [-1.8, 0.346410161514],
[-1.6, 0.692820323028], [-1.4, 1.03923048454], [-1.2,
1.38564064606], [-1, 1.73205080757], [-0.666666666667,
1.73205080757], [-0.333333333333, 1.73205080757], [0,
1.73205080757], [0.333333333333, 1.73205080757],
[0.666666666667, 1.73205080757], [1, 1.73205080757],
[1.14285714286, 1.48461497792], [1.28571428571,
1.23717914826], [1.42857142857, 0.989743318611],
[1.57142857143, 0.742307488958], [1.71428571429,
0.494871659305], [1.85714285714, 0.247435829653]]);
assert_approx(subdivide_path(pentagon(side=2), [3,4,3,4], method="segment", closed=false),
[[1.7013016167, 0], [1.30944478184, -0.539344662917],
[0.917587946981, -1.07868932583], [0.525731112119,
-1.61803398875], [0.0502028539716, -1.46352549156],
[-0.425325404176, -1.30901699437], [-0.900853662324,
-1.15450849719], [-1.37638192047, -1], [-1.37638192047,
-0.333333333333], [-1.37638192047, 0.333333333333],
[-1.37638192047, 1], [-0.900853662324, 1.15450849719],
[-0.425325404176, 1.30901699437], [0.0502028539716,
1.46352549156], [0.525731112119, 1.61803398875]]);
assert_approx(subdivide_path(pentagon(side=2), 17),
[[1.7013016167, 0], [1.30944478184,
-0.539344662917], [0.917587946981, -1.07868932583],
[0.525731112119, -1.61803398875], [0.0502028539716,
-1.46352549156], [-0.425325404176, -1.30901699437],
[-0.900853662324, -1.15450849719], [-1.37638192047,
-1], [-1.37638192047, -0.333333333333],
[-1.37638192047, 0.333333333333], [-1.37638192047,
1], [-0.900853662324, 1.15450849719],
[-0.425325404176, 1.30901699437], [0.0502028539716,
1.46352549156], [0.525731112119, 1.61803398875],
[0.917587946981, 1.07868932583], [1.30944478184,
0.539344662917]]);
assert_approx(subdivide_path(pentagon(side=2), 17, exact=false),
[[1.7013016167, 0], [1.30944478184,
-0.539344662917], [0.917587946981, -1.07868932583],
[0.525731112119, -1.61803398875], [-0.108306565411,
-1.41202265917], [-0.742344242941, -1.20601132958],
[-1.37638192047, -1], [-1.37638192047,
-0.333333333333], [-1.37638192047, 0.333333333333],
[-1.37638192047, 1], [-0.742344242941,
1.20601132958], [-0.108306565411, 1.41202265917],
[0.525731112119, 1.61803398875], [0.917587946981,
1.07868932583], [1.30944478184, 0.539344662917]]);
assert_approx(subdivide_path(pentagon(side=2), 18, exact=false),
[[1.7013016167, 0], [1.40740899056,
-0.404508497187], [1.11351636441,
-0.809016994375], [0.819623738265,
-1.21352549156], [0.525731112119, -1.61803398875],
[0.0502028539716, -1.46352549156],
[-0.425325404176, -1.30901699437],
[-0.900853662324, -1.15450849719],
[-1.37638192047, -1], [-1.37638192047, -0.5],
[-1.37638192047, 0], [-1.37638192047, 0.5],
[-1.37638192047, 1], [-0.900853662324,
1.15450849719], [-0.425325404176, 1.30901699437],
[0.0502028539716, 1.46352549156], [0.525731112119,
1.61803398875], [0.819623738265, 1.21352549156],
[1.11351636441, 0.809016994375], [1.40740899056,
0.404508497187]]);
assert_approx(subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12),
[[0, 0, 0], [2/3, 0, 1/3], [4/3, 0, 2/3], [2, 0, 1], [2, 0.75, 1.25], [2, 1.5, 1.5], [2, 2.25, 1.75], [2, 3, 2], [1.6, 2.4, 1.6], [1.2, 1.8, 1.2], [0.8, 1.2, 0.8], [0.4, 0.6, 0.4]]);
}
test_subdivide_path();
module test_subdivide_long_segments(){
path = pentagon(d=100);
spath = subdivide_long_segments(path, 10, closed=true);
assert_approx(spath,
[[50, 0], [44.2418082865, -7.92547096913], [38.4836165729,
-15.8509419383], [32.7254248594, -23.7764129074], [26.9672331458,
-31.7018838765], [21.2090414323, -39.6273548456], [15.4508497187,
-47.5528258148], [6.1338998125, -44.5255652814], [-3.18305009375,
-41.498304748], [-12.5, -38.4710442147], [-21.8169499062,
-35.4437836813], [-31.1338998125, -32.416523148], [-40.4508497187,
-29.3892626146], [-40.4508497187, -19.5928417431], [-40.4508497187,
-9.79642087154], [-40.4508497187, 0], [-40.4508497187, 9.79642087154],
[-40.4508497187, 19.5928417431], [-40.4508497187, 29.3892626146],
[-31.1338998125, 32.416523148], [-21.8169499062, 35.4437836813],
[-12.5, 38.4710442147], [-3.18305009375, 41.498304748], [6.1338998125,
44.5255652814], [15.4508497187, 47.5528258148], [21.2090414323,
39.6273548456], [26.9672331458, 31.7018838765], [32.7254248594,
23.7764129074], [38.4836165729, 15.8509419383], [44.2418082865,
7.92547096913]]);
}
test_subdivide_long_segments();
module test_resample_path(){
path = xscale(2,circle($fn=250, r=10));
sampled = resample_path(path, 16);
assert_approx(sampled,
[[20, 0], [17.1657142861, -5.13020769642],
[11.8890531315, -8.04075246881], [6.03095737128,
-9.53380030092], [1.72917236085e-14, -9.99921044204],
[-6.03095737128, -9.53380030092], [-11.8890531315,
-8.04075246881], [-17.1657142861, -5.13020769642], [-20,
-3.19176120946e-14], [-17.1657142861, 5.13020769642],
[-11.8890531315, 8.04075246881], [-6.03095737128,
9.53380030092], [-4.20219414821e-14, 9.99921044204],
[6.03095737128, 9.53380030092], [11.8890531315,
8.04075246881], [17.1657142861, 5.13020769642]]);
path2 = square(20);
assert_approx(resample_path(path2, spacing=6),
[[20, 0], [13.8461538462, 0], [7.69230769231, 0], [1.53846153846, 0],
[0, 4.61538461538], [0, 10.7692307692], [0, 16.9230769231], [3.07692307692, 20],
[9.23076923077, 20], [15.3846153846, 20], [20, 18.4615384615], [20, 12.3076923077], [20, 6.15384615385]]);
assert_equal(resample_path(path2, spacing=6,closed=false),[[20, 0], [14, 0], [8, 0], [2, 0], [0, 4], [0, 10], [0, 16], [2, 20], [8, 20], [14, 20], [20, 20]]);
assert_approx(resample_path(path, spacing=17),
[[20, 0], [8.01443073309, -9.16170407964],
[-8.01443073309, -9.16170407964], [-20,
-1.59309060367e-14], [-8.01443073309, 9.16170407964],
[8.01443073309, 9.16170407964]]);
}
test_resample_path();
module test_path_closest_point(){
path = circle(d=100,$fn=6);
pt = [20,10];
closest = path_closest_point(path, pt);
assert_approx(closest, [5, [38.1698729811, 20.4903810568]]);
}
test_path_closest_point();
module test_path_tangents(){
path = circle(r=1, $fn=200);
path_t = path_tangents(path,closed=true);
assert_approx(path_t, hstack(column(path,1), -column(path,0)));
rect = square([10,3]);
tr1 = path_tangents(rect,closed=true);
tr2 = path_tangents(rect,closed=true,uniform=false);
tr3 = path_tangents(rect,closed=false);
tr4 = path_tangents(rect,closed=false,uniform=false);
assert_approx(tr1, [[-0.957826285221, -0.287347885566], [-0.957826285221, 0.287347885566], [0.957826285221, 0.287347885566], [0.957826285221, -0.287347885566]]);
assert_approx(tr2, [[-0.707106781187, -0.707106781187], [-0.707106781187, 0.707106781187], [0.707106781187, 0.707106781187], [0.707106781187, -0.707106781187]]);
assert_approx(tr3, [[-0.99503719021, -0.099503719021], [-0.957826285221, 0.287347885566], [0.957826285221, 0.287347885566], [0.99503719021, -0.099503719021]]);
assert_approx(tr4, [[-1, 0], [-0.707106781187, 0.707106781187], [0.707106781187, 0.707106781187], [1, 0]]);
}
test_path_tangents();
module test_path_curvature(){
c8 = path3d(circle(r=8, $fn=100));
c28 = path3d(circle(r=28, $fn=100));
assert(approx(path_curvature(c8,closed=true), repeat(1/8, 100), 4e-4));
assert(approx(path_curvature(c28,closed=true), repeat(1/28, 100), 4e-4));
}
test_path_curvature();
module test_path_torsion(){
c = path3d(circle(r=1, $fn=100));
tc = path_torsion(c, closed=true);
assert(all_zero(tc));
a=3;b=7;
helix = [for(t=[0:1:20]) [a*cos(t), a*sin(t), b*t*PI/180]];
th = path_torsion(helix, closed=false);
assert(approx(th[5], b/(a*a+b*b), 1e-5));
}
test_path_torsion();
//echo(fmt_float(sampled));

View file

@ -216,11 +216,4 @@ module test_mask2d_ogee() {
test_mask2d_ogee();
module test_dashed_stroke() {
segs = dashed_stroke([[0,0],[10,0]], dashpat=[3,2], closed=false);
assert_equal(segs,[[[0,0],[3,0]], [[5,0],[8,0]]]);
}
test_dashed_stroke();
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

View file

@ -395,4 +395,61 @@ module test_skew() {
test_skew();
module test_apply() {
assert(approx(apply(affine3d_xrot(90),2*UP),2*FRONT));
assert(approx(apply(affine3d_yrot(90),2*UP),2*RIGHT));
assert(approx(apply(affine3d_zrot(90),2*UP),2*UP));
assert(approx(apply(affine3d_zrot(90),2*RIGHT),2*BACK));
assert(approx(apply(affine3d_zrot(90),2*BACK+2*RIGHT),2*BACK+2*LEFT));
assert(approx(apply(affine3d_xrot(135),2*BACK+2*UP),2*sqrt(2)*FWD));
assert(approx(apply(affine3d_yrot(135),2*RIGHT+2*UP),2*sqrt(2)*DOWN));
assert(approx(apply(affine3d_zrot(45),2*BACK+2*RIGHT),2*sqrt(2)*BACK));
module check_path_apply(mat,path)
assert_approx(apply(mat,path),path3d([for (p=path) mat*concat(p,1)]));
check_path_apply(xrot(45), path3d(rect(100,center=true)));
check_path_apply(yrot(45), path3d(rect(100,center=true)));
check_path_apply(zrot(45), path3d(rect(100,center=true)));
check_path_apply(rot([20,30,40])*scale([0.9,1.1,1])*move([10,20,30]), path3d(rect(100,center=true)));
module check_patch_apply(mat,patch)
assert_approx(apply(mat,patch), [for (path=patch) path3d([for (p=path) mat*concat(p,1)])]);
flat = [for (x=[-50:25:50]) [for (y=[-50:25:50]) [x,y,0]]];
check_patch_apply(xrot(45), flat);
check_patch_apply(yrot(45), flat);
check_patch_apply(zrot(45), flat);
check_patch_apply(rot([20,30,40])*scale([0.9,1.1,1])*move([10,20,30]), flat);
}
test_apply();
module test_is_2d_transform() {
assert(!is_2d_transform(affine2d_identity()));
assert(!is_2d_transform(affine2d_translate([5,8])));
assert(!is_2d_transform(affine2d_scale([3,4])));
assert(!is_2d_transform(affine2d_zrot(30)));
assert(!is_2d_transform(affine2d_mirror([-1,1])));
assert(!is_2d_transform(affine2d_skew(30,15)));
assert(is_2d_transform(affine3d_identity()));
assert(is_2d_transform(affine3d_translate([30,40,0])));
assert(!is_2d_transform(affine3d_translate([30,40,50])));
assert(is_2d_transform(affine3d_scale([3,4,1])));
assert(!is_2d_transform(affine3d_xrot(30)));
assert(!is_2d_transform(affine3d_yrot(30)));
assert(is_2d_transform(affine3d_zrot(30)));
assert(is_2d_transform(affine3d_skew(sxy=2)));
assert(is_2d_transform(affine3d_skew(syx=2)));
assert(!is_2d_transform(affine3d_skew(szx=2)));
assert(!is_2d_transform(affine3d_skew(szy=2)));
}
test_is_2d_transform();
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

View file

@ -76,6 +76,15 @@ module test_is_def() {
test_is_def();
module test_segs() {
assert_equal(segs(50,$fn=8), 8);
assert_equal(segs(50,$fa=2,$fs=2), 158);
}
test_segs();
module test_is_str() {
assert(!is_str(undef));
assert(!is_str(true));

View file

@ -113,6 +113,40 @@ module test_v_theta() {
test_v_theta();
module test_min_index() {
vals = rands(-100,100,100,seed=75);
minval = min(vals);
minidx = min_index(vals);
assert_equal(vals[minidx], minval);
assert_equal(min_index([3,4,5,6]), 0);
assert_equal(min_index([4,3,5,6]), 1);
assert_equal(min_index([4,5,3,6]), 2);
assert_equal(min_index([4,5,6,3]), 3);
assert_equal(min_index([6,5,4,3]), 3);
assert_equal(min_index([6,3,4,5]), 1);
assert_equal(min_index([-56,72,-874,5]), 2);
}
test_min_index();
module test_max_index() {
vals = rands(-100,100,100,seed=97);
maxval = max(vals);
maxidx = max_index(vals);
assert_equal(vals[maxidx], maxval);
assert_equal(max_index([3,4,5,6]), 3);
assert_equal(max_index([3,4,6,5]), 2);
assert_equal(max_index([3,6,4,5]), 1);
assert_equal(max_index([6,3,4,5]), 0);
assert_equal(max_index([5,6,4,3]), 1);
assert_equal(max_index([-56,72,-874,5]), 1);
}
test_max_index();
module test_unit() {
assert(unit([10,0,0]) == [1,0,0]);
assert(unit([0,10,0]) == [0,1,0]);

View file

@ -427,6 +427,7 @@ function _turtle3d_state_valid(state) =
&& is_num(state[3])
&& is_num(state[4]);
module turtle3d(commands, state=RIGHT, transforms=false, full_state=false, repeat=1) {no_module();}
function turtle3d(commands, state=RIGHT, transforms=false, full_state=false, repeat=1) =
assert(is_bool(transforms))
let(

View file

@ -737,7 +737,7 @@ function _split_2dpolygons_at_each_x(polys, xs, _i=0) =
], xs, _i=_i+1
);
/// Function: _slice_3dpolygons()
/// Internal Function: _slice_3dpolygons()
/// Usage:
/// splitpolys = _slice_3dpolygons(polys, dir, cuts);
/// Topics: Geometry, Polygons, Intersections
@ -877,7 +877,7 @@ function vnf_area(vnf) =
sum([for(face=vnf[1]) polygon_area(select(verts,face))]);
/// Function: _vnf_centroid()
/// Internal Function: _vnf_centroid()
/// Usage:
/// vol = _vnf_centroid(vnf);
/// Description: