From 2494de9368115f6ebab7b76d04ca904ee7c116f8 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 11 Sep 2021 18:48:23 -0400 Subject: [PATCH] reordering and moving for improved docs removed xxrot and xxflip --- geometry.scad | 753 ++++++++++++++------------------------- regions.scad | 4 +- rounding.scad | 6 +- skin.scad | 2 +- tests/test_geometry.scad | 74 +--- tests/test_vectors.scad | 56 ++- transforms.scad | 360 ------------------- vectors.scad | 54 +++ vnf.scad | 158 +++++++- 9 files changed, 556 insertions(+), 911 deletions(-) diff --git a/geometry.scad b/geometry.scad index 93e2fc1..51175c7 100644 --- a/geometry.scad +++ b/geometry.scad @@ -44,10 +44,10 @@ function _valid_line(line,dim,eps=EPSILON) = function _valid_plane(p, eps=EPSILON) = is_vector(p,4) && ! approx(norm(p),0,eps); -// Function: point_left_of_line2d() +/// Internal Function: point_left_of_line2d() // Usage: // pt = point_left_of_line2d(point, line); -// Topics: Geometry, Points, Lines +/// Topics: Geometry, Points, Lines // Description: // Return >0 if point is left of the line defined by `line`. // Return =0 if point is on the line. @@ -55,7 +55,7 @@ function _valid_plane(p, eps=EPSILON) = is_vector(p,4) && ! approx(norm(p),0,eps // Arguments: // point = The point to check position of. // line = Array of two points forming the line segment to test against. -function point_left_of_line2d(point, line) = +function _point_left_of_line2d(point, line) = assert( is_vector(point,2) && is_vector(line*point, 2), "Improper input." ) cross(line[0]-point, line[1]-line[0]); @@ -327,7 +327,7 @@ function line_closest_point(line, pt, bounded=false) = // fast = If true, don't verify that all points are collinear. Default: false // eps = How much variance is allowed in testing each point against the line. Default: `EPSILON` (1e-9) function line_from_points(points, fast=false, eps=EPSILON) = - assert( is_path(points,dim=undef), "Improper point list." ) + assert( is_path(points), "Invalid point list." ) 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])=0, "The tolerance should be a non-negative value." ) + len(points)<=2 ? false + : let( ip = noncollinear_triple(points,error=false,eps=eps) ) + ip == [] ? false : + let( plane = plane3pt(points[ip[0]],points[ip[1]],points[ip[2]]) ) + _pointlist_greatest_distance(points,plane) < eps; + + + // Function: plane3pt() // Usage: // plane = plane3pt(p1, p2, p3); @@ -546,61 +566,6 @@ function plane_offset(plane) = -// Function: plane_closest_point() -// Usage: -// pts = plane_closest_point(plane, points); -// Topics: Geometry, Planes, Projection -// Description: -// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or -// 3d points, return the closest 3D orthogonal projection of the points on the plane. -// In other words, for every point given, returns the closest point to it on the plane. -// Arguments: -// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. -// points = List of points to project -// Example(FlatSpin,VPD=500,VPT=[2,20,10]): -// points = move([10,20,30], p=yrot(25, p=path3d(circle(d=100, $fn=36)))); -// plane = plane_from_normal([1,0,1]); -// proj = plane_closest_point(plane,points); -// color("red") move_copies(points) sphere(d=2,$fn=12); -// color("blue") move_copies(proj) sphere(d=2,$fn=12); -// move(centroid(proj)) { -// rot(from=UP,to=plane_normal(plane)) { -// anchor_arrow(30); -// %cube([120,150,0.1],center=true); -// } -// } -function plane_closest_point(plane, points) = - is_vector(points,3) ? plane_closest_point(plane,[points])[0] : - assert( _valid_plane(plane), "Invalid plane." ) - assert( is_matrix(points,undef,3), "Must supply 3D points.") - let( - plane = normalize_plane(plane), - n = point3d(plane) - ) - [for(pi=points) pi - (pi*n - plane[3])*n]; - - -// Function: point_plane_distance() -// Usage: -// dist = point_plane_distance(plane, point) -// Topics: Geometry, Planes, Distance -// Description: -// Given a plane as [A,B,C,D] where the cartesian equation for that plane -// is Ax+By+Cz=D, determines how far from that plane the given point is. -// The returned distance will be positive if the point is above the -// plane, meaning on the side where the plane normal points. -// If the point is below the plane, then the distance returned -// will be negative. The normal of the plane is [A,B,C]. -// Arguments: -// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. -// point = The distance evaluation point. -function point_plane_distance(plane, point) = - assert( _valid_plane(plane), "Invalid input plane." ) - assert( is_vector(point,3), "The point should be a 3D point." ) - let( plane = normalize_plane(plane) ) - point3d(plane)* point - plane[3]; - - // Returns [POINT, U] if line intersects plane at one point, where U is zero at line[0] and 1 at line[1] // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. @@ -616,36 +581,17 @@ function _general_plane_line_intersection(plane, line, eps=EPSILON) = : [ line[0]-a/b*(line[1]-line[0]), -a/b ]; -// Function: normalize_plane() +/// Internal Function: normalize_plane() // Usage: // nplane = normalize_plane(plane); -// Topics: Geometry, Planes +/// Topics: Geometry, Planes // Description: // Returns a new representation [A,B,C,D] of `plane` where norm([A,B,C]) is equal to one. -function normalize_plane(plane) = +function _normalize_plane(plane) = assert( _valid_plane(plane), str("Invalid plane. ",plane ) ) plane/norm(point3d(plane)); -// Function: plane_line_angle() -// Usage: -// angle = plane_line_angle(plane,line); -// Topics: Geometry, Planes, Lines, Angle -// Description: -// Compute the angle between a plane [A, B, C, D] and a 3d line, specified as a pair of 3d points [p1,p2]. -// The resulting angle is signed, with the sign positive if the vector p2-p1 lies above the plane, on -// the same side of the plane as the plane's normal vector. -function plane_line_angle(plane, line) = - assert( _valid_plane(plane), "Invalid plane." ) - assert( _valid_line(line,dim=3), "Invalid 3d line." ) - let( - linedir = unit(line[1]-line[0]), - normal = plane_normal(plane), - sin_angle = linedir*normal, - cos_angle = norm(cross(linedir,normal)) - ) atan2(sin_angle,cos_angle); - - // Function: plane_line_intersection() // Usage: // pt = plane_line_intersection(plane, line, [bounded], [eps]); @@ -765,23 +711,81 @@ function plane_intersection(plane1,plane2,plane3) = [point, point+normal]; -// Function: coplanar() + +// Function: plane_line_angle() // Usage: -// test = coplanar(points,[eps]); -// Topics: Geometry, Coplanarity +// angle = plane_line_angle(plane,line); +// Topics: Geometry, Planes, Lines, Angle // Description: -// Returns true if the given 3D points are non-collinear and are on a plane. +// Compute the angle between a plane [A, B, C, D] and a 3d line, specified as a pair of 3d points [p1,p2]. +// The resulting angle is signed, with the sign positive if the vector p2-p1 lies above the plane, on +// the same side of the plane as the plane's normal vector. +function plane_line_angle(plane, line) = + assert( _valid_plane(plane), "Invalid plane." ) + assert( _valid_line(line,dim=3), "Invalid 3d line." ) + let( + linedir = unit(line[1]-line[0]), + normal = plane_normal(plane), + sin_angle = linedir*normal, + cos_angle = norm(cross(linedir,normal)) + ) atan2(sin_angle,cos_angle); + + + +// Function: plane_closest_point() +// Usage: +// pts = plane_closest_point(plane, points); +// Topics: Geometry, Planes, Projection +// Description: +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or +// 3d points, return the closest 3D orthogonal projection of the points on the plane. +// In other words, for every point given, returns the closest point to it on the plane. // Arguments: -// points = The points to test. -// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) -function 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 - : let( ip = noncollinear_triple(points,error=false,eps=eps) ) - ip == [] ? false : - let( plane = plane3pt(points[ip[0]],points[ip[1]],points[ip[2]]) ) - _pointlist_greatest_distance(points,plane) < eps; +// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. +// points = List of points to project +// Example(FlatSpin,VPD=500,VPT=[2,20,10]): +// points = move([10,20,30], p=yrot(25, p=path3d(circle(d=100, $fn=36)))); +// plane = plane_from_normal([1,0,1]); +// proj = plane_closest_point(plane,points); +// color("red") move_copies(points) sphere(d=2,$fn=12); +// color("blue") move_copies(proj) sphere(d=2,$fn=12); +// move(centroid(proj)) { +// rot(from=UP,to=plane_normal(plane)) { +// anchor_arrow(30); +// %cube([120,150,0.1],center=true); +// } +// } +function plane_closest_point(plane, points) = + is_vector(points,3) ? plane_closest_point(plane,[points])[0] : + assert( _valid_plane(plane), "Invalid plane." ) + assert( is_matrix(points,undef,3), "Must supply 3D points.") + let( + plane = _normalize_plane(plane), + n = point3d(plane) + ) + [for(pi=points) pi - (pi*n - plane[3])*n]; + + +// Function: point_plane_distance() +// Usage: +// dist = point_plane_distance(plane, point) +// Topics: Geometry, Planes, Distance +// Description: +// Given a plane as [A,B,C,D] where the cartesian equation for that plane +// is Ax+By+Cz=D, determines how far from that plane the given point is. +// The returned distance will be positive if the point is above the +// plane, meaning on the side where the plane normal points. +// If the point is below the plane, then the distance returned +// will be negative. The normal of the plane is [A,B,C]. +// Arguments: +// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. +// point = The distance evaluation point. +function point_plane_distance(plane, point) = + assert( _valid_plane(plane), "Invalid input plane." ) + assert( is_vector(point,3), "The point should be a 3D point." ) + let( plane = _normalize_plane(plane) ) + point3d(plane)* point - plane[3]; + // the maximum distance from points to the plane @@ -1217,58 +1221,6 @@ function noncollinear_triple(points,error=true,eps=EPSILON) = [0, b, max_index(distlist)]; -// Function: pointlist_bounds() -// Usage: -// pt_pair = pointlist_bounds(pts); -// Topics: Geometry, Bounding Boxes, Bounds -// Description: -// Finds the bounds containing all the points in `pts` which can be a list of points in any dimension. -// Returns a list of two items: a list of the minimums and a list of the maximums. For example, with -// 3d points `[[MINX, MINY, MINZ], [MAXX, MAXY, MAXZ]]` -// Arguments: -// pts = List of points. -function pointlist_bounds(pts) = - assert(is_path(pts,dim=undef,fast=true) , "Invalid pointlist." ) - let( - select = ident(len(pts[0])), - spread = [ - for(i=[0:len(pts[0])-1]) - let( spreadi = pts*select[i] ) - [ min(spreadi), max(spreadi) ] - ] - ) transpose(spread); - - -// Function: closest_point() -// Usage: -// index = closest_point(pt, points); -// Topics: Geometry, Points, Distance -// Description: -// Given a list of `points`, finds the index of the closest point to `pt`. -// Arguments: -// pt = The point to find the closest point to. -// points = The list of points to search. -function closest_point(pt, points) = - assert( is_vector(pt), "Invalid point." ) - assert(is_path(points,dim=len(pt)), "Invalid pointlist or incompatible dimensions." ) - min_index([for (p=points) norm(p-pt)]); - - -// Function: furthest_point() -// Usage: -// index = furthest_point(pt, points); -// Topics: Geometry, Points, Distance -// Description: -// Given a list of `points`, finds the index of the furthest point from `pt`. -// Arguments: -// pt = The point to find the farthest point from. -// points = The list of points to search. -function furthest_point(pt, points) = - assert( is_vector(pt), "Invalid point." ) - assert(is_path(points,dim=len(pt)), "Invalid pointlist or incompatible dimensions." ) - max_index([for (p=points) norm(p-pt)]); - - // Section: Polygons @@ -1302,6 +1254,183 @@ function polygon_area(poly, signed=false) = signed ? total : abs(total); +// Function: centroid() +// Usage: +// cpt = centroid(poly); +// Topics: Geometry, Polygons, Centroid +// Description: +// Given a simple 2D polygon, returns the 2D coordinates of the polygon's centroid. +// Given a simple 3D planar polygon, returns the 3D coordinates of the polygon's centroid. +// Collinear points produce an error. The results are meaningless for self-intersecting +// polygons or an error is produced. +// Arguments: +// poly = Points of the polygon from which the centroid is calculated. +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) +function centroid(poly, eps=EPSILON) = + assert( is_path(poly,dim=[2,3]), "The input must be a 2D or 3D polygon." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) + let( + n = len(poly[0])==2 ? 1 : + let( plane = plane_from_points(poly, fast=true) ) + assert( !is_undef(plane), "The polygon must be planar." ) + plane_normal(plane), + v0 = poly[0] , + val = sum([ + for(i=[1:len(poly)-2]) + let( + v1 = poly[i], + v2 = poly[i+1], + area = cross(v2-v0,v1-v0)*n + ) [ area, (v0+v1+v2)*area ] + ]) + ) + assert(!approx(val[0],0, eps), "The polygon is self-intersecting or its points are collinear.") + val[1]/val[0]/3; + + + +// Function: polygon_normal() +// Usage: +// vec = polygon_normal(poly); +// Topics: Geometry, Polygons +// Description: +// Given a 3D simple planar polygon, returns a unit normal vector for the polygon. The vector +// is oriented so that if the normal points towards the viewer, the polygon winds in the clockwise +// direction. If the polygon has zero area, returns `undef`. If the polygon is self-intersecting +// the the result is undefined. It doesn't check for coplanarity. +// Arguments: +// poly = The list of 3D path points for the perimeter of the polygon. +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==0 ? undef : unit(-area_vec); + + +// Function: point_in_polygon() +// Usage: +// test = point_in_polygon(point, poly, [eps]) +// Topics: Geometry, Polygons +// Description: +// This function tests whether the given 2D point is inside, outside or on the boundary of +// the specified 2D polygon using either the Nonzero Winding rule or the Even-Odd rule. +// See https://en.wikipedia.org/wiki/Nonzero-rule and https://en.wikipedia.org/wiki/Even–odd_rule. +// The polygon is given as a list of 2D points, not including the repeated end point. +// Returns -1 if the point is outside the polygon. +// Returns 0 if the point is on the boundary. +// Returns 1 if the point lies in the interior. +// The polygon does not need to be simple: it may have self-intersections. +// But the polygon cannot have holes (it must be simply connected). +// Rounding errors may give mixed results for points on or near the boundary. +// Arguments: +// point = The 2D point to check position of. +// poly = The list of 2D path points forming the perimeter of the polygon. +// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd" (Default: true ) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) +function point_in_polygon(point, poly, nonzero=true, 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." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) + // 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) ) + point_on_segment(point, seg, eps=eps)? 1:0 + ] + ) + 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) + ] + ) sum(windchk) != 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 + ] + ) 2*(len(cross)%2)-1; + + +// Function: is_polygon_clockwise() +// Usage: +// test = is_polygon_clockwise(poly); +// Topics: Geometry, Polygons, Clockwise +// See Also: clockwise_polygon(), ccw_polygon(), reverse_polygon() +// Description: +// Return true if the given 2D simple polygon is in clockwise order, false otherwise. +// Results for complex (self-intersecting) polygon are indeterminate. +// Arguments: +// poly = The list of 2D path points for the perimeter of the polygon. +function is_polygon_clockwise(poly) = + assert(is_path(poly,dim=2), "Input should be a 2d path") + polygon_area(poly, signed=true)<-EPSILON; + + +// Function: clockwise_polygon() +// Usage: +// newpoly = clockwise_polygon(poly); +// Topics: Geometry, Polygons, Clockwise +// See Also: is_polygon_clockwise(), ccw_polygon(), reverse_polygon() +// Description: +// Given a 2D polygon path, returns the clockwise winding version of that path. +// Arguments: +// 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); + + +// Function: ccw_polygon() +// Usage: +// newpoly = ccw_polygon(poly); +// See Also: is_polygon_clockwise(), clockwise_polygon(), reverse_polygon() +// Topics: Geometry, Polygons, Clockwise +// Description: +// Given a 2D polygon poly, returns the counter-clockwise winding version of that poly. +// Arguments: +// 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; + + +// Function: reverse_polygon() +// Usage: +// newpoly = reverse_polygon(poly) +// Topics: Geometry, Polygons, Clockwise +// See Also: is_polygon_clockwise(), ccw_polygon(), clockwise_polygon() +// Description: +// Reverses a polygon's winding direction, while still using the same start point. +// Arguments: +// poly = The list of the path points for the perimeter of the polygon. +function reverse_polygon(poly) = + assert(is_path(poly), "Input should be a polygon") + [ poly[0], for(i=[len(poly)-1:-1:1]) poly[i] ]; + + + // Function: polygon_shift() // Usage: // newpoly = polygon_shift(poly, i); @@ -1375,7 +1504,7 @@ function reindex_polygon(reference, poly, return_error=false) = dim = len(reference[0]), N = len(reference), fixpoly = dim != 2? poly : - polygon_is_clockwise(reference) + is_polygon_clockwise(reference) ? clockwise_polygon(poly) : ccw_polygon(poly), I = [for(i=reference) 1], @@ -1427,334 +1556,6 @@ function align_polygon(reference, poly, angles, cp) = ) alignments[best][0]; -// Function: centroid() -// Usage: -// cpt = centroid(poly); -// Topics: Geometry, Polygons, Centroid -// Description: -// Given a simple 2D polygon, returns the 2D coordinates of the polygon's centroid. -// Given a simple 3D planar polygon, returns the 3D coordinates of the polygon's centroid. -// Collinear points produce an error. The results are meaningless for self-intersecting -// polygons or an error is produced. -// Arguments: -// poly = Points of the polygon from which the centroid is calculated. -// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) -function centroid(poly, eps=EPSILON) = - assert( is_path(poly,dim=[2,3]), "The input must be a 2D or 3D polygon." ) - assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) - let( - n = len(poly[0])==2 ? 1 : - let( plane = plane_from_points(poly, fast=true) ) - assert( !is_undef(plane), "The polygon must be planar." ) - plane_normal(plane), - v0 = poly[0] , - val = sum([ - for(i=[1:len(poly)-2]) - let( - v1 = poly[i], - v2 = poly[i+1], - area = cross(v2-v0,v1-v0)*n - ) [ area, (v0+v1+v2)*area ] - ]) - ) - assert(!approx(val[0],0, eps), "The polygon is self-intersecting or its points are collinear.") - val[1]/val[0]/3; - - - -// Function: point_in_polygon() -// Usage: -// test = point_in_polygon(point, poly, [eps]) -// Topics: Geometry, Polygons -// Description: -// This function tests whether the given 2D point is inside, outside or on the boundary of -// the specified 2D polygon using either the Nonzero Winding rule or the Even-Odd rule. -// See https://en.wikipedia.org/wiki/Nonzero-rule and https://en.wikipedia.org/wiki/Even–odd_rule. -// The polygon is given as a list of 2D points, not including the repeated end point. -// Returns -1 if the point is outside the polygon. -// Returns 0 if the point is on the boundary. -// Returns 1 if the point lies in the interior. -// The polygon does not need to be simple: it may have self-intersections. -// But the polygon cannot have holes (it must be simply connected). -// Rounding errors may give mixed results for points on or near the boundary. -// Arguments: -// point = The 2D point to check position of. -// poly = The list of 2D path points forming the perimeter of the polygon. -// nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd" (Default: true ) -// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) -function point_in_polygon(point, poly, nonzero=true, 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." ) - assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) - // 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) ) - point_on_segment(point, seg, eps=eps)? 1:0 - ] - ) - 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) - ] - ) sum(windchk) != 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 - ] - ) 2*(len(cross)%2)-1; - - -// Function: polygon_is_clockwise() -// Usage: -// test = polygon_is_clockwise(poly); -// Topics: Geometry, Polygons, Clockwise -// See Also: clockwise_polygon(), ccw_polygon(), reverse_polygon() -// Description: -// Return true if the given 2D simple polygon is in clockwise order, false otherwise. -// Results for complex (self-intersecting) polygon are indeterminate. -// Arguments: -// poly = The list of 2D path points for the perimeter of the polygon. -function polygon_is_clockwise(poly) = - assert(is_path(poly,dim=2), "Input should be a 2d path") - polygon_area(poly, signed=true)<-EPSILON; - - -// Function: clockwise_polygon() -// Usage: -// newpoly = clockwise_polygon(poly); -// Topics: Geometry, Polygons, Clockwise -// See Also: polygon_is_clockwise(), ccw_polygon(), reverse_polygon() -// Description: -// Given a 2D polygon path, returns the clockwise winding version of that path. -// Arguments: -// 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); - - -// Function: ccw_polygon() -// Usage: -// newpoly = ccw_polygon(poly); -// See Also: polygon_is_clockwise(), clockwise_polygon(), reverse_polygon() -// Topics: Geometry, Polygons, Clockwise -// Description: -// Given a 2D polygon poly, returns the counter-clockwise winding version of that poly. -// Arguments: -// 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; - - -// Function: reverse_polygon() -// Usage: -// newpoly = reverse_polygon(poly) -// Topics: Geometry, Polygons, Clockwise -// See Also: polygon_is_clockwise(), ccw_polygon(), clockwise_polygon() -// Description: -// Reverses a polygon's winding direction, while still using the same start point. -// Arguments: -// poly = The list of the path points for the perimeter of the polygon. -function reverse_polygon(poly) = - assert(is_path(poly), "Input should be a polygon") - [ poly[0], for(i=[len(poly)-1:-1:1]) poly[i] ]; - - -// Function: polygon_normal() -// Usage: -// vec = polygon_normal(poly); -// Topics: Geometry, Polygons -// Description: -// Given a 3D simple planar polygon, returns a unit normal vector for the polygon. The vector -// is oriented so that if the normal points towards the viewer, the polygon winds in the clockwise -// direction. If the polygon has zero area, returns `undef`. If the polygon is self-intersecting -// the the result is undefined. It doesn't check for coplanarity. -// Arguments: -// poly = The list of 3D path points for the perimeter of the polygon. -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==0 ? undef : unit(-area_vec); - - -function _split_polygon_at_x(poly, x) = - let( - xs = subindex(poly,0) - ) (min(xs) >= x || max(xs) <= x)? [poly] : - let( - poly2 = [ - for (p = pair(poly,true)) each [ - p[0], - if( - (p[0].x < x && p[1].x > x) || - (p[1].x < x && p[0].x > x) - ) let( - u = (x - p[0].x) / (p[1].x - p[0].x) - ) [ - x, // Important for later exact match tests - u*(p[1].y-p[0].y)+p[0].y, - u*(p[1].z-p[0].z)+p[0].z, - ] - ] - ], - out1 = [for (p = poly2) if(p.x <= x) p], - out2 = [for (p = poly2) if(p.x >= x) p], - out3 = [ - if (len(out1)>=3) each split_path_at_self_crossings(out1), - if (len(out2)>=3) each split_path_at_self_crossings(out2), - ], - out = [for (p=out3) if (len(p) > 2) cleanup_path(p)] - ) out; - - -function _split_polygon_at_y(poly, y) = - let( - ys = subindex(poly,1) - ) (min(ys) >= y || max(ys) <= y)? [poly] : - let( - poly2 = [ - for (p = pair(poly,true)) each [ - p[0], - if( - (p[0].y < y && p[1].y > y) || - (p[1].y < y && p[0].y > y) - ) let( - u = (y - p[0].y) / (p[1].y - p[0].y) - ) [ - u*(p[1].x-p[0].x)+p[0].x, - y, // Important for later exact match tests - u*(p[1].z-p[0].z)+p[0].z, - ] - ] - ], - out1 = [for (p = poly2) if(p.y <= y) p], - out2 = [for (p = poly2) if(p.y >= y) p], - out3 = [ - if (len(out1)>=3) each split_path_at_self_crossings(out1), - if (len(out2)>=3) each split_path_at_self_crossings(out2), - ], - out = [for (p=out3) if (len(p) > 2) cleanup_path(p)] - ) out; - - -function _split_polygon_at_z(poly, z) = - let( - zs = subindex(poly,2) - ) (min(zs) >= z || max(zs) <= z)? [poly] : - let( - poly2 = [ - for (p = pair(poly,true)) each [ - p[0], - if( - (p[0].z < z && p[1].z > z) || - (p[1].z < z && p[0].z > z) - ) let( - u = (z - p[0].z) / (p[1].z - p[0].z) - ) [ - u*(p[1].x-p[0].x)+p[0].x, - u*(p[1].y-p[0].y)+p[0].y, - z, // Important for later exact match tests - ] - ] - ], - out1 = [for (p = poly2) if(p.z <= z) p], - out2 = [for (p = poly2) if(p.z >= z) p], - out3 = [ - if (len(out1)>=3) each split_path_at_self_crossings(close_path(out1), closed=false), - if (len(out2)>=3) each split_path_at_self_crossings(close_path(out2), closed=false), - ], - out = [for (p=out3) if (len(p) > 2) cleanup_path(p)] - ) out; - - -// Function: split_polygons_at_each_x() -// Usage: -// splitpolys = split_polygons_at_each_x(polys, xs); -// Topics: Geometry, Polygons, Intersections -// Description: -// Given a list of 3D polygons, splits all of them wherever they cross any X value given in `xs`. -// Arguments: -// polys = A list of 3D polygons to split. -// xs = A list of scalar X values to split at. -function split_polygons_at_each_x(polys, xs, _i=0) = - assert( [for (poly=polys) if (!is_path(poly,3)) 1] == [], "Expects list of 3D paths.") - assert( is_vector(xs), "The split value list should contain only numbers." ) - _i>=len(xs)? polys : - split_polygons_at_each_x( - [ - for (poly = polys) - each _split_polygon_at_x(poly, xs[_i]) - ], xs, _i=_i+1 - ); - - -// Function: split_polygons_at_each_y() -// Usage: -// splitpolys = split_polygons_at_each_y(polys, ys); -// Topics: Geometry, Polygons, Intersections -// Description: -// Given a list of 3D polygons, splits all of them wherever they cross any Y value given in `ys`. -// Arguments: -// polys = A list of 3D polygons to split. -// ys = A list of scalar Y values to split at. -function split_polygons_at_each_y(polys, ys, _i=0) = - assert( [for (poly=polys) if (!is_path(poly,3)) 1] == [], "Expects list of 3D paths.") - assert( is_vector(ys), "The split value list should contain only numbers." ) - _i>=len(ys)? polys : - split_polygons_at_each_y( - [ - for (poly = polys) - each _split_polygon_at_y(poly, ys[_i]) - ], ys, _i=_i+1 - ); - - -// Function: split_polygons_at_each_z() -// Usage: -// splitpolys = split_polygons_at_each_z(polys, zs); -// Topics: Geometry, Polygons, Intersections -// Description: -// Given a list of 3D polygons, splits all of them wherever they cross any Z value given in `zs`. -// Arguments: -// polys = A list of 3D polygons to split. -// zs = A list of scalar Z values to split at. -function split_polygons_at_each_z(polys, zs, _i=0) = - assert( [for (poly=polys) if (!is_path(poly,3)) 1] == [], "Expects list of 3D paths.") - assert( is_vector(zs), "The split value list should contain only numbers." ) - _i>=len(zs)? polys : - split_polygons_at_each_z( - [ - for (poly = polys) - each _split_polygon_at_z(poly, zs[_i]) - ], zs, _i=_i+1 - ); - // Section: Convex Sets diff --git a/regions.scad b/regions.scad index 3355bb3..258b958 100644 --- a/regions.scad +++ b/regions.scad @@ -721,7 +721,7 @@ function offset( is_region(path)? ( assert(!return_faces, "return_faces not supported for regions.") let( - path = [for (p=path) polygon_is_clockwise(p)? p : reverse(p)], + path = [for (p=path) clockwise_polygon(p)], rgn = exclusive_or([for (p = path) [p]]), pathlist = sort(idx=0,[ for (i=[0:1:len(rgn)-1]) [ @@ -743,7 +743,7 @@ function offset( let( chamfer = is_def(r) ? false : chamfer, quality = max(0,round(quality)), - flip_dir = closed && !polygon_is_clockwise(path)? -1 : 1, + flip_dir = closed && !is_polygon_clockwise(path)? -1 : 1, d = flip_dir * (is_def(r) ? r : delta), shiftsegs = [for(i=[0:len(path)-1]) _shift_segment(select(path,i,i+1), d)], // good segments are ones where no point on the segment is less than distance d from any point on the path diff --git a/rounding.scad b/rounding.scad index 52fe7c4..27ff5ce 100644 --- a/rounding.scad +++ b/rounding.scad @@ -964,7 +964,7 @@ function offset_sweep( ["points", []], ], path = check_and_fix_path(path, [2], closed=true), - clockwise = polygon_is_clockwise(path), + clockwise = is_polygon_clockwise(path), dummy1 = _struct_valid(top,"offset_sweep","top"), dummy2 = _struct_valid(bottom,"offset_sweep","bottom"), top = struct_set(argspec, top, grow=false), @@ -1849,8 +1849,8 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b let( // Determine which points are concave by making bottom 2d if necessary bot_proj = len(bottom[0])==2 ? bottom : project_plane(select(bottom,0,2),bottom), - bottom_sign = polygon_is_clockwise(bot_proj) ? 1 : -1, - concave = [for(i=[0:N-1]) bottom_sign*sign(point_left_of_line2d(select(bot_proj,i+1), select(bot_proj, i-1,i)))>0], + bottom_sign = is_polygon_clockwise(bot_proj) ? 1 : -1, + concave = [for(i=[0:N-1]) bottom_sign*sign(_point_left_of_line2d(select(bot_proj,i+1), select(bot_proj, i-1,i)))>0], top = is_undef(top) ? path3d(bottom,height/2) : len(top[0])==2 ? path3d(top,height/2) : top, diff --git a/skin.scad b/skin.scad index 369e4e8..5e39c89 100644 --- a/skin.scad +++ b/skin.scad @@ -969,7 +969,7 @@ function path_sweep2d(shape, path, closed=false, caps, quality=1, style="min_edg assert(!closed || !caps, "Cannot make closed shape with caps") let( profile = ccw_polygon(shape), - flip = closed && polygon_is_clockwise(path) ? -1 : 1, + flip = closed && is_polygon_clockwise(path) ? -1 : 1, path = flip ? reverse(path) : path, proflist= transpose( [for(pt = profile) diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index e1b73c9..8dc44d8 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -43,9 +43,6 @@ test_circle_3points(); test_circle_point_tangents(); test_noncollinear_triple(); -test_pointlist_bounds(); -test_closest_point(); -test_furthest_point(); test_polygon_area(); test_is_convex_polygon(); test_polygon_shift(); @@ -54,7 +51,7 @@ test_reindex_polygon(); test_align_polygon(); test_centroid(); test_point_in_polygon(); -test_polygon_is_clockwise(); +test_is_polygon_clockwise(); test_clockwise_polygon(); test_ccw_polygon(); test_reverse_polygon(); @@ -848,69 +845,14 @@ module test_point_in_polygon() { *test_point_in_polygon(); -module test_pointlist_bounds() { - pts = [ - [-53,27,12], - [-63,97,36], - [84,-32,-5], - [63,-24,42], - [23,57,-42] - ]; - assert(pointlist_bounds(pts) == [[-63,-32,-42], [84,97,42]]); - pts2d = [ - [-53,12], - [-63,36], - [84,-5], - [63,42], - [23,-42] - ]; - assert(pointlist_bounds(pts2d) == [[-63,-42],[84,42]]); - pts5d = [ - [-53, 27, 12,-53, 12], - [-63, 97, 36,-63, 36], - [ 84,-32, -5, 84, -5], - [ 63,-24, 42, 63, 42], - [ 23, 57,-42, 23,-42] - ]; - assert(pointlist_bounds(pts5d) == [[-63,-32,-42,-63,-42],[84,97,42,84,42]]); - assert(pointlist_bounds([[3,4,5,6]]), [[3,4,5,6],[3,4,5,6]]); + +module test_is_polygon_clockwise() { + assert(is_polygon_clockwise([[-1,1],[1,1],[1,-1],[-1,-1]])); + assert(!is_polygon_clockwise([[1,1],[-1,1],[-1,-1],[1,-1]])); + assert(is_polygon_clockwise(circle(d=100))); + assert(is_polygon_clockwise(square(100))); } -*test_pointlist_bounds(); - - -module test_closest_point() { - ptlist = [for (i=count(100)) rands(-100,100,2,seed_value=8463+i)]; - testpts = [for (i=count(100)) rands(-100,100,2,seed_value=6834+i)]; - for (pt = testpts) { - pidx = closest_point(pt,ptlist); - dists = [for (p=ptlist) norm(pt-p)]; - mindist = min(dists); - assert(mindist == dists[pidx]); - } -} -*test_closest_point(); - - -module test_furthest_point() { - ptlist = [for (i=count(100)) rands(-100,100,2,seed_value=8463+i)]; - testpts = [for (i=count(100)) rands(-100,100,2,seed_value=6834+i)]; - for (pt = testpts) { - pidx = furthest_point(pt,ptlist); - dists = [for (p=ptlist) norm(pt-p)]; - mindist = max(dists); - assert(mindist == dists[pidx]); - } -} -*test_furthest_point(); - - -module test_polygon_is_clockwise() { - assert(polygon_is_clockwise([[-1,1],[1,1],[1,-1],[-1,-1]])); - assert(!polygon_is_clockwise([[1,1],[-1,1],[-1,-1],[1,-1]])); - assert(polygon_is_clockwise(circle(d=100))); - assert(polygon_is_clockwise(square(100))); -} -*test_polygon_is_clockwise(); +*test_is_polygon_clockwise(); module test_clockwise_polygon() { diff --git a/tests/test_vectors.scad b/tests/test_vectors.scad index e2c6759..f5d3098 100644 --- a/tests/test_vectors.scad +++ b/tests/test_vectors.scad @@ -207,7 +207,61 @@ module test_vector_nearest(){ } test_vector_nearest(); -cube(); + +module test_pointlist_bounds() { + pts = [ + [-53,27,12], + [-63,97,36], + [84,-32,-5], + [63,-24,42], + [23,57,-42] + ]; + assert(pointlist_bounds(pts) == [[-63,-32,-42], [84,97,42]]); + pts2d = [ + [-53,12], + [-63,36], + [84,-5], + [63,42], + [23,-42] + ]; + assert(pointlist_bounds(pts2d) == [[-63,-42],[84,42]]); + pts5d = [ + [-53, 27, 12,-53, 12], + [-63, 97, 36,-63, 36], + [ 84,-32, -5, 84, -5], + [ 63,-24, 42, 63, 42], + [ 23, 57,-42, 23,-42] + ]; + assert(pointlist_bounds(pts5d) == [[-63,-32,-42,-63,-42],[84,97,42,84,42]]); + assert(pointlist_bounds([[3,4,5,6]]), [[3,4,5,6],[3,4,5,6]]); +} +test_pointlist_bounds(); + + +module test_closest_point() { + ptlist = [for (i=count(100)) rands(-100,100,2,seed_value=8463+i)]; + testpts = [for (i=count(100)) rands(-100,100,2,seed_value=6834+i)]; + for (pt = testpts) { + pidx = closest_point(pt,ptlist); + dists = [for (p=ptlist) norm(pt-p)]; + mindist = min(dists); + assert(mindist == dists[pidx]); + } +} +test_closest_point(); + + +module test_furthest_point() { + ptlist = [for (i=count(100)) rands(-100,100,2,seed_value=8463+i)]; + testpts = [for (i=count(100)) rands(-100,100,2,seed_value=6834+i)]; + for (pt = testpts) { + pidx = furthest_point(pt,ptlist); + dists = [for (p=ptlist) norm(pt-p)]; + mindist = max(dists); + assert(mindist == dists[pidx]); + } +} +test_furthest_point(); // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/transforms.scad b/transforms.scad index 67de0e4..c14e0ad 100644 --- a/transforms.scad +++ b/transforms.scad @@ -603,183 +603,6 @@ module zrot(a=0, p, cp) function zrot(a=0, p, cp) = rot(a, cp=cp, p=p); -// Function&Module: xyrot() -// -// Usage: As Module -// xyrot(a, [cp=]) ... -// Usage: As a Function to rotate points -// rotated = xyrot(a, p, [cp=]); -// Usage: As a Function to get rotation matrix -// mat = xyrot(a, [cp=]); -// -// Topics: Affine, Matrices, Transforms, Rotation -// See Also: rot(), xrot(), yrot(), zrot(), xzrot(), yzrot(), xyzrot(), affine3d_rot_by_axis() -// -// Description: -// Rotates around the [1,1,0] vector axis by the given number of degrees. If `cp` is given, rotations are performed around that centerpoint. -// * Called as a module, rotates all children. -// * Called as a function with a `p` argument containing a point, returns the rotated point. -// * Called as a function with a `p` argument containing a list of points, returns the list of rotated points. -// * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the rotated patch. -// * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the rotated VNF. -// * Called as a function without a `p` argument, returns the affine3d rotational matrix. -// -// Arguments: -// a = angle to rotate by in degrees. -// p = If called as a function, this contains data to rotate: a point, list of points, bezier patch or VNF. -// --- -// cp = centerpoint to rotate around. Default: [0,0,0] -// -// Example: -// #cylinder(h=50, r=10, center=true); -// xyrot(90) cylinder(h=50, r=10, center=true); -module xyrot(a=0, p, cp) -{ - assert(is_undef(p), "Module form `xyrot()` does not accept p= argument."); - if (a==0) { - children(); // May be slightly faster? - } else { - mat = xyrot(a=a, cp=cp); - multmatrix(mat) children(); - } -} - -function xyrot(a=0, p, cp) = rot(a=a, v=[1,1,0], cp=cp, p=p); - - -// Function&Module: xzrot() -// -// Usage: As Module -// xzrot(a, [cp=]) ... -// Usage: As Function to rotate points -// rotated = xzrot(a, p, [cp=]); -// Usage: As Function to return rotation matrix -// mat = xzrot(a, [cp=]); -// -// Topics: Affine, Matrices, Transforms, Rotation -// See Also: rot(), xrot(), yrot(), zrot(), xyrot(), yzrot(), xyzrot(), affine3d_rot_by_axis() -// -// Description: -// Rotates around the [1,0,1] vector axis by the given number of degrees. If `cp` is given, rotations are performed around that centerpoint. -// * Called as a module, rotates all children. -// * Called as a function with a `p` argument containing a point, returns the rotated point. -// * Called as a function with a `p` argument containing a list of points, returns the list of rotated points. -// * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the rotated patch. -// * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the rotated VNF. -// * Called as a function without a `p` argument, returns the affine3d rotational matrix. -// -// Arguments: -// a = angle to rotate by in degrees. -// p = If called as a function, this contains data to rotate: a point, list of points, bezier patch or VNF. -// --- -// cp = centerpoint to rotate around. Default: [0,0,0] -// -// Example: -// #cylinder(h=50, r=10, center=true); -// xzrot(90) cylinder(h=50, r=10, center=true); -module xzrot(a=0, p, cp) -{ - assert(is_undef(p), "Module form `xzrot()` does not accept p= argument."); - if (a==0) { - children(); // May be slightly faster? - } else { - mat = xzrot(a=a, cp=cp); - multmatrix(mat) children(); - } -} - -function xzrot(a=0, p, cp) = rot(a=a, v=[1,0,1], cp=cp, p=p); - - -// Function&Module: yzrot() -// -// Usage: As Module -// yzrot(a, [cp=]) ... -// Usage: As Function to rotate points -// rotated = yzrot(a, p, [cp=]); -// Usage: As Function to return rotation matrix -// mat = yzrot(a, [cp=]); -// -// Topics: Affine, Matrices, Transforms, Rotation -// See Also: rot(), xrot(), yrot(), zrot(), xyrot(), xzrot(), xyzrot(), affine3d_rot_by_axis() -// -// Description: -// Rotates around the [0,1,1] vector axis by the given number of degrees. If `cp` is given, rotations are performed around that centerpoint. -// * Called as a module, rotates all children. -// * Called as a function with a `p` argument containing a point, returns the rotated point. -// * Called as a function with a `p` argument containing a list of points, returns the list of rotated points. -// * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the rotated patch. -// * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the rotated VNF. -// * Called as a function without a `p` argument, returns the affine3d rotational matrix. -// -// Arguments: -// a = angle to rotate by in degrees. -// p = If called as a function, this contains data to rotate: a point, list of points, bezier patch or VNF. -// --- -// cp = centerpoint to rotate around. Default: [0,0,0] -// -// Example: -// #cylinder(h=50, r=10, center=true); -// yzrot(90) cylinder(h=50, r=10, center=true); -module yzrot(a=0, p, cp) -{ - assert(is_undef(p), "Module form `yzrot()` does not accept p= argument."); - if (a==0) { - children(); // May be slightly faster? - } else { - mat = yzrot(a=a, cp=cp); - multmatrix(mat) children(); - } -} - -function yzrot(a=0, p, cp) = rot(a=a, v=[0,1,1], cp=cp, p=p); - - -// Function&Module: xyzrot() -// -// Usage: As Module -// xyzrot(a, [cp=]) ... -// Usage: As Function to rotate points -// rotated = xyzrot(a, p, [cp=]); -// Usage: As Function to return rotation matrix -// mat = xyzrot(a, [cp=]); -// -// Topics: Affine, Matrices, Transforms, Rotation -// See Also: rot(), xrot(), yrot(), zrot(), xyrot(), xzrot(), yzrot(), affine3d_rot_by_axis() -// -// Description: -// Rotates around the [1,1,1] vector axis by the given number of degrees. If `cp` is given, rotations are performed around that centerpoint. -// * Called as a module, rotates all children. -// * Called as a function with a `p` argument containing a point, returns the rotated point. -// * Called as a function with a `p` argument containing a list of points, returns the list of rotated points. -// * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the rotated patch. -// * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the rotated VNF. -// * Called as a function without a `p` argument, returns the affine3d rotational matrix. -// -// Arguments: -// a = angle to rotate by in degrees. -// p = If called as a function, this contains data to rotate: a point, list of points, bezier patch or VNF. -// --- -// cp = centerpoint to rotate around. Default: [0,0,0] -// -// Example: -// #cylinder(h=50, r=10, center=true); -// xyzrot(90) cylinder(h=50, r=10, center=true); -module xyzrot(a=0, p, cp) -{ - assert(is_undef(p), "Module form `xyzrot()` does not accept p= argument."); - if (a==0) { - children(); // May be slightly faster? - } else { - mat = xyzrot(a=a, cp=cp); - multmatrix(mat) children(); - } -} - -function xyzrot(a=0, p, cp) = rot(a=a, v=[1,1,1], cp=cp, p=p); - - - ////////////////////////////////////////////////////////////////////// // Section: Scaling and Mirroring @@ -1265,189 +1088,6 @@ function zflip(p, z=0) = move([0,0,z],p=mirror([0,0,1],p=move([0,0,-z],p=p))); -// Function&Module: xyflip() -// -// Usage: As Module -// xyflip([cp]) ... -// Usage: As Function -// pt = xyflip(p, [cp]); -// Usage: Get Affine Matrix -// pt = xyflip([cp], [planar=]); -// -// Topics: Affine, Matrices, Transforms, Reflection, Mirroring -// See Also: mirror(), xflip(), yflip(), zflip(), xzflip(), yzflip(), affine2d_mirror(), affine3d_mirror() -// -// Description: -// Mirrors/reflects across the origin [0,0,0], along the reflection plane where X=Y. If `cp` is given, the reflection plane passes through that point -// * Called as the built-in module, mirrors all children across the line/plane. -// * Called as a function with a point in the `p` argument, returns the point mirrored across the line/plane. -// * Called as a function with a list of points in the `p` argument, returns the list of points, with each one mirrored across the line/plane. -// * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the mirrored patch. -// * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the mirrored VNF. -// * Called as a function without a `p` argument, and `planer=true`, returns the affine2d 3x3 mirror matrix. -// * Called as a function without a `p` argument, and `planar=false`, returns the affine3d 4x4 mirror matrix. -// -// Arguments: -// p = If given, the point, path, patch, or VNF to mirror. Function use only. -// cp = The centerpoint of the plane of reflection, given either as a point, or as a scalar distance away from the origin. -// --- -// planar = If true, and p is not given, returns a 2D affine transformation matrix. Function use only. Default: False -// -// Example(2D): -// xyflip() text("Foobar", size=20, halign="center"); -// -// Example: -// left(10) frame_ref(); -// right(10) xyflip() frame_ref(); -// -// Example: -// xyflip(cp=-15) frame_ref(); -// -// Example: -// xyflip(cp=[10,10,10]) frame_ref(); -// -// Example: Called as Function for a 3D matrix -// mat = xyflip(); -// multmatrix(mat) frame_ref(); -// -// Example(2D): Called as Function for a 2D matrix -// mat = xyflip(planar=true); -// multmatrix(mat) text("Foobar", size=20, halign="center"); -module xyflip(p, cp=0, planar) { - assert(is_undef(p), "Module form `xyflip()` does not accept p= argument."); - assert(is_undef(planar), "Module form `xyflip()` does not accept planar= argument."); - mat = xyflip(cp=cp); - multmatrix(mat) children(); -} - -function xyflip(p, cp=0, planar=false) = - assert(is_finite(cp) || is_vector(cp)) - let( - v = unit([-1,1,0]), - n = planar? point2d(v) : v - ) - cp == 0 || cp==[0,0,0]? mirror(n, p=p) : - let( - cp = is_finite(cp)? n * cp : - is_vector(cp)? assert(len(cp) == len(n)) cp : - assert(is_finite(cp) || is_vector(cp)), - mat = move(cp) * mirror(n) * move(-cp) - ) is_undef(p)? mat : apply(mat, p); - - -// Function&Module: xzflip() -// -// Usage: As Module -// xzflip([cp]) ... -// Usage: As Function -// pt = xzflip([cp], p); -// Usage: Get Affine Matrix -// pt = xzflip([cp]); -// -// Topics: Affine, Matrices, Transforms, Reflection, Mirroring -// See Also: mirror(), xflip(), yflip(), zflip(), xyflip(), yzflip(), affine2d_mirror(), affine3d_mirror() -// -// Description: -// Mirrors/reflects across the origin [0,0,0], along the reflection plane where X=Y. If `cp` is given, the reflection plane passes through that point -// * Called as the built-in module, mirrors all children across the line/plane. -// * Called as a function with a point in the `p` argument, returns the point mirrored across the line/plane. -// * Called as a function with a list of points in the `p` argument, returns the list of points, with each one mirrored across the line/plane. -// * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the mirrored patch. -// * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the mirrored VNF. -// * Called as a function without a `p` argument, returns the affine3d 4x4 mirror matrix. -// -// Arguments: -// p = If given, the point, path, patch, or VNF to mirror. Function use only. -// cp = The centerpoint of the plane of reflection, given either as a point, or as a scalar distance away from the origin. -// -// Example: -// left(10) frame_ref(); -// right(10) xzflip() frame_ref(); -// -// Example: -// xzflip(cp=-15) frame_ref(); -// -// Example: -// xzflip(cp=[10,10,10]) frame_ref(); -// -// Example: Called as Function -// mat = xzflip(); -// multmatrix(mat) frame_ref(); -module xzflip(p, cp=0) { - assert(is_undef(p), "Module form `xzflip()` does not accept p= argument."); - mat = xzflip(cp=cp); - multmatrix(mat) children(); -} - -function xzflip(p, cp=0) = - assert(is_finite(cp) || is_vector(cp)) - let( n = unit([-1,0,1]) ) - cp == 0 || cp==[0,0,0]? mirror(n, p=p) : - let( - cp = is_finite(cp)? n * cp : - is_vector(cp,3)? cp : - assert(is_finite(cp) || is_vector(cp,3)), - mat = move(cp) * mirror(n) * move(-cp) - ) is_undef(p)? mat : apply(mat, p); - - -// Function&Module: yzflip() -// -// Usage: As Module -// yzflip([x=]) ... -// Usage: As Function -// pt = yzflip(p, [x=]); -// Usage: Get Affine Matrix -// pt = yzflip([x=]); -// -// Topics: Affine, Matrices, Transforms, Reflection, Mirroring -// See Also: mirror(), xflip(), yflip(), zflip(), xyflip(), xzflip(), affine2d_mirror(), affine3d_mirror() -// -// Description: -// Mirrors/reflects across the origin [0,0,0], along the reflection plane where X=Y. If `cp` is given, the reflection plane passes through that point -// * Called as the built-in module, mirrors all children across the line/plane. -// * Called as a function with a point in the `p` argument, returns the point mirrored across the line/plane. -// * Called as a function with a list of points in the `p` argument, returns the list of points, with each one mirrored across the line/plane. -// * Called as a function with a [bezier patch](beziers.scad) in the `p` argument, returns the mirrored patch. -// * Called as a function with a [VNF structure](vnf.scad) in the `p` argument, returns the mirrored VNF. -// * Called as a function without a `p` argument, returns the affine3d 4x4 mirror matrix. -// -// Arguments: -// p = If given, the point, path, patch, or VNF to mirror. Function use only. -// cp = The centerpoint of the plane of reflection, given either as a point, or as a scalar distance away from the origin. -// -// Example: -// left(10) frame_ref(); -// right(10) yzflip() frame_ref(); -// -// Example: -// yzflip(cp=-15) frame_ref(); -// -// Example: -// yzflip(cp=[10,10,10]) frame_ref(); -// -// Example: Called as Function -// mat = yzflip(); -// multmatrix(mat) frame_ref(); -module yzflip(p, cp=0) { - assert(is_undef(p), "Module form `yzflip()` does not accept p= argument."); - mat = yzflip(cp=cp); - multmatrix(mat) children(); -} - -function yzflip(p, cp=0) = - assert(is_finite(cp) || is_vector(cp)) - let( n = unit([0,-1,1]) ) - cp == 0 || cp==[0,0,0]? mirror(n, p=p) : - let( - cp = is_finite(cp)? n * cp : - is_vector(cp,3)? cp : - assert(is_finite(cp) || is_vector(cp,3)), - mat = move(cp) * mirror(n) * move(-cp) - ) is_undef(p)? mat : apply(mat, p); - - - ////////////////////////////////////////////////////////////////////// // Section: Other Transformations ////////////////////////////////////////////////////////////////////// diff --git a/vectors.scad b/vectors.scad index 25498b1..a2fe32b 100644 --- a/vectors.scad +++ b/vectors.scad @@ -132,6 +132,28 @@ function v_lookup(x, v) = lerp(lo,hi,u); +// Function: pointlist_bounds() +// Usage: +// pt_pair = pointlist_bounds(pts); +// Topics: Geometry, Bounding Boxes, Bounds +// Description: +// Finds the bounds containing all the points in `pts` which can be a list of points in any dimension. +// Returns a list of two items: a list of the minimums and a list of the maximums. For example, with +// 3d points `[[MINX, MINY, MINZ], [MAXX, MAXY, MAXZ]]` +// Arguments: +// pts = List of points. +function pointlist_bounds(pts) = + assert(is_path(pts,dim=undef,fast=true) , "Invalid pointlist." ) + let( + select = ident(len(pts[0])), + spread = [ + for(i=[0:len(pts[0])-1]) + let( spreadi = pts*select[i] ) + [ min(spreadi), max(spreadi) ] + ] + ) transpose(spread); + + // Function: unit() // Usage: // unit(v, [error]); @@ -241,9 +263,41 @@ function vector_axis(v1,v2=undef,v3=undef) = + // Section: Vector Searching + +// Function: closest_point() +// Usage: +// index = closest_point(pt, points); +// Topics: Geometry, Points, Distance +// Description: +// Given a list of `points`, finds the index of the closest point to `pt`. +// Arguments: +// pt = The point to find the closest point to. +// points = The list of points to search. +function closest_point(pt, points) = + assert( is_vector(pt), "Invalid point." ) + assert(is_path(points,dim=len(pt)), "Invalid pointlist or incompatible dimensions." ) + min_index([for (p=points) norm(p-pt)]); + + +// Function: furthest_point() +// Usage: +// index = furthest_point(pt, points); +// Topics: Geometry, Points, Distance +// Description: +// Given a list of `points`, finds the index of the furthest point from `pt`. +// Arguments: +// pt = The point to find the farthest point from. +// points = The list of points to search. +function furthest_point(pt, points) = + assert( is_vector(pt), "Invalid point." ) + assert(is_path(points,dim=len(pt)), "Invalid pointlist or incompatible dimensions." ) + max_index([for (p=points) norm(p-pt)]); + + // Function: vector_search() // Usage: // indices = vector_search(query, r, target); diff --git a/vnf.scad b/vnf.scad index f10fc0c..e59b328 100644 --- a/vnf.scad +++ b/vnf.scad @@ -694,8 +694,8 @@ function vnf_bend(vnf,r,d,axis="Z") = [for(i = [1:1:steps-1]) i*step+bmin.x], facepolys = [for (face=vnf[1]) select(verts,face)], splits = axis=="X"? - split_polygons_at_each_y(facepolys, bend_at) : - split_polygons_at_each_x(facepolys, bend_at), + _split_polygons_at_each_y(facepolys, bend_at) : + _split_polygons_at_each_x(facepolys, bend_at), newtris = _triangulate_planar_convex_polygons(splits), bent_faces = [ for (tri = newtris) [ @@ -712,6 +712,160 @@ function vnf_bend(vnf,r,d,axis="Z") = ) vnf_add_faces(faces=bent_faces); + +function _split_polygon_at_x(poly, x) = + let( + xs = subindex(poly,0) + ) (min(xs) >= x || max(xs) <= x)? [poly] : + let( + poly2 = [ + for (p = pair(poly,true)) each [ + p[0], + if( + (p[0].x < x && p[1].x > x) || + (p[1].x < x && p[0].x > x) + ) let( + u = (x - p[0].x) / (p[1].x - p[0].x) + ) [ + x, // Important for later exact match tests + u*(p[1].y-p[0].y)+p[0].y, + u*(p[1].z-p[0].z)+p[0].z, + ] + ] + ], + out1 = [for (p = poly2) if(p.x <= x) p], + out2 = [for (p = poly2) if(p.x >= x) p], + out3 = [ + if (len(out1)>=3) each split_path_at_self_crossings(out1), + if (len(out2)>=3) each split_path_at_self_crossings(out2), + ], + out = [for (p=out3) if (len(p) > 2) cleanup_path(p)] + ) out; + + +function _split_polygon_at_y(poly, y) = + let( + ys = subindex(poly,1) + ) (min(ys) >= y || max(ys) <= y)? [poly] : + let( + poly2 = [ + for (p = pair(poly,true)) each [ + p[0], + if( + (p[0].y < y && p[1].y > y) || + (p[1].y < y && p[0].y > y) + ) let( + u = (y - p[0].y) / (p[1].y - p[0].y) + ) [ + u*(p[1].x-p[0].x)+p[0].x, + y, // Important for later exact match tests + u*(p[1].z-p[0].z)+p[0].z, + ] + ] + ], + out1 = [for (p = poly2) if(p.y <= y) p], + out2 = [for (p = poly2) if(p.y >= y) p], + out3 = [ + if (len(out1)>=3) each split_path_at_self_crossings(out1), + if (len(out2)>=3) each split_path_at_self_crossings(out2), + ], + out = [for (p=out3) if (len(p) > 2) cleanup_path(p)] + ) out; + + +function _split_polygon_at_z(poly, z) = + let( + zs = subindex(poly,2) + ) (min(zs) >= z || max(zs) <= z)? [poly] : + let( + poly2 = [ + for (p = pair(poly,true)) each [ + p[0], + if( + (p[0].z < z && p[1].z > z) || + (p[1].z < z && p[0].z > z) + ) let( + u = (z - p[0].z) / (p[1].z - p[0].z) + ) [ + u*(p[1].x-p[0].x)+p[0].x, + u*(p[1].y-p[0].y)+p[0].y, + z, // Important for later exact match tests + ] + ] + ], + out1 = [for (p = poly2) if(p.z <= z) p], + out2 = [for (p = poly2) if(p.z >= z) p], + out3 = [ + if (len(out1)>=3) each split_path_at_self_crossings(close_path(out1), closed=false), + if (len(out2)>=3) each split_path_at_self_crossings(close_path(out2), closed=false), + ], + out = [for (p=out3) if (len(p) > 2) cleanup_path(p)] + ) out; + + +/// Function: _split_polygons_at_each_x() +// Usage: +// splitpolys = split_polygons_at_each_x(polys, xs); +/// Topics: Geometry, Polygons, Intersections +// Description: +// Given a list of 3D polygons, splits all of them wherever they cross any X value given in `xs`. +// Arguments: +// polys = A list of 3D polygons to split. +// xs = A list of scalar X values to split at. +function _split_polygons_at_each_x(polys, xs, _i=0) = + assert( [for (poly=polys) if (!is_path(poly,3)) 1] == [], "Expects list of 3D paths.") + assert( is_vector(xs), "The split value list should contain only numbers." ) + _i>=len(xs)? polys : + split_polygons_at_each_x( + [ + for (poly = polys) + each _split_polygon_at_x(poly, xs[_i]) + ], xs, _i=_i+1 + ); + + +///Internal Function: _split_polygons_at_each_y() +// Usage: +// splitpolys = _split_polygons_at_each_y(polys, ys); +/// Topics: Geometry, Polygons, Intersections +// Description: +// Given a list of 3D polygons, splits all of them wherever they cross any Y value given in `ys`. +// Arguments: +// polys = A list of 3D polygons to split. +// ys = A list of scalar Y values to split at. +function _split_polygons_at_each_y(polys, ys, _i=0) = + assert( [for (poly=polys) if (!is_path(poly,3)) 1] == [], "Expects list of 3D paths.") + assert( is_vector(ys), "The split value list should contain only numbers." ) + _i>=len(ys)? polys : + split_polygons_at_each_y( + [ + for (poly = polys) + each _split_polygon_at_y(poly, ys[_i]) + ], ys, _i=_i+1 + ); + + +/// Internal Function: _split_polygons_at_each_z() +// Usage: +// splitpolys = split_polygons_at_each_z(polys, zs); +/// Topics: Geometry, Polygons, Intersections +// Description: +// Given a list of 3D polygons, splits all of them wherever they cross any Z value given in `zs`. +// Arguments: +// polys = A list of 3D polygons to split. +// zs = A list of scalar Z values to split at. +function split_polygons_at_each_z(polys, zs, _i=0) = + assert( [for (poly=polys) if (!is_path(poly,3)) 1] == [], "Expects list of 3D paths.") + assert( is_vector(zs), "The split value list should contain only numbers." ) + _i>=len(zs)? polys : + split_polygons_at_each_z( + [ + for (poly = polys) + each _split_polygon_at_z(poly, zs[_i]) + ], zs, _i=_i+1 + ); + + // Function&Module: vnf_validate() // Usage: As Function // fails = vnf_validate(vnf);