From 80e6cf63168f71c8d9b4c5cb1464e7ac954c45bf Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 25 Mar 2021 01:57:07 +0000 Subject: [PATCH 01/17] Minor corrections Corrections and simplifications of polygon_area(), centroid() and point_in_polygon. --- geometry.scad | 36 +++++++++++++++++++----------------- tests/test_geometry.scad | 17 ++++++++++------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/geometry.scad b/geometry.scad index 3e3525d..968a291 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1670,26 +1670,26 @@ function furthest_point(pt, points) = // area = polygon_area(poly); // Description: // Given a 2D or 3D planar polygon, returns the area of that polygon. -// If the polygon is self-crossing, the results are undefined. For non-planar points the result is undef. -// When `signed` is true, a signed area is returned; a positive area indicates a counterclockwise polygon. +// If the polygon is self-crossing, the results are undefined. For non-planar 3D polygon the result is undef. +// When `signed` is true, a signed area is returned; a positive area indicates a clockwise polygon. // Arguments: // poly = polygon to compute the area of. // signed = if true, a signed area is returned (default: false) function polygon_area(poly, signed=false) = assert(is_path(poly), "Invalid polygon." ) len(poly)<3 ? 0 : - let( cpoly = close_path(simplify_path(poly)) ) len(poly[0])==2 - ? sum([for(i=[1:1:len(poly)-2]) cross(poly[i]-poly[0],poly[i+1]-poly[0]) ])/2 + ? let( total = sum([for(i=[1:1:len(poly)-2]) cross(poly[i]-poly[0],poly[i+1]-poly[0]) ])/2 ) + signed ? total : abs(total) : let( plane = plane_from_points(poly) ) plane==undef? undef : let( - n = unit(plane_normal(plane)), + n = plane_normal(plane), total = sum([ - for(i=[1:1:len(cpoly)-2]) + for(i=[1:1:len(poly)-2]) let( - v1 = cpoly[i] - cpoly[0], - v2 = cpoly[i+1] - cpoly[0] + v1 = poly[i] - poly[0], + v2 = poly[i+1] - poly[0] ) cross(v1,v2) * n ])/2 @@ -1853,7 +1853,9 @@ function centroid(poly) = for(i=[0:len(poly)-1]) let(segment=select(poly,i,i+1)) det2(segment)*sum(segment) - ]) / 6 / polygon_area(poly) + ]) / 6 / polygon_area(poly,signed=true) +// polygon_area(concat([[0,0]],segment),signed=true)*sum(segment) +// ]) / 3 / polygon_area(poly,signed=true) : let( plane = plane_from_points(poly, fast=true) ) assert( !is_undef(plane), "The polygon must be planar." ) let( @@ -1879,7 +1881,7 @@ function centroid(poly) = // 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 polyon. +// 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 can have self-intersections. @@ -1890,7 +1892,7 @@ function centroid(poly) = // 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 = Acceptable variance. Default: `EPSILON` (1e-9) -function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = +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." ) @@ -1923,12 +1925,12 @@ function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = p0 = poly[i]-point, p1 = poly[(i+1)%n]-point ) - if( ( (p1.y>eps && p0.y<=0) || (p1.y<=0 && p0.y>eps) ) - && 0 < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y) ) + 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_is_clockwise() @@ -1979,7 +1981,7 @@ function reverse_polygon(poly) = // n = polygon_normal(poly); // Description: // Given a 3D planar polygon, returns a unit-length normal vector for the -// clockwise orientation of the polygon. +// clockwise orientation of the polygon. If the polygon points are collinear, returns `undef`. function polygon_normal(poly) = assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( @@ -1989,7 +1991,7 @@ function polygon_normal(poly) = for (i=[1:1:len(poly)-2]) cross(poly[i+1]-p0, poly[i]-p0) ]) - ) unit(n); + ) unit(n,undef); function _split_polygon_at_x(poly, x) = diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 63a1966..28ddc69 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -842,7 +842,7 @@ module test_cleanup_path() { module test_polygon_area() { assert(approx(polygon_area([[1,1],[-1,1],[-1,-1],[1,-1]]), 4)); - assert(approx(polygon_area(circle(r=50,$fn=1000)), -PI*50*50, eps=0.1)); + assert(approx(polygon_area(circle(r=50,$fn=1000),signed=true), -PI*50*50, eps=0.1)); } *test_polygon_area(); @@ -914,7 +914,7 @@ module test_noncollinear_triple() { module test_centroid() { $fn = 24; assert_approx(centroid(circle(d=100)), [0,0]); - assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [20,0]); + assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); poly = [for(a=[0:90:360]) move([1,2.5,3.1], rot(p=[cos(a),sin(a),0],from=[0,0,1],to=[1,1,1])) ]; @@ -943,19 +943,22 @@ module test_point_in_polygon() { poly2 = [ [-3,-3],[2,-3],[2,1],[-1,1],[-1,-1],[1,-1],[1,2],[-3,2] ]; assert(point_in_polygon([0,0], poly) == 1); assert(point_in_polygon([20,0], poly) == -1); - assert(point_in_polygon([20,0], poly,EPSILON,nonzero=false) == -1); + assert(point_in_polygon([20,0], poly,nonzero=false) == -1); assert(point_in_polygon([5,5], poly) == 1); assert(point_in_polygon([-5,5], poly) == 1); assert(point_in_polygon([-5,-5], poly) == 1); assert(point_in_polygon([5,-5], poly) == 1); - assert(point_in_polygon([5,-5], poly,EPSILON,nonzero=false) == 1); + assert(point_in_polygon([5,-5], poly,nonzero=false,eps=EPSILON) == 1); assert(point_in_polygon([-10,-10], poly) == -1); assert(point_in_polygon([10,0], poly) == 0); assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); - assert(point_in_polygon([0,-10], poly,EPSILON,nonzero=false) == 0); - assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=true) == 1); - assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=false) == -1); + assert(point_in_polygon([0,-10], poly,nonzero=false) == 0); + assert(point_in_polygon([0,0], poly2,nonzero=true) == 1); + assert(point_in_polygon([0,1], poly2,nonzero=true) == 0); + assert(point_in_polygon([0,1], poly2,nonzero=false) == 0); + assert(point_in_polygon([1,0], poly2,nonzero=false) == 0); + assert(point_in_polygon([0,0], poly2,nonzero=false,eps=EPSILON) == -1); } *test_point_in_polygon(); From f54be543797293c9dfba57ab4df4bd257ce6f0b9 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 25 Mar 2021 02:18:33 +0000 Subject: [PATCH 02/17] Minor edition --- geometry.scad | 2 -- 1 file changed, 2 deletions(-) diff --git a/geometry.scad b/geometry.scad index 968a291..060b5cd 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1854,8 +1854,6 @@ function centroid(poly) = let(segment=select(poly,i,i+1)) det2(segment)*sum(segment) ]) / 6 / polygon_area(poly,signed=true) -// polygon_area(concat([[0,0]],segment),signed=true)*sum(segment) -// ]) / 3 / polygon_area(poly,signed=true) : let( plane = plane_from_points(poly, fast=true) ) assert( !is_undef(plane), "The polygon must be planar." ) let( From 097c18b804073bd8fd67bdf00d4f2d5d7dc94e76 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 25 Mar 2021 08:36:43 +0000 Subject: [PATCH 03/17] Revert "Minor edition" This reverts commit f54be543797293c9dfba57ab4df4bd257ce6f0b9. --- geometry.scad | 2 ++ 1 file changed, 2 insertions(+) diff --git a/geometry.scad b/geometry.scad index 060b5cd..968a291 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1854,6 +1854,8 @@ function centroid(poly) = let(segment=select(poly,i,i+1)) det2(segment)*sum(segment) ]) / 6 / polygon_area(poly,signed=true) +// polygon_area(concat([[0,0]],segment),signed=true)*sum(segment) +// ]) / 3 / polygon_area(poly,signed=true) : let( plane = plane_from_points(poly, fast=true) ) assert( !is_undef(plane), "The polygon must be planar." ) let( From da7f9a2412acd2d87711dcc15a85fc4ae8c76aff Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 25 Mar 2021 08:36:57 +0000 Subject: [PATCH 04/17] Revert "Minor corrections" This reverts commit 80e6cf63168f71c8d9b4c5cb1464e7ac954c45bf. --- geometry.scad | 36 +++++++++++++++++------------------- tests/test_geometry.scad | 17 +++++++---------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/geometry.scad b/geometry.scad index 968a291..3e3525d 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1670,26 +1670,26 @@ function furthest_point(pt, points) = // area = polygon_area(poly); // Description: // Given a 2D or 3D planar polygon, returns the area of that polygon. -// If the polygon is self-crossing, the results are undefined. For non-planar 3D polygon the result is undef. -// When `signed` is true, a signed area is returned; a positive area indicates a clockwise polygon. +// If the polygon is self-crossing, the results are undefined. For non-planar points the result is undef. +// When `signed` is true, a signed area is returned; a positive area indicates a counterclockwise polygon. // Arguments: // poly = polygon to compute the area of. // signed = if true, a signed area is returned (default: false) function polygon_area(poly, signed=false) = assert(is_path(poly), "Invalid polygon." ) len(poly)<3 ? 0 : + let( cpoly = close_path(simplify_path(poly)) ) len(poly[0])==2 - ? let( total = sum([for(i=[1:1:len(poly)-2]) cross(poly[i]-poly[0],poly[i+1]-poly[0]) ])/2 ) - signed ? total : abs(total) + ? sum([for(i=[1:1:len(poly)-2]) cross(poly[i]-poly[0],poly[i+1]-poly[0]) ])/2 : let( plane = plane_from_points(poly) ) plane==undef? undef : let( - n = plane_normal(plane), + n = unit(plane_normal(plane)), total = sum([ - for(i=[1:1:len(poly)-2]) + for(i=[1:1:len(cpoly)-2]) let( - v1 = poly[i] - poly[0], - v2 = poly[i+1] - poly[0] + v1 = cpoly[i] - cpoly[0], + v2 = cpoly[i+1] - cpoly[0] ) cross(v1,v2) * n ])/2 @@ -1853,9 +1853,7 @@ function centroid(poly) = for(i=[0:len(poly)-1]) let(segment=select(poly,i,i+1)) det2(segment)*sum(segment) - ]) / 6 / polygon_area(poly,signed=true) -// polygon_area(concat([[0,0]],segment),signed=true)*sum(segment) -// ]) / 3 / polygon_area(poly,signed=true) + ]) / 6 / polygon_area(poly) : let( plane = plane_from_points(poly, fast=true) ) assert( !is_undef(plane), "The polygon must be planar." ) let( @@ -1881,7 +1879,7 @@ function centroid(poly) = // 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 -1 if the point is outside the polyon. // 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 can have self-intersections. @@ -1892,7 +1890,7 @@ function centroid(poly) = // 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 = Acceptable variance. Default: `EPSILON` (1e-9) -function point_in_polygon(point, poly, nonzero=true, eps=EPSILON) = +function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = // 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." ) @@ -1925,12 +1923,12 @@ function point_in_polygon(point, poly, nonzero=true, eps=EPSILON) = 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) ) + if( ( (p1.y>eps && p0.y<=0) || (p1.y<=0 && p0.y>eps) ) + && 0 < 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_is_clockwise() @@ -1981,7 +1979,7 @@ function reverse_polygon(poly) = // n = polygon_normal(poly); // Description: // Given a 3D planar polygon, returns a unit-length normal vector for the -// clockwise orientation of the polygon. If the polygon points are collinear, returns `undef`. +// clockwise orientation of the polygon. function polygon_normal(poly) = assert(is_path(poly,dim=3), "Invalid 3D polygon." ) let( @@ -1991,7 +1989,7 @@ function polygon_normal(poly) = for (i=[1:1:len(poly)-2]) cross(poly[i+1]-p0, poly[i]-p0) ]) - ) unit(n,undef); + ) unit(n); function _split_polygon_at_x(poly, x) = diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 28ddc69..63a1966 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -842,7 +842,7 @@ module test_cleanup_path() { module test_polygon_area() { assert(approx(polygon_area([[1,1],[-1,1],[-1,-1],[1,-1]]), 4)); - assert(approx(polygon_area(circle(r=50,$fn=1000),signed=true), -PI*50*50, eps=0.1)); + assert(approx(polygon_area(circle(r=50,$fn=1000)), -PI*50*50, eps=0.1)); } *test_polygon_area(); @@ -914,7 +914,7 @@ module test_noncollinear_triple() { module test_centroid() { $fn = 24; assert_approx(centroid(circle(d=100)), [0,0]); - assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); + assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); poly = [for(a=[0:90:360]) move([1,2.5,3.1], rot(p=[cos(a),sin(a),0],from=[0,0,1],to=[1,1,1])) ]; @@ -943,22 +943,19 @@ module test_point_in_polygon() { poly2 = [ [-3,-3],[2,-3],[2,1],[-1,1],[-1,-1],[1,-1],[1,2],[-3,2] ]; assert(point_in_polygon([0,0], poly) == 1); assert(point_in_polygon([20,0], poly) == -1); - assert(point_in_polygon([20,0], poly,nonzero=false) == -1); + assert(point_in_polygon([20,0], poly,EPSILON,nonzero=false) == -1); assert(point_in_polygon([5,5], poly) == 1); assert(point_in_polygon([-5,5], poly) == 1); assert(point_in_polygon([-5,-5], poly) == 1); assert(point_in_polygon([5,-5], poly) == 1); - assert(point_in_polygon([5,-5], poly,nonzero=false,eps=EPSILON) == 1); + assert(point_in_polygon([5,-5], poly,EPSILON,nonzero=false) == 1); assert(point_in_polygon([-10,-10], poly) == -1); assert(point_in_polygon([10,0], poly) == 0); assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); - assert(point_in_polygon([0,-10], poly,nonzero=false) == 0); - assert(point_in_polygon([0,0], poly2,nonzero=true) == 1); - assert(point_in_polygon([0,1], poly2,nonzero=true) == 0); - assert(point_in_polygon([0,1], poly2,nonzero=false) == 0); - assert(point_in_polygon([1,0], poly2,nonzero=false) == 0); - assert(point_in_polygon([0,0], poly2,nonzero=false,eps=EPSILON) == -1); + assert(point_in_polygon([0,-10], poly,EPSILON,nonzero=false) == 0); + assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=true) == 1); + assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=false) == -1); } *test_point_in_polygon(); From cdfc267ac7823b90501e512c653659901de5f127 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Thu, 25 Mar 2021 11:51:10 +0000 Subject: [PATCH 05/17] Fixes and improvements --- geometry.scad | 196 ++++++++++++++++++++++++--------------- tests/test_geometry.scad | 21 +++-- 2 files changed, 131 insertions(+), 86 deletions(-) diff --git a/geometry.scad b/geometry.scad index 3e3525d..b99290f 100644 --- a/geometry.scad +++ b/geometry.scad @@ -19,10 +19,10 @@ // Arguments: // point = The point to test. // edge = Array of two points forming the line segment to test against. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function point_on_segment2d(point, edge, eps=EPSILON) = assert( is_vector(point,2), "Invalid point." ) - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) assert( _valid_line(edge,2,eps=eps), "Invalid segment." ) let( dp = point-edge[0], de = edge[1]-edge[0], @@ -76,12 +76,12 @@ function point_left_of_line2d(point, line) = // a = First point or list of points. // b = Second point or undef; it should be undef if `c` is undef // c = Third point or undef. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function collinear(a, b, c, eps=EPSILON) = assert( is_path([a,b,c],dim=undef) || ( is_undef(b) && is_undef(c) && is_path(a,dim=undef) ), "Input should be 3 points or a list of points with same dimension.") - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) let( points = is_def(c) ? [a,b,c]: a ) len(points)<3 ? true : noncollinear_triple(points,error=false,eps=eps)==[]; @@ -153,10 +153,11 @@ function _general_line_intersection(s1,s2,eps=EPSILON) = // Arguments: // l1 = First 2D line, given as a list of two 2D points on the line. // l2 = Second 2D line, given as a list of two 2D points on the line. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function line_intersection(l1,l2,eps=EPSILON) = assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) assert( _valid_line(l1,dim=2,eps=eps) &&_valid_line(l2,dim=2,eps=eps), "Invalid line(s)." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) let(isect = _general_line_intersection(l1,l2,eps=eps)) isect[0]; @@ -170,9 +171,9 @@ function line_intersection(l1,l2,eps=EPSILON) = // Arguments: // line = The unbounded 2D line, defined by two 2D points on the line. // ray = The 2D ray, given as a list `[START,POINT]` of the 2D start-point START, and a 2D point POINT on the ray. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function line_ray_intersection(line,ray,eps=EPSILON) = - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) assert( _valid_line(line,dim=2,eps=eps) && _valid_line(ray,dim=2,eps=eps), "Invalid line or ray." ) let( isect = _general_line_intersection(line,ray,eps=eps) @@ -190,9 +191,9 @@ function line_ray_intersection(line,ray,eps=EPSILON) = // Arguments: // line = The unbounded 2D line, defined by two 2D points on the line. // segment = The bounded 2D line segment, given as a list of the two 2D endpoints of the segment. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function line_segment_intersection(line,segment,eps=EPSILON) = - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) assert( _valid_line(line, dim=2,eps=eps) &&_valid_line(segment,dim=2,eps=eps), "Invalid line or segment." ) let( isect = _general_line_intersection(line,segment,eps=eps) @@ -211,9 +212,9 @@ function line_segment_intersection(line,segment,eps=EPSILON) = // Arguments: // r1 = First 2D ray, given as a list `[START,POINT]` of the 2D start-point START, and a 2D point POINT on the ray. // r2 = Second 2D ray, given as a list `[START,POINT]` of the 2D start-point START, and a 2D point POINT on the ray. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function ray_intersection(r1,r2,eps=EPSILON) = - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) assert( _valid_line(r1,dim=2,eps=eps) && _valid_line(r2,dim=2,eps=eps), "Invalid ray(s)." ) let( isect = _general_line_intersection(r1,r2,eps=eps) @@ -231,10 +232,10 @@ function ray_intersection(r1,r2,eps=EPSILON) = // Arguments: // ray = The 2D ray, given as a list `[START,POINT]` of the 2D start-point START, and a 2D point POINT on the ray. // segment = The bounded 2D line segment, given as a list of the two 2D endpoints of the segment. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function ray_segment_intersection(ray,segment,eps=EPSILON) = assert( _valid_line(ray,dim=2,eps=eps) && _valid_line(segment,dim=2,eps=eps), "Invalid ray or segment." ) - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) let( isect = _general_line_intersection(ray,segment,eps=eps) ) @@ -252,10 +253,10 @@ function ray_segment_intersection(ray,segment,eps=EPSILON) = // Arguments: // s1 = First 2D segment, given as a list of the two 2D endpoints of the line segment. // s2 = Second 2D segment, given as a list of the two 2D endpoints of the line segment. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function segment_intersection(s1,s2,eps=EPSILON) = assert( _valid_line(s1,dim=2,eps=eps) && _valid_line(s2,dim=2,eps=eps), "Invalid segment(s)." ) - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) let( isect = _general_line_intersection(s1,s2,eps=eps) ) @@ -463,7 +464,7 @@ function segment_closest_point(seg,pt) = // 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_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) let( pb = furthest_point(points[0],points) ) approx(norm(points[pb]-points[0]),0) ? undef : fast || collinear(points) ? [points[pb], points[0]] : undef; @@ -811,6 +812,8 @@ function adj_opp_to_ang(adj,opp) = // Description: // Returns the area of a triangle formed between three 2D or 3D vertices. // Result will be negative if the points are 2D and in clockwise order. +// Arguments: +// a, b, c = The three vertices of the triangle. // Examples: // triangle_area([0,0], [5,10], [10,0]); // Returns -50 // triangle_area([10,0], [5,10], [0,0]); // Returns 50 @@ -878,6 +881,9 @@ function plane3pt_indexed(points, i1, i2, i3) = // plane_from_normal(normal, [pt]) // Description: // Returns a plane defined by a normal vector and a point. +// Arguments: +// normal = Normal vector to the plane to find.. +// pt = Point 3D on the plane to find. // Example: // plane_from_normal([0,0,1], [2,2,2]); // Returns the xy plane passing through the point (2,2,2) function plane_from_normal(normal, pt=[0,0,0]) = @@ -897,7 +903,7 @@ function plane_from_normal(normal, pt=[0,0,0]) = // Arguments: // points = The list of points to find the plane of. // fast = If true, don't verify that all points in the list are coplanar. Default: false -// eps = How much variance is allowed in testing that each point is on the same plane. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) // Example(3D): // xyzpath = rot(45, v=[-0.3,1,0], p=path3d(star(n=6,id=70,d=100), 70)); // plane = plane_from_points(xyzpath); @@ -906,9 +912,8 @@ function plane_from_normal(normal, pt=[0,0,0]) = // move(cp) rot(from=UP,to=plane_normal(plane)) anchor_arrow(); function plane_from_points(points, fast=false, eps=EPSILON) = assert( is_path(points,dim=3), "Improper 3d point list." ) - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) let( - points = deduplicate(points), indices = noncollinear_triple(points,error=false) ) indices==[] ? undef : @@ -932,7 +937,7 @@ function plane_from_points(points, fast=false, eps=EPSILON) = // Arguments: // poly = The planar 3D polygon to find the plane of. // fast = If true, doesn't verify that all points in the polygon are coplanar. Default: false -// eps = How much variance is allowed in testing that each point is on the same plane. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) // Example(3D): // xyzpath = rot(45, v=[0,1,0], p=path3d(star(n=5,step=2,d=100), 70)); // plane = plane_from_polygon(xyzpath); @@ -941,7 +946,7 @@ function plane_from_points(points, fast=false, eps=EPSILON) = // move(cp) rot(from=UP,to=plane_normal(plane)) anchor_arrow(); function plane_from_polygon(poly, fast=false, eps=EPSILON) = assert( is_path(poly,dim=3), "Invalid polygon." ) - assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) let( poly = deduplicate(poly), n = polygon_normal(poly), @@ -955,6 +960,8 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = // plane_normal(plane); // Description: // Returns the unit length normal vector for the given plane. +// Argument: +// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_normal(plane) = assert( _valid_plane(plane), "Invalid input plane." ) unit([plane.x, plane.y, plane.z]); @@ -967,6 +974,8 @@ function plane_normal(plane) = // Returns coeficient D of the normalized plane equation `Ax+By+Cz=D`, or the scalar offset of the plane from the origin. // This value may be negative. // The absolute value of this coefficient is the distance of the plane from the origin. +// Argument: +// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_offset(plane) = assert( _valid_plane(plane), "Invalid input plane." ) plane[3]/norm([plane.x, plane.y, plane.z]); @@ -1029,6 +1038,8 @@ function projection_on_plane(plane, points) = // pt = plane_point_nearest_origin(plane); // Description: // Returns the point on the plane that is closest to the origin. +// Argument: +// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_point_nearest_origin(plane) = let( plane = normalize_plane(plane) ) point3d(plane) * plane[3]; @@ -1045,7 +1056,7 @@ function plane_point_nearest_origin(plane) = // towards. If the point is behind the plane, then the distance returned // will be negative. The normal of the plane is the same as [A,B,C]. // Arguments: -// plane = The [A,B,C,D] values for the equation of the plane. +// 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 distance_from_plane(plane, point) = assert( _valid_plane(plane), "Invalid input plane." ) @@ -1061,7 +1072,7 @@ function distance_from_plane(plane, point) = // Takes a point, and a plane [A,B,C,D] where the equation of that plane is `Ax+By+Cz=D`. // Returns the coordinates of the closest point on that plane to the given `point`. // Arguments: -// plane = The [A,B,C,D] coefficients for the equation of the plane. +// plane = The [A,B,C,D] coefficients for the plane equation `Ax+By+Cz=D`. // point = The 3D point to find the closest point to. function closest_point_on_plane(plane, point) = assert( _valid_plane(plane), "Invalid input plane." ) @@ -1129,7 +1140,7 @@ function plane_line_angle(plane, line) = // plane = The [A,B,C,D] values for the equation of the plane. // line = A list of two distinct 3D points that are on the line. // bounded = If false, the line is considered unbounded. If true, it is treated as a bounded line segment. If given as `[true, false]` or `[false, true]`, the boundedness of the points are specified individually, allowing the line to be treated as a half-bounded ray. Default: false (unbounded) -// eps = The tolerance value in determining whether the line is parallel to the plane. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function plane_line_intersection(plane, line, bounded=false, eps=EPSILON) = assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) assert(_valid_plane(plane,eps=eps) && _valid_line(line,dim=3,eps=eps), "Invalid plane and/or line.") @@ -1158,7 +1169,7 @@ function plane_line_intersection(plane, line, bounded=false, eps=EPSILON) = // poly = The 3D planar polygon to find the intersection with. // line = A list of two distinct 3D points on the line. // bounded = If false, the line is considered unbounded. If true, it is treated as a bounded line segment. If given as `[true, false]` or `[false, true]`, the boundedness of the points are specified individually, allowing the line to be treated as a half-bounded ray. Default: false (unbounded) -// eps = The tolerance value in determining whether the line is parallel to the plane. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function polygon_line_intersection(poly, line, bounded=false, eps=EPSILON) = assert( is_finite(eps) && eps>=0, "The tolerance should be a positive number." ) assert(is_path(poly,dim=3), "Invalid polygon." ) @@ -1213,6 +1224,10 @@ function polygon_line_intersection(poly, line, bounded=false, eps=EPSILON) = // If you give three planes the intersection is returned as a point. If you give two planes the intersection // is returned as a list of two points on the line of intersection. If any two input planes are parallel // or coincident then returns undef. +// Arguments: +// plane1 = The [A,B,C,D] coefficients for the first plane equation `Ax+By+Cz=D`. +// plane2 = The [A,B,C,D] coefficients for the second plane equation `Ax+By+Cz=D`. +// plane3 = The [A,B,C,D] coefficients for the third plane equation `Ax+By+Cz=D`. function plane_intersection(plane1,plane2,plane3) = assert( _valid_plane(plane1) && _valid_plane(plane2) && (is_undef(plane3) ||_valid_plane(plane3)), "The input must be 2 or 3 planes." ) @@ -1239,10 +1254,10 @@ function plane_intersection(plane1,plane2,plane3) = // Returns true if the given 3D points are non-collinear and are on a plane. // Arguments: // points = The points to test. -// eps = How much variance is allowed in the planarity test. Default: `EPSILON` (1e-9) +// 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 number." ) + 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 : @@ -1259,7 +1274,7 @@ function coplanar(points, eps=EPSILON) = // Arguments: // plane = The plane to test the points on. // points = The list of 3D points to test. -// eps = How much variance is allowed in the planarity testing. Default: `EPSILON` (1e-9) +// eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) function points_on_plane(points, plane, eps=EPSILON) = assert( _valid_plane(plane), "Invalid plane." ) assert( is_matrix(points,undef,3) && len(points)>0, "Invalid pointlist." ) // using is_matrix it accepts len(points)==1 @@ -1278,7 +1293,7 @@ function points_on_plane(points, plane, eps=EPSILON) = // plane that the normal points towards. The normal of the plane is the // same as [A,B,C]. // Arguments: -// plane = The [A,B,C,D] coefficients for the equation of the plane. +// plane = The [A,B,C,D] coefficients for the first plane equation `Ax+By+Cz=D`. // point = The 3D point to test. function in_front_of_plane(plane, point) = distance_from_plane(plane, point) > EPSILON; @@ -1523,6 +1538,13 @@ function circle_point_tangents(r, d, cp, pt) = // returns only two entries. If one circle is inside the other one then no tangents exist // so the function returns the empty set. When the circles are tangent a degenerate tangent line // passes through the point of tangency of the two circles: this degenerate line is NOT returned. +// Arguments: +// c1 = Center of the first circle. +// r1 = Radius of the first circle. +// c2 = Center of the second circle. +// r2 = Radius of the second circle. +// d1 = Diameter of the first circle. +// d2 = Diameter of the second circle. // Example(2D): Four tangents, first in green, second in black, third in blue, last in red. // $fn=32; // c1 = [3,4]; r1 = 2; @@ -1592,11 +1614,15 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = // Usage: // noncollinear_triple(points); // Description: -// Finds the indices of three good non-collinear points from the points list `points`. -// If all points are collinear, returns []. +// Finds the indices of three good non-collinear points from the pointlist `points`. +// If all points are collinear returns [] when `error=true` or an error otherwise . +// Arguments: +// points = List of input points. +// error = Defines the behaviour for collinear input points. When `true`, produces an error, otherwise returns []. Default: `true`. +// eps = Tolerance for collinearity test. Default: EPSILON. function noncollinear_triple(points,error=true,eps=EPSILON) = assert( is_path(points), "Invalid input points." ) - assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative number." ) + assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) let( pa = points[0], b = furthest_point(pa, points), @@ -1670,29 +1696,29 @@ function furthest_point(pt, points) = // area = polygon_area(poly); // Description: // Given a 2D or 3D planar polygon, returns the area of that polygon. -// If the polygon is self-crossing, the results are undefined. For non-planar points the result is undef. -// When `signed` is true, a signed area is returned; a positive area indicates a counterclockwise polygon. +// If the polygon is self-crossing, the results are undefined. For non-planar 3D polygon the result is undef. +// When `signed` is true, a signed area is returned; a positive area indicates a clockwise polygon. // Arguments: -// poly = polygon to compute the area of. -// signed = if true, a signed area is returned (default: false) +// poly = Polygon to compute the area of. +// signed = If true, a signed area is returned. Default: false. function polygon_area(poly, signed=false) = assert(is_path(poly), "Invalid polygon." ) len(poly)<3 ? 0 : - let( cpoly = close_path(simplify_path(poly)) ) len(poly[0])==2 - ? sum([for(i=[1:1:len(poly)-2]) cross(poly[i]-poly[0],poly[i+1]-poly[0]) ])/2 + ? let( total = sum([for(i=[1:1:len(poly)-2]) cross(poly[i]-poly[0],poly[i+1]-poly[0]) ])/2 ) + signed ? total : abs(total) : let( plane = plane_from_points(poly) ) plane==undef? undef : let( - n = unit(plane_normal(plane)), + n = plane_normal(plane), total = sum([ - for(i=[1:1:len(cpoly)-2]) - let( - v1 = cpoly[i] - cpoly[0], - v2 = cpoly[i+1] - cpoly[0] - ) - cross(v1,v2) * n - ])/2 + for(i=[1:1:len(poly)-2]) + let( + v1 = poly[i] - poly[0], + v2 = poly[i+1] - poly[0] + ) + cross(v1,v2) * n + ])/2 ) signed ? total : abs(total); @@ -1703,6 +1729,8 @@ function polygon_area(poly, signed=false) = // Description: // Returns true if the given 2D polygon is convex. The result is meaningless if the polygon is not simple (self-intersecting). // If the points are collinear the result is true. +// Arguments: +// poly = Polygon to check. // Example: // is_convex_polygon(circle(d=50)); // Returns: true // Example: @@ -1742,6 +1770,9 @@ function polygon_shift(poly, i) = // polygon_shift_to_closest_point(path, pt); // Description: // Given a polygon `poly`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`. +// Arguments: +// poly = The list of points in the polygon path. +// pt = The reference point. function polygon_shift_to_closest_point(poly, pt) = assert(is_vector(pt), "Invalid point." ) assert(is_path(poly,dim=len(pt)), "Invalid polygon or incompatible dimension with the point." ) @@ -1845,32 +1876,35 @@ function align_polygon(reference, poly, angles, cp) = // 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. -// If the polygon is self-intersecting, the results are undefined. -function centroid(poly) = +// 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." ) - len(poly[0])==2 - ? sum([ - for(i=[0:len(poly)-1]) - let(segment=select(poly,i,i+1)) - det2(segment)*sum(segment) - ]) / 6 / polygon_area(poly) - : let( plane = plane_from_points(poly, fast=true) ) - assert( !is_undef(plane), "The polygon must be planar." ) - let( - n = plane_normal(plane), + 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( - v0 = poly[0], - v1 = poly[i], - v2 = poly[i+1], - area = cross(v2-v0,v1-v0)*n - ) - [ area, (v0+v1+v2)*area ] - ] ) + 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: // point_in_polygon(point, poly, ) @@ -1879,7 +1913,7 @@ function centroid(poly) = // 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 polyon. +// 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 can have self-intersections. @@ -1889,17 +1923,17 @@ function centroid(poly) = // 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 = Acceptable variance. Default: `EPSILON` (1e-9) -function point_in_polygon(point, poly, eps=EPSILON, nonzero=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, "Invalid tolerance." ) + 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=EPSILON) ) + if( !approx(seg[0],seg[1],eps) ) point_on_segment2d(point, seg, eps=eps)? 1:0 ] ) sum(on_brd) > 0 @@ -1923,12 +1957,12 @@ function point_in_polygon(point, poly, eps=EPSILON, nonzero=true) = p0 = poly[i]-point, p1 = poly[(i+1)%n]-point ) - if( ( (p1.y>eps && p0.y<=0) || (p1.y<=0 && p0.y>eps) ) - && 0 < p0.x - p0.y *(p1.x - p0.x)/(p1.y - p0.y) ) + 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_is_clockwise() @@ -1949,6 +1983,8 @@ function polygon_is_clockwise(poly) = // clockwise_polygon(poly); // 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); @@ -1959,6 +1995,8 @@ function clockwise_polygon(poly) = // ccw_polygon(poly); // 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; @@ -1969,6 +2007,8 @@ function ccw_polygon(poly) = // reverse_polygon(poly) // 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") let(lp=len(poly)) [for (i=idx(poly)) poly[(lp-i)%lp]]; @@ -1979,7 +2019,9 @@ function reverse_polygon(poly) = // n = polygon_normal(poly); // Description: // Given a 3D planar polygon, returns a unit-length normal vector for the -// clockwise orientation of the polygon. +// clockwise orientation of the polygon. If the polygon points are collinear, returns `undef`. +// 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( @@ -1989,7 +2031,7 @@ function polygon_normal(poly) = for (i=[1:1:len(poly)-2]) cross(poly[i+1]-p0, poly[i]-p0) ]) - ) unit(n); + ) unit(n,undef); function _split_polygon_at_x(poly, x) = diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 63a1966..c239d7c 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -842,7 +842,8 @@ module test_cleanup_path() { module test_polygon_area() { assert(approx(polygon_area([[1,1],[-1,1],[-1,-1],[1,-1]]), 4)); - assert(approx(polygon_area(circle(r=50,$fn=1000)), -PI*50*50, eps=0.1)); + assert(approx(polygon_area(circle(r=50,$fn=1000),signed=true), -PI*50*50, eps=0.1)); + assert(approx(polygon_area(rot([13,27,75],p=path3d(circle(r=50,$fn=1000),fill=23)),signed=true), PI*50*50, eps=0.1)); } *test_polygon_area(); @@ -914,10 +915,9 @@ module test_noncollinear_triple() { module test_centroid() { $fn = 24; assert_approx(centroid(circle(d=100)), [0,0]); - assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [20,0]); + assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); - poly = [for(a=[0:90:360]) - move([1,2.5,3.1], rot(p=[cos(a),sin(a),0],from=[0,0,1],to=[1,1,1])) ]; + poly = move([1,2.5,3.1],p=rot([12,49,24], p=path3d(circle(10,$fn=33)))); assert_approx(centroid(poly), [1,2.5,3.1]); } *test_centroid(); @@ -943,19 +943,22 @@ module test_point_in_polygon() { poly2 = [ [-3,-3],[2,-3],[2,1],[-1,1],[-1,-1],[1,-1],[1,2],[-3,2] ]; assert(point_in_polygon([0,0], poly) == 1); assert(point_in_polygon([20,0], poly) == -1); - assert(point_in_polygon([20,0], poly,EPSILON,nonzero=false) == -1); + assert(point_in_polygon([20,0], poly,nonzero=false) == -1); assert(point_in_polygon([5,5], poly) == 1); assert(point_in_polygon([-5,5], poly) == 1); assert(point_in_polygon([-5,-5], poly) == 1); assert(point_in_polygon([5,-5], poly) == 1); - assert(point_in_polygon([5,-5], poly,EPSILON,nonzero=false) == 1); + assert(point_in_polygon([5,-5], poly,nonzero=false,eps=EPSILON) == 1); assert(point_in_polygon([-10,-10], poly) == -1); assert(point_in_polygon([10,0], poly) == 0); assert(point_in_polygon([0,10], poly) == 0); assert(point_in_polygon([0,-10], poly) == 0); - assert(point_in_polygon([0,-10], poly,EPSILON,nonzero=false) == 0); - assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=true) == 1); - assert(point_in_polygon([0,0], poly2,EPSILON,nonzero=false) == -1); + assert(point_in_polygon([0,-10], poly,nonzero=false) == 0); + assert(point_in_polygon([0,0], poly2,nonzero=true) == 1); + assert(point_in_polygon([0,1], poly2,nonzero=true) == 0); + assert(point_in_polygon([0,1], poly2,nonzero=false) == 0); + assert(point_in_polygon([1,0], poly2,nonzero=false) == 0); + assert(point_in_polygon([0,0], poly2,nonzero=false,eps=EPSILON) == -1); } *test_point_in_polygon(); From 685e727d8dfdf0cb5478a021ce06bf8b5dfea05d Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 28 Mar 2021 16:59:35 +0100 Subject: [PATCH 06/17] teste edicao em common marcado 217 - 224 --- common.scad | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/common.scad b/common.scad index ab24380..dc2333a 100644 --- a/common.scad +++ b/common.scad @@ -38,7 +38,6 @@ function typeof(x) = "invalid"; - // Function: is_type() // Usage: // bool = is_type(x, types); @@ -262,6 +261,8 @@ function is_bool_list(list, length) = // See Also: first_defined(), one_defined(), num_defined() // Description: // Returns the value given as `v` if it is not `undef`. Otherwise, returns the value of `dflt`. +// Returns the value given as `v` if it is not `undef`. +// Otherwise, returns the value of `dflt`. // Arguments: // v = Value to pass through if not `undef`. // dflt = Value to return if `v` *is* `undef`. @@ -279,6 +280,9 @@ function default(v,dflt=undef) = is_undef(v)? dflt : v; // Arguments: // v = The list whose items are being checked. // recursive = If true, sublists are checked recursively for defined values. The first sublist that has a defined item is returned. +// Examples: +// list = *** +// val = first_defined(list) function first_defined(v,recursive=false,_i=0) = _i) // Topics: Undef Handling // See Also: default(), first_defined(), num_defined(), any_defined(), all_defined() +// one_defined(vars, names, ) // Description: // Examines the input list `vals` and returns the entry which is not `undef`. // If more than one entry is not `undef` then an error is asserted, specifying @@ -590,13 +595,13 @@ function segs(r) = // Module: no_children() -// Topics: Error Checking -// See Also: no_function(), no_module() // Usage: // no_children($children); +// Topics: Error Checking +// See Also: no_function(), no_module() // Description: -// Assert that the calling module does not support children. Prints an error message to this effect and fails if children are present, -// as indicated by its argument. +// Assert that the calling module does not support children. Prints an error message to this effect +// and fails if children are present, as indicated by its argument. // Arguments: // $children = number of children the module has. // Example: @@ -638,6 +643,7 @@ module no_module() { } + // Section: Testing Helpers @@ -660,7 +666,7 @@ function _valstr(x) = // expected = The value that was expected. // info = Extra info to print out to make the error clearer. // Example: -// assert_approx(1/3, 0.333333333333333, str("numer=",1,", demon=",3)); +// assert_approx(1/3, 0.333333333333333, str("number=",1,", demon=",3)); module assert_approx(got, expected, info) { no_children($children); if (!approx(got, expected)) { @@ -759,8 +765,8 @@ module shape_compare(eps=1/1024) { // The syntax is: `[for (INIT; CONDITION; NEXT) RETVAL]` where: // - INIT is zero or more `let()` style assignments that are evaluated exactly one time, before the first loop. // - CONDITION is an expression evaluated at the start of each loop. If true, continues with the loop. -// - RETVAL is an expression that returns a list item for each loop. -// - NEXT is one or more `let()` style assignments that is evaluated at the end of each loop. +// - RETVAL is an expression that returns a list item at each loop beginning. +// - NEXT is one or more `let()` style assignments that is evaluated for each loop. // . // Since the INIT phase is only run once, and the CONDITION and RETVAL expressions cannot update // variables, that means that only the NEXT phase can be used for iterative calculations. @@ -805,6 +811,7 @@ function looping(state) = state < 2; // Function: loop_while() // Usage: +// state = loop_while(state, continue) // state = loop_while(state, continue); // Topics: Iteration // See Also: looping(), loop_done() @@ -823,6 +830,7 @@ function loop_while(state, continue) = // Function: loop_done() // Usage: +// loop_done(state) // bool = loop_done(state); // Topics: Iteration // See Also: looping(), loop_while() From 46b0f03af35a8fa957b708d0a7b3e2a2b30f1906 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 28 Mar 2021 17:06:00 +0100 Subject: [PATCH 07/17] teste2 --- affine.scad | 27 +-- arrays.scad | 35 ++-- beziers.scad | 25 ++- common.scad | 11 +- geometry.scad | 4 +- math.scad | 79 +++++---- paths.scad | 13 +- skin.scad | 371 +++++++++++++++-------------------------- tests/test_all.scad | 29 ++++ tests/test_common.scad | 3 + vectors.scad | 9 +- vnf.scad | 28 +++- 12 files changed, 299 insertions(+), 335 deletions(-) create mode 100644 tests/test_all.scad diff --git a/affine.scad b/affine.scad index 55c3ea3..2a363e8 100644 --- a/affine.scad +++ b/affine.scad @@ -82,8 +82,8 @@ function is_affine(x,dim=[2,3]) = // b = is_2d_transform(move([10,20,30])); // Returns: false // b = is_2d_transform(scale([2,3,4])); // Returns: true function is_2d_transform(t) = // z-parameters are zero, except we allow t[2][2]!=1 so scale() works - t[2][0]==0 && t[2][1]==0 && t[2][3]==0 && t[0][2] == 0 && t[1][2]==0 && - (t[2][2]==1 || !(t[0][0]==1 && t[0][1]==0 && t[1][0]==0 && t[1][1]==1)); // But rule out zscale() + t[2][0]==0 && t[2][1]==0 && t[2][3]==0 && t[0][2] == 0 && t[1][2]==0 && + (t[2][2]==1 || !(t[0][0]==1 && t[0][1]==0 && t[1][0]==0 && t[1][1]==1)); // But rule out zscale() // Function: affine2d_to_3d() @@ -234,7 +234,6 @@ function rot_decode(M) = - // Section: Affine2d 3x3 Transformation Matrices @@ -632,19 +631,11 @@ function affine3d_rot_from_to(from, to) = let( from = unit(point3d(from)), to = unit(point3d(to)) - ) approx(from,to)? affine3d_identity() : - let( - u = vector_axis(from,to), - ang = vector_angle(from,to), - c = cos(ang), - c2 = 1-c, - s = sin(ang) - ) [ - [u.x*u.x*c2+c , u.x*u.y*c2-u.z*s, u.x*u.z*c2+u.y*s, 0], - [u.y*u.x*c2+u.z*s, u.y*u.y*c2+c , u.y*u.z*c2-u.x*s, 0], - [u.z*u.x*c2-u.y*s, u.z*u.y*c2+u.x*s, u.z*u.z*c2+c , 0], - [ 0, 0, 0, 1] - ]; + ) + approx(from,[0,0,0]) + || approx(to,[0,0,0]) + || approx(from+to,[0,0,0])? affine3d_identity() : + affine3d_mirror(from+to) * affine3d_mirror(from); // Function: affine3d_frame_map() @@ -705,8 +696,7 @@ function affine3d_frame_map(x,y,z, reverse=false) = assert(ocheck, "Inputs must be orthogonal when reverse==true") [for (r=map) [for (c=r) c, 0], [0,0,0,1]] ) : [for (r=transpose(map)) [for (c=r) c, 0], [0,0,0,1]]; - - + // Function: affine3d_mirror() // Usage: @@ -875,5 +865,4 @@ function affine3d_skew_yz(ya=0, za=0) = ]; - // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/arrays.scad b/arrays.scad index 3fd8bbc..e6e39c4 100644 --- a/arrays.scad +++ b/arrays.scad @@ -39,9 +39,11 @@ function is_homogeneous(l, depth=10) = !is_list(l) || l==[] ? false : let( l0=l[0] ) [] == [for(i=[1:len(l)-1]) if( ! _same_type(l[i],l0, depth+1) ) 0 ]; + function is_homogenous(l, depth=10) = is_homogeneous(l, depth); + function _same_type(a,b, depth) = (depth==0) || (is_undef(a) && is_undef(b)) || @@ -50,7 +52,7 @@ function _same_type(a,b, depth) = (is_string(a) && is_string(b)) || (is_list(a) && is_list(b) && len(a)==len(b) && []==[for(i=idx(a)) if( ! _same_type(a[i],b[i],depth-1) ) 0] ); - + // Function: select() // Description: @@ -227,6 +229,8 @@ function in_list(val,list,idx) = // Usage: // idx = find_first_match(val, list, , ); // indices = find_first_match(val, list, all=true, , ); +// Topics: List Handling +// See Also: max_index(), list_increasing(), list_decreasing() // Description: // Finds the first item in `list` that matches `val`, returning the index. // Arguments: @@ -360,7 +364,6 @@ function repeat(val, n, i=0) = // If both `n` and `e` are given, returns `n` values evenly spread from `s` // to `e`, and `step` is ignored. // Arguments: -// --- // n = Desired number of values in returned list, if given. // s = Starting value. Default: 0 // e = Ending value to stop at, if given. @@ -488,6 +491,7 @@ function deduplicate(list, closed=false, eps=EPSILON) = // closed = If true, drops trailing indices if what they index matches what the first index indexes. // eps = The maximum difference to allow between numbers or vectors. // Examples: +// list = [0,1,2,3]; // a = deduplicate_indexed([8,6,4,6,3], [1,4,3,1,2,2,0,1]); // Returns: [1,4,3,2,0,1] // b = deduplicate_indexed([8,6,4,6,3], [1,4,3,1,2,2,0,1], closed=true); // Returns: [1,4,3,2,0] // c = deduplicate_indexed([[7,undef],[7,undef],[1,4],[1,4],[1,4+1e-12]],eps=0); // Returns: [0,2,4] @@ -617,7 +621,7 @@ function list_set(list=[],indices,values,dflt=0,minlen=0) = // b = list_insert([3,6,9,12],[1,3],[5,11]); // Returns [3,5,6,9,11,12] function list_insert(list, indices, values) = assert(is_list(list)) - !is_list(indices) ? + !is_list(indices) ? assert( is_finite(indices) && is_finite(values), "Invalid indices/values." ) assert( indices<=len(list), "Indices must be <= len(list) ." ) [ @@ -1287,6 +1291,8 @@ function permutations(l,n=2) = // pairs = zip(a,b); // triples = zip(a,b,c); // quads = zip([LIST1,LIST2,LIST3,LIST4]); +// Topics: List Handling, Iteration +// See Also: zip_long() // Description: // Zips together two or more lists into a single list. For example, if you have two // lists [3,4,5], and [8,7,6], and zip them together, you get [[3,8],[4,7],[5,6]]. @@ -1312,6 +1318,8 @@ function zip(a,b,c) = // pairs = zip_long(a,b); // triples = zip_long(a,b,c); // quads = zip_long([LIST1,LIST2,LIST3,LIST4]); +// Topics: List Handling, Iteration +// See Also: zip() // Description: // Zips together two or more lists into a single list. For example, if you have two // lists [3,4,5], and [8,7,6], and zip them together, you get [[3,8],[4,7],[5,6]]. @@ -1333,9 +1341,9 @@ function zip_long(a,b,c,fill) = b!=undef? zip_long([a,b,if (c!=undef) c],fill=fill) : let(n = list_longest(a)) [for (i=[0:1:n-1]) [for (x=a) i); @@ -1633,11 +1641,11 @@ function submatrix_set(M,A,m=0,n=0) = // Function: array_group() // Usage: // groups = array_group(v, , ); +// Topics: Matrices, Array Handling +// See Also: subindex(), submatrix(), hstack(), flatten(), full_flatten() // Description: // Takes a flat array of values, and groups items in sets of `cnt` length. // The opposite of this is `flatten()`. -// Topics: Matrices, Array Handling -// See Also: subindex(), submatrix(), hstack(), flatten(), full_flatten() // Arguments: // v = The list of items to group. // cnt = The number of items to put in each grouping. Default:2 @@ -1661,6 +1669,7 @@ function array_group(v, cnt=2, dflt=0) = // Arguments: // l = List to flatten. // Example: +// flatten([[1,2,3], [4,5,[6,7,8]]]) returns [1,2,3,4,5,[6,7,8]] // l = flatten([[1,2,3], [4,5,[6,7,8]]]); // returns [1,2,3,4,5,[6,7,8]] function flatten(l) = !is_list(l)? l : @@ -1810,7 +1819,7 @@ function transpose(arr, reverse=false) = // A = matrix to test // eps = epsilon for comparing equality. Default: 1e-12 function is_matrix_symmetric(A,eps=1e-12) = - approx(A,transpose(A)); - - + approx(A,transpose(A), eps); + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/beziers.scad b/beziers.scad index 2a74ac3..3c3be95 100644 --- a/beziers.scad +++ b/beziers.scad @@ -6,6 +6,7 @@ // include ////////////////////////////////////////////////////////////////////// + // Terminology: // Path = A series of points joined by straight line segements. // Bezier Curve = A mathematical curve that joins two endpoints, following a curve determined by one or more control points. @@ -377,7 +378,6 @@ function bezier_tangent(curve, u) = [for (v=res) unit(v)]; - // Function: bezier_curvature() // Usage: // crv = bezier_curvature(curve, u); @@ -407,6 +407,7 @@ function bezier_curvature(curve, u) = ]; + // Function: bezier_curve() // Usage: // path = bezier_curve(curve, n, ); @@ -416,9 +417,10 @@ function bezier_curvature(curve, u) = // Takes a list of bezier curve control points and generates n points along the bezier path. // Points start at the first control point and are sampled every `1/n`th // of the way along the bezier parameter, ending *before* the final control point by default. -// The distance between the points will *not* be equidistant. If you wish to add the -// endpoint you can set `endpoint` to true. The degree of the bezier curve is one -// less than the number of points in `curve`. +// If you wish to add the endpoint you can set `endpoint` to true. +// The distance between the points will *not* be equidistant. The points are usually more +// concentrated where the curve has a greater curvature. The degree of the bezier curve is +// one less than the number of points in `curve`. // Arguments: // curve = The list of endpoints and control points for this bezier segment. // n = The number of points to generate along the bezier curve. @@ -435,9 +437,11 @@ function bezier_curvature(curve, u) = // bez = [[0,0], [5,15], [40,20], [60,-15], [80,0]]; // move_copies(bezier_curve(bez, 8)) sphere(r=1.5, $fn=12); // trace_bezier(bez, N=len(bez)-1); -function bezier_curve(curve,n,endpoint) = [each bezier_points(curve, [0:1/n:(n-0.5)/n]), - if (endpoint) curve[len(curve)-1] - ]; +function bezier_curve(curve,n,endpoint=false) = + [each bezier_points(curve, [0:1/n:(n-0.5)/n]), + if (endpoint) curve[len(curve)-1] + ]; + // Function: bezier_segment_closest_point() // Usage: @@ -524,7 +528,6 @@ function bezier_segment_length(curve, start_u=0, end_u=1, max_deflect=0.01) = ]); - // Function: bezier_line_intersection() // Usage: // u = bezier_line_intersection(curve, line); @@ -548,7 +551,6 @@ function bezier_line_intersection(curve, line) = [for(u=real_roots(q)) if (u>=0 && u<=1) u]; - // Function: fillet3pts() // Usage: // bez_path_pts = fillet3pts(p0, p1, p2, r); @@ -661,7 +663,6 @@ function bezier_path_closest_point(path, pt, N=3, max_err=0.01, seg=0, min_seg=u ); - // Function: bezier_path_length() // Usage: // plen = bezier_path_length(path, , ); @@ -946,7 +947,6 @@ module bezier_polygon(bezier, splinesteps=16, N=3) { } - // Module: trace_bezier() // Usage: // trace_bezier(bez, , ) { @@ -1294,6 +1294,7 @@ function patch_reverse(patch) = [for (row=patch) reverse(row)]; + // Section: Bezier Surface Modules @@ -1338,8 +1339,6 @@ function bezier_surface(patches=[], splinesteps=16, vnf=EMPTY_VNF, style="defaul bezier_surface(patches=patches, splinesteps=splinesteps, vnf=vnf, style=style, i=i+1); - - // Module: trace_bezier_patches() // Usage: // trace_bezier_patches(patches, [size], [splinesteps], [showcps], [showdots], [showpatch], [convexity], [style]); diff --git a/common.scad b/common.scad index dc2333a..af380e4 100644 --- a/common.scad +++ b/common.scad @@ -125,7 +125,7 @@ function is_integer(n) = is_finite(n) && n == round(n); // bool = is_nan("foo"); // Returns: false // bool = is_nan(NAN); // Returns: true function is_nan(x) = (x!=x); - + // Function: is_finite() // Usage: @@ -214,7 +214,14 @@ function is_consistent(list, pattern) = is_list(list) && (len(list)==0 || (let(pattern = is_undef(pattern) ? _list_pattern(list[0]): _list_pattern(pattern) ) - []==[for(entry=0*list) if (entry != pattern) entry])); + []==[for(entry=0*list) if (entry != pattern) 0])); +// +// Note: in the event that 0*list produces a warning for non numeric /list/ in a future version, +// the last line above may be rewriten as: +// []==[for(entry=_list_pattern(list)) if (entry != pattern) entry])); + +/* +*/ //Internal function //Creates a list with the same structure of `list` with each of its elements replaced by 0. diff --git a/geometry.scad b/geometry.scad index b145c6f..b1ac0cc 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1072,6 +1072,7 @@ function distance_from_plane(plane, point) = let( plane = normalize_plane(plane) ) point3d(plane)* point - plane[3]; + // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. @@ -1594,7 +1595,6 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = ]; - // Function: circle_line_intersection() // Usage: // isect = circle_line_intersection(c,r,line,,); @@ -1627,7 +1627,6 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = let( offset = sqrt(r*r-d*d), uvec=unit(line[1]-line[0]) ) [closest-offset*uvec, closest+offset*uvec] - ) [for(p=isect) if ((!bounded[0] || (p-line[0])*(line[1]-line[0])>=0) @@ -1635,7 +1634,6 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = - // Section: Pointlists diff --git a/math.scad b/math.scad index bf676e4..34ce5ca 100644 --- a/math.scad +++ b/math.scad @@ -22,8 +22,7 @@ INF = 1/0; // Constant: NAN // Description: The value `nan`, useful for comparisons. -NAN = acos(2); - +NAN = 0/0; // Section: Simple math @@ -588,7 +587,9 @@ function sum(v, dflt=0) = is_vector(v) || is_matrix(v) ? [for(i=[1:len(v)]) 1]*v : _sum(v,v[0]*0); -function _sum(v,_total,_i=0) = _i>=len(v) ? _total : _sum(v,_total+v[_i], _i+1); +function _sum(v,_total,_i=0) = + _i>=len(v) ? _total : _sum(v,_total+v[_i], _i+1); + // Function: cumsum() // Usage: @@ -985,20 +986,18 @@ function determinant(M) = // n = Is given, requires the matrix to have the given width. // square = If true, requires the matrix to have a width equal to its height. Default: false function is_matrix(A,m,n,square=false) = - is_list(A) - && (( is_undef(m) && len(A) ) || len(A)==m) - && is_list(A[0]) - && (( is_undef(n) && len(A[0]) ) || len(A[0])==n) + is_consistent(A) + && len(A)>0 + && is_vector(A[0],n) && (!square || len(A) == len(A[0])) - && is_vector(A[0]) - && is_consistent(A); - + && ( is_undef(m) || len(A)==m ); + // Function: norm_fro() // Usage: // norm_fro(A) // Description: -// Computes frobenius norm of input matrix. The frobenius norm is the square root of the sum of the +// Computes Frobenius norm of input matrix. The Frobenius norm is the square root of the sum of the // squares of all of the entries of the matrix. On vectors it is the same as the usual 2-norm. // This is an easily computed norm that is convenient for comparing two matrices. function norm_fro(A) = @@ -1006,6 +1005,17 @@ function norm_fro(A) = norm(flatten(A)); +// Function: norm_max() +// Usage: +// norm_max(v) +// Description: +// Computes maximum norm of input vector. +// The maximum norm is the maximum of the absolute values of the coordinates of the vector. +function norm_max(v) = + assert(is_vector(v) && len(v)>0, "The norm_max requires a vector with length greater than 0.") + max(max(v),max(-v)); + + // Function: matrix_trace() // Usage: // matrix_trace(M) @@ -1165,7 +1175,7 @@ function all_nonnegative(x) = // Arguments: // a = First value. // b = Second value. -// eps = The maximum allowed difference between `a` and `b` that will return true. +// eps = The maximum allowed difference between `a` and `b` that will return true. Default: EPSILON. // Example: // approx(-0.3333333333,-1/3); // Returns: true // approx(0.3333333333,1/3); // Returns: true @@ -1175,6 +1185,7 @@ function all_nonnegative(x) = function approx(a,b,eps=EPSILON) = (a==b && is_bool(a) == is_bool(b)) || (is_num(a) && is_num(b) && abs(a-b) <= eps) || + (is_vector(a) && is_vector(b,len(a)) && norm_max(a-b) <= eps ) || (is_list(a) && is_list(b) && len(a) == len(b) && [] == [for (i=idx(a)) if (!approx(a[i],b[i],eps=eps)) 1]); @@ -1485,9 +1496,15 @@ function complex(list) = // Multiplies two complex numbers, vectors or matrices, where complex numbers // or entries are represented as vectors: [REAL, IMAGINARY]. Note that all // entries in both arguments must be complex. +// Multiplies two complex numbers represented by 2D vectors. +// Returns a complex number as a 2D vector [REAL, IMAGINARY]. // Arguments: // z1 = First complex number, vector or matrix // z2 = Second complex number, vector or matrix +function c_mul(z1,z2) = + is_matrix([z1,z2],2,2) ? _c_mul(z1,z2) : + _combine_complex(_c_mul(_split_complex(z1), _split_complex(z2))); + function _split_complex(data) = is_vector(data,2) ? data @@ -1497,6 +1514,7 @@ function _split_complex(data) = [for(vec=data) vec * [0,1]] ]; + function _combine_complex(data) = is_vector(data,2) ? data : is_num(data[0][0]) ? [for(i=[0:len(data[0])-1]) [data[0][i],data[1][i]]] @@ -1504,13 +1522,10 @@ function _combine_complex(data) = [for(j=[0:1:len(data[0][0])-1]) [data[0][i][j], data[1][i][j]]]]; + function _c_mul(z1,z2) = [ z1.x*z2.x - z1.y*z2.y, z1.x*z2.y + z1.y*z2.x ]; -function c_mul(z1,z2) = - is_matrix([z1,z2],2,2) ? _c_mul(z1,z2) : - _combine_complex(_c_mul(_split_complex(z1), _split_complex(z2))); - // Function: c_div() // Usage: @@ -1538,6 +1553,7 @@ function c_conj(z) = is_vector(z,2) ? [z.x,-z.y] : [for(entry=z) c_conj(entry)]; + // Function: c_real() // Usage: // x = c_real(z) @@ -1548,6 +1564,7 @@ function c_real(z) = : is_num(z[0][0]) ? z*[1,0] : [for(vec=z) vec * [1,0]]; + // Function: c_imag() // Usage: // x = c_imag(z) @@ -1566,6 +1583,7 @@ function c_imag(z) = // Produce an n by n complex identity matrix function c_ident(n) = [for (i = [0:1:n-1]) [for (j = [0:1:n-1]) (i==j)?[1,0]:[0,0]]]; + // Function: c_norm() // Usage: // n = c_norm(z) @@ -1618,12 +1636,12 @@ function quadratic_roots(a,b,c,real=false) = // where a_n is the z^n coefficient. Polynomial coefficients are real. // The result is a number if `z` is a number and a complex number otherwise. function polynomial(p,z,k,total) = - is_undef(k) - ? assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) - assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) - polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) - : k==len(p) ? total - : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : c_mul(total,z)+[p[k],0]); +    is_undef(k) +    ? assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) +        assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) +        polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) +    : k==len(p) ? total +    : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : c_mul(total,z)+[p[k],0]); // Function: poly_mult() // Usage: @@ -1633,15 +1651,14 @@ function polynomial(p,z,k,total) = // Given a list of polynomials represented as real coefficient lists, with the highest degree coefficient first, // computes the coefficient list of the product polynomial. function poly_mult(p,q) = - is_undef(q) ? - len(p)==2 - ? poly_mult(p[0],p[1]) - : poly_mult(p[0], poly_mult(select(p,1,-1))) - : - assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult") - p*p==0 || q*q==0 - ? [0] - : _poly_trim(convolve(p,q)); +    is_undef(q) ? +        len(p)==2 + ? poly_mult(p[0],p[1]) +        : poly_mult(p[0], poly_mult(select(p,1,-1))) +    : assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult") + p*p==0 || q*q==0 + ? [0] + : _poly_trim(convolve(p,q)); // Function: poly_div() diff --git a/paths.scad b/paths.scad index 5d391c3..98a6892 100644 --- a/paths.scad +++ b/paths.scad @@ -989,7 +989,6 @@ module jittered_poly(path, dist=1/512) { - // Section: 3D Modules @@ -1008,20 +1007,19 @@ module jittered_poly(path, dist=1/512) { // xcopies(3) circle(3, $fn=32); // } module extrude_from_to(pt1, pt2, convexity, twist, scale, slices) { - rtp = xyz_to_spherical(pt2-pt1); translate(pt1) { - rotate([0, rtp[2], rtp[1]]) { - if (rtp[0] > 0) { - linear_extrude(height=rtp[0], convexity=convexity, center=false, slices=slices, twist=twist, scale=scale) { + rot(from=[0,0,1],to=pt2-pt1) { + h = norm(pt2-pt1); + if (h > 0) { + linear_extrude(height=h, convexity=convexity, center=false, slices=slices, twist=twist, scale=scale) { children(); } } } - } + } } - // Module: spiral_sweep() // Description: // Takes a closed 2D polygon path, centered on the XY plane, and sweeps/extrudes it along a 3D spiral path. @@ -1443,7 +1441,6 @@ function path_cut(path,cutdist,closed) = ]; - // Input `data` is a list that sums to an integer. // Returns rounded version of input data so that every // entry is rounded to an integer and the sum is the same as diff --git a/skin.scad b/skin.scad index e53def9..9f0f25c 100644 --- a/skin.scad +++ b/skin.scad @@ -1,11 +1,13 @@ ////////////////////////////////////////////////////////////////////// // LibFile: skin.scad // Functions to skin arbitrary 2D profiles/paths in 3-space. -// Inspired by list-comprehension-demos skin(): -// - https://github.com/openscad/list-comprehension-demos/blob/master/skin.scad -// Includes: +// To use, add the following line to the beginning of your file: +// ``` // include // include +// ``` +// Inspired by list-comprehension-demos skin(): +// - https://github.com/openscad/list-comprehension-demos/blob/master/skin.scad ////////////////////////////////////////////////////////////////////// @@ -13,15 +15,16 @@ // Function&Module: skin() // Usage: As module: -// skin(profiles, slices, , , , , , , , ,,,,) ; +// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z], [convexity], +// [anchor],[cp],[spin],[orient],[extent]); // Usage: As function: -// vnf = skin(profiles, slices, , , , , , ); +// vnf = skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); // Description: // Given a list of two or more path `profiles` in 3d space, produces faces to skin a surface between // the profiles. Optionally the first and last profiles can have endcaps, or the first and last profiles // can be connected together. Each profile should be roughly planar, but some variation is allowed. // Each profile must rotate in the same clockwise direction. If called as a function, returns a -// [VNF structure](vnf.scad) `[VERTICES, FACES]`. If called as a module, creates a polyhedron +// [VNF structure](vnf.scad) like `[VERTICES, FACES]`. If called as a module, creates a polyhedron // of the skinned profiles. // . // The profiles can be specified either as a list of 3d curves or they can be specified as @@ -33,47 +36,45 @@ // For this operation to be well-defined, the profiles must all have the same vertex count and // we must assume that profiles are aligned so that vertex `i` links to vertex `i` on all polygons. // Many interesting cases do not comply with this restriction. Two basic methods can handle -// these cases: either subdivide edges (insert additional points along edges) -// or duplicate vertcies (insert edges of length 0) so that both polygons have -// the same number of points. -// Duplicating vertices allows two distinct points in one polygon to connect to a single point -// in the other one, creating -// triangular faces. You can adjust non-matching polygons yourself +// these cases: either add points to edges (resample) so that the profiles are compatible, +// or repeat vertices. Repeating vertices allows two edges to terminate at the same point, creating +// triangular faces. You can adjust non-matching profiles yourself // either by resampling them using `subdivide_path` or by duplicating vertices using -// `repeat_entries`. It is OK to pass a polygon that has the same vertex repeated, such as +// `repeat_entries`. It is OK to pass a profile that has the same vertex repeated, such as // a square with 5 points (two of which are identical), so that it can match up to a pentagon. // Such a combination would create a triangular face at the location of the duplicated vertex. -// Alternatively, `skin` provides methods (described below) for inserting additional vertices -// automatically to make incompatible paths match. +// Alternatively, `skin` provides methods (described below) for matching up incompatible paths. // . // In order for skinned surfaces to look good it is usually necessary to use a fine sampling of // points on all of the profiles, and a large number of extra interpolated slices between the // profiles that you specify. It is generally best if the triangles forming your polyhedron // are approximately equilateral. The `slices` parameter specifies the number of slices to insert // between each pair of profiles, either a scalar to insert the same number everywhere, or a vector -// to insert a different number between each pair. -// . -// Resampling may occur, depending on the `method` parameter, to make profiles compatible. -// To force (possibly additional) resampling of the profiles to increase the point density you can set `refine=N`, which -// will multiply the number of points on your profile by `N`. You can choose between two resampling -// schemes using the `sampling` option, which you can set to `"length"` or `"segment"`. -// The length resampling method resamples proportional to length. -// The segment method divides each segment of a profile into the same number of points. -// This means that if you refine a profile with the "segment" method you will get N points -// on each edge, but if you refine a profile with the "length" method you will get new points -// distributed around the profile based on length, so small segments will get fewer new points than longer ones. -// A uniform division may be impossible, in which case the code computes an approximation, which may result -// in arbitrary distribution of extra points. See `subdivide_path` for more details. -// Note that when dealing with continuous curves it is always better to adjust the +// to insert a different number between each pair. To resample the profiles you can use set +// `refine=N` which will place `N` points on each edge of your profile. This has the effect of +// multiplying the number of points by N, so a profile with 8 points will have 8*N points after +// refinement. Note that when dealing with continuous curves it is always better to adjust the // sampling in your code to generate the desired sampling rather than using the `refine` argument. // . -// You can choose from five methods for specifying alignment for incommensurate profiles. -// The available methods are `"distance"`, `"fast_distance"`, `"tangent"`, `"direct"` and `"reindex"`. +// Two methods are available for resampling, `"length"` and `"segment"`. Specify them using +// the `sampling` argument. The length resampling method resamples proportional to length. +// The segment method divides each segment of a profile into the same number of points. +// A uniform division may be impossible, in which case the code computes an approximation. +// See `subdivide_path` for more details. +// +// You can choose from four methods for specifying alignment for incommensurate profiles. +// The available methods are `"distance"`, `"tangent"`, `"direct"` and `"reindex"`. // It is useful to distinguish between continuous curves like a circle and discrete profiles // like a hexagon or star, because the algorithms' suitability depend on this distinction. // . -// The default method for aligning profiles is `method="direct"`. -// If you simply supply a list of compatible profiles it will link them up +// The "direct" and "reindex" methods work by resampling the profiles if necessary. As noted above, +// for continuous input curves, it is better to generate your curves directly at the desired sample size, +// but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled +// to match the circle. You can do this in two different ways using the `sampling` parameter. The default +// of `sampling="length"` approximates a uniform length sampling of the profile. The other option +// is `sampling="segment"` which attempts to place the same number of new points on each segment. +// If the segments are of varying length, this will produce a different result. Note that "direct" is +// the default method. If you simply supply a list of compatible profiles it will link them up // exactly as you have provided them. You may find that profiles you want to connect define the // right shapes but the point lists don't start from points that you want aligned in your skinned // polyhedron. You can correct this yourself using `reindex_polygon`, or you can use the "reindex" @@ -81,51 +82,22 @@ // in the polyhedron---in will produce the least twisted possible result. This algorithm has quadratic // run time so it can be slow with very large profiles. // . -// When the profiles are incommensurate, the "direct" and "reindex" resample them to match. As noted above, -// for continuous input curves, it is better to generate your curves directly at the desired sample size, -// but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled -// to match the circle. When you use "direct" or "reindex" the default `sampling` value is -// of `sampling="length"` to approximate a uniform length sampling of the profile. This will generally -// produce the natural result for connecting two continuously sampled profiles or a continuous -// profile and a polygonal one. However depending on your particular case, -// `sampling="segment"` may produce a more pleasing result. These two approaches differ only when -// the segments of your input profiles have unequal length. -// . -// The "distance", "fast_distance" and "tangent" methods work by duplicating vertices to create -// triangular faces. In the skined object created by two polygons, every vertex of a polygon must -// have an edge that connects to some vertex on the other one. If you connect two squares this can be -// accomplished with four edges, but if you want to connect a square to a pentagon you must add a -// fifth edge for the "extra" vertex on the pentagon. You must now decide which vertex on the square to -// connect the "extra" edge to. How do you decide where to put that fifth edge? The "distance" method answers this -// question by using an optimization: it minimizes the total length of all the edges connecting -// the two polygons. This algorithm generally produces a good result when both profiles are discrete ones with +// The "distance" and "tangent" methods are work by duplicating vertices to create +// triangular faces. The "distance" method finds the global minimum distance method for connecting two +// profiles. This algorithm generally produces a good result when both profiles are discrete ones with // a small number of vertices. It is computationally intensive (O(N^3)) and may be -// slow on large inputs. The resulting surfaces generally have curved faces, so be -// sure to select a sufficiently large value for `slices` and `refine`. Note that for -// this method, `sampling` must be set to `"segment"`, and hence this is the default setting. -// Using sampling by length would ignore the repeated vertices and ruin the alignment. -// The "fast_distance" method restricts the optimization by assuming that an edge should connect -// vertex 0 of the two polygons. This reduces the run time to O(N^2) and makes -// the method usable on profiles with more points if you take care to index the inputs to match. -// . +// slow on large inputs. The resulting surfaces generally have curves faces, so be +// sure to select a sufficiently large value for `slices` and `refine`. // The `"tangent"` method generally produces good results when -// connecting a discrete polygon to a convex, finely sampled curve. Given a polygon and a curve, consider one edge -// on the polygon. Find a plane passing through the edge that is tangent to the curve. The endpoints of the edge and -// the point of tangency define a triangular face in the output polyhedron. If you work your way around the polygon -// edges, you can establish a series of triangular faces in this way, with edges linking the polygon to the curve. -// You can then complete the edge assignment by connecting all the edges in between the triangular faces together, -// with many edges meeting at each polygon vertex. The result is an alternation of flat triangular faces with conical -// curves joining them. Another way to think about it is that it splits the points on the curve up into groups and -// connects all the points in one group to the same vertex on the polygon. -// . -// The "tangent" method may fail if the curved profile is non-convex, or doesn't have enough points to distinguish -// all of the tangent points from each other. The algorithm treats whichever input profile has fewer points as the polygon -// and the other one as the curve. Using `refine` with this method will have little effect on the model, so +// connecting a discrete polygon to a convex, finely sampled curve. It works by finding +// a plane that passed through each edge of the polygon that is tangent to +// the curve. It may fail if the curved profile is non-convex, or doesn't have enough points to distinguish +// all of the tangent points from each other. It connects all of the points of the curve to the corners of the discrete +// polygon using triangular faces. Using `refine` with this method will have little effect on the model, so // you should do it only for agreement with other profiles, and these models are linear, so extra slices also -// have no effect. For best efficiency set `refine=1` and `slices=0`. As with the "distance" method, refinement -// must be done using the "segment" sampling scheme to preserve alignment across duplicated points. -// Note that the "tangent" method produces similar results to the "distance" method on curved inputs. If this -// method fails due to concavity, "fast_distance" may be a good option. +// have no effect. For best efficiency set `refine=1` and `slices=0`. When you use refinement with either +// of these methods, it is always the "segment" based resampling described above. This is necessary because +// sampling by length will ignore the repeated vertices and break the alignment. // . // It is possible to specify `method` and `refine` as arrays, but it is important to observe // matching rules when you do this. If a pair of profiles is connected using "tangent" or "distance" @@ -139,12 +111,11 @@ // Arguments: // profiles = list of 2d or 3d profiles to be skinned. (If 2d must also give `z`.) // slices = scalar or vector number of slices to insert between each pair of profiles. Set to zero to use only the profiles you provided. Recommend starting with a value around 10. -// --- -// refine = resample profiles to this number of points per edge. Can be a list to give a refinement for each profile. Recommend using a value above 10 when using the "distance" or "fast_distance" methods. Default: 1. -// sampling = sampling method to use with "direct" and "reindex" methods. Can be "length" or "segment". Ignored if any profile pair uses either the "distance", "fast_distance", or "tangent" methods. Default: "length". +// refine = resample profiles to this number of points per edge. Can be a list to give a refinement for each profile. Recommend using a value above 10 when using the "distance" method. Default: 1. +// sampling = sampling method to use with "direct" and "reindex" methods. Can be "length" or "segment". Ignored if any profile pair uses either the "distance" or "tangent" methods. Default: "length". // closed = set to true to connect first and last profile (to make a torus). Default: false // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. -// method = method for connecting profiles, one of "distance", "fast_distance", "tangent", "direct" or "reindex". Default: "direct". +// method = method for connecting profiles, one of "distance", "tangent", "direct" or "reindex". Default: "direct". // z = array of height values for each profile if the profiles are 2d // convexity = convexity setting for use with polyhedron. (module only) Default: 10 // anchor = Translate so anchor point is at the origin. (module only) Default: "origin" @@ -161,21 +132,21 @@ // Example: Offsetting the starting edge connects to circles in an interesting way: // circ = circle($fn=80, r=3); // skin([circ, rot(110,p=circ)], z=[0,5], slices=20); -// Example(FlatSpin,VPD=20): +// Example(FlatSpin): // skin([ yrot(37,p=path3d(circle($fn=128, r=4))), path3d(square(3),3)], method="reindex",slices=10); -// Example(FlatSpin,VPD=16): Ellipses connected with twist +// Example(FlatSpin): Ellipses connected with twist // ellipse = xscale(2.5,p=circle($fn=80)); // skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10); -// Example(FlatSpin,VPD=16): Ellipses connected without a twist. (Note ellipses stay in the same position: just the connecting edges are different.) +// Example(FlatSpin): Ellipses connected without a twist. (Note ellipses stay in the same position: just the connecting edges are different.) // ellipse = xscale(2.5,p=circle($fn=80)); // skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10, method="reindex"); -// Example(FlatSpin,VPD=500): +// Example(FlatSpin): // $fn=24; // skin([ // yrot(0, p=yscale(2,p=path3d(circle(d=75)))), // [[40,0,100], [35,-15,100], [20,-30,100],[0,-40,100],[-40,0,100],[0,40,100],[20,30,100], [35,15,100]] // ],slices=10); -// Example(FlatSpin,VPD=600): +// Example(FlatSpin): // $fn=48; // skin([ // for (b=[0,90]) [ @@ -229,14 +200,14 @@ // skin([for (i=[0:layers-1]) zrot(-30*i,p=path3d(hexagon(ir=r),i*height/layers))],slices=0); // up(height/layers) cylinder(r=holeradius, h=height); // } -// Example(FlatSpin,VPD=300): A box that is octagonal on the outside and circular on the inside +// Example(FlatSpin): A box that is octagonal on the outside and circular on the inside // height = 45; // sub_base = octagon(d=71, rounding=2, $fn=128); // base = octagon(d=75, rounding=2, $fn=128); // interior = regular_ngon(n=len(base), d=60); // right_half() // skin([ sub_base, base, base, sub_base, interior], z=[0,2,height, height, 2], slices=0, refine=1, method="reindex"); -// Example: Connecting a pentagon and circle with the "tangent" method produces large triangular faces and cone shaped corners. +// Example: Connecting a pentagon and circle with the "tangent" method produces triangular faces. // skin([pentagon(4), circle($fn=80,r=2)], z=[0,3], slices=10, method="tangent"); // Example: rounding corners of a square. Note that `$fn` makes the number of points constant, and avoiding the `rounding=0` case keeps everything simple. In this case, the connections between profiles are linear, so there is no benefit to setting `slices` bigger than zero. // shapes = [for(i=[.01:.045:2])zrot(-i*180/2,cp=[-8,0,0],p=xrot(90,p=path3d(regular_ngon(n=4, side=4, rounding=i, $fn=64))))]; @@ -247,53 +218,53 @@ // Example: You can fix it by specifying "tangent" for the first method, but you still need "direct" for the rest. // shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)]; // skin(shapes, slices=0, method=concat(["tangent"],repeat("direct",len(shapes)-2))); -// Example(FlatSpin,VPD=35): Connecting square to pentagon using "direct" method. +// Example(FlatSpin): Connecting square to pentagon using "direct" method. // skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10); -// Example(FlatSpin,VPD=35): Connecting square to shifted pentagon using "direct" method. +// Example(FlatSpin): Connecting square to shifted pentagon using "direct" method. // skin([regular_ngon(n=4, r=4), right(4,p=regular_ngon(n=5,r=5))], z=[0,4], refine=10, slices=10); -// Example(FlatSpin,VPD=35): To improve the look, you can actually rotate the polygons for a more symmetric pattern of lines. You have to resample yourself before calling `align_polygon` and you should choose a length that is a multiple of both polygon lengths. +// Example(FlatSpin): To improve the look, you can actually rotate the polygons for a more symmetric pattern of lines. You have to resample yourself before calling `align_polygon` and you should choose a length that is a multiple of both polygon lengths. // sq = subdivide_path(regular_ngon(n=4, r=4),40); // pent = subdivide_path(regular_ngon(n=5,r=5),40); // skin([sq, align_polygon(sq,pent,[0:1:360/5])], z=[0,4], slices=10); -// Example(FlatSpin,VPD=35): For the shifted pentagon we can also align, making sure to pass an appropriate centerpoint to `align_polygon`. +// Example(FlatSpin): For the shifted pentagon we can also align, making sure to pass an appropriate centerpoint to `align_polygon`. // sq = subdivide_path(regular_ngon(n=4, r=4),40); // pent = right(4,p=subdivide_path(regular_ngon(n=5,r=5),40)); // skin([sq, align_polygon(sq,pent,[0:1:360/5],cp=[4,0])], z=[0,4], refine=10, slices=10); -// Example(FlatSpin,VPD=35): The "distance" method is a completely different approach. +// Example(FlatSpin): The "distance" method is a completely different approach. // skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10, method="distance"); -// Example(FlatSpin,VPD=35,VPT=[0,0,4]): Connecting pentagon to heptagon inserts two triangular faces on each side +// Example(FlatSpin): Connecting pentagon to heptagon inserts two triangular faces on each side // small = path3d(circle(r=3, $fn=5)); // big = up(2,p=yrot( 0,p=path3d(circle(r=3, $fn=7), 6))); // skin([small,big],method="distance", slices=10, refine=10); -// Example(FlatSpin,VPD=35,VPT=[0,0,4]): But just a slight rotation of the top profile moves the two triangles to one end +// Example(FlatSpin): But just a slight rotation of the top profile moves the two triangles to one end // small = path3d(circle(r=3, $fn=5)); // big = up(2,p=yrot(14,p=path3d(circle(r=3, $fn=7), 6))); // skin([small,big],method="distance", slices=10, refine=10); -// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Another "distance" example: +// Example(FlatSpin): Another "distance" example: // off = [0,2]; // shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]); // rshape = rot(180,cp=centroid(shape)+off, p=shape); // skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15); -// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Slightly shifting the profile changes the optimal linkage +// Example(FlatSpin): Slightly shifting the profile changes the optimal linkage // off = [0,1]; // shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]); // rshape = rot(180,cp=centroid(shape)+off, p=shape); // skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15); -// Example(FlatSpin,VPD=444,VPT=[0,0,50]): This optimal solution doesn't look terrible: +// Example(FlatSpin): This optimal solution doesn't look terrible: // prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]); // prof2 = path3d(regular_ngon(n=7, r=50),100); // skin([prof1, prof2], method="distance", slices=10, refine=10); -// Example(FlatSpin,VPD=444,VPT=[0,0,50]): But this one looks better. The "distance" method doesn't find it because it uses two more edges, so it clearly has a higher total edge distance. We force it by doubling the first two vertices of one of the profiles. +// Example(FlatSpin): But this one looks better. The "distance" method doesn't find it because it uses two more edges, so it clearly has a higher total edge distance. We force it by doubling the first two vertices of one of the profiles. // prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]); // prof2 = path3d(regular_ngon(n=7, r=50),100); // skin([repeat_entries(prof1,[2,2,1,1,1,1,1]), // prof2], // method="distance", slices=10, refine=10); -// Example(FlatSpin,VPD=80,VPT=[0,0,7]): The "distance" method will often produces results similar to the "tangent" method if you use it with a polygon and a curve, but the results can also look like this: +// Example(FlatSpin): The "distance" method will often produces results similar to the "tangent" method if you use it with a polygon and a curve, but the results can also look like this: // skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))], method="distance", slices=0); -// Example(FlatSpin,VPD=80,VPT=[0,0,7]): Using the "tangent" method produces: +// Example(FlatSpin): Using the "tangent" method produces: // skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))], method="tangent", slices=0); -// Example(FlatSpin,VPD=74): Torus using hexagons and pentagons, where `closed=true` +// Example(FlatSpin): Torus using hexagons and pentagons, where `closed=true` // hex = right(7,p=path3d(hexagon(r=3))); // pent = right(7,p=path3d(pentagon(r=3))); // N=5; @@ -316,7 +287,7 @@ // rot(17,p=regular_ngon(n=6, r=3)), // rot(37,p=regular_ngon(n=4, r=3))], // z=[0,2,4,6,9], method="distance", slices=10, refine=10); -// Example(FlatSpin,VPD=935,VPT=[75,0,123]): Vertex count of the polygon changes at every profile +// Example(FlatSpin): Vertex count of the polygon changes at every profile // skin([ // for (ang = [0:10:90]) // rot([0,ang,0], cp=[200,0,0], p=path3d(circle(d=100,$fn=12-(ang/10)))) @@ -395,7 +366,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close assert(len(bad)==0, str("Profiles ",bad," are not a paths or have length less than 3")) let( profcount = len(profiles) - (closed?0:1), - legal_methods = ["direct","reindex","distance","fast_distance","tangent"], + legal_methods = ["direct","reindex","distance","tangent"], caps = is_def(caps) ? caps : closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), @@ -423,15 +394,13 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close assert(methodlistok==[], str("method list contains invalid method at ",methodlistok)) assert(len(method) == profcount,"Method list is the wrong length") assert(in_list(sampling,["length","segment"]), "sampling must be set to \"length\" or \"segment\"") - assert(sampling=="segment" || (!in_list("distance",method) && !in_list("fast_distance",method) && !in_list("tangent",method)), "sampling is set to \"length\" which is only allowed with methods \"direct\" and \"reindex\"") + assert(sampling=="segment" || (!in_list("distance",method) && !in_list("tangent",method)), "sampling is set to \"length\" which is only allowed iwith methods \"direct\" and \"reindex\"") assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") let( profile_dim=array_dim(profiles,2), - profiles_zcheck = (profile_dim != 2) || (profile_dim==2 && is_list(z) && len(z)==len(profiles)), profiles_ok = (profile_dim==2 && is_list(z) && len(z)==len(profiles)) || profile_dim==3 ) - assert(profiles_zcheck, "z parameter is invalid or has the wrong length.") assert(profiles_ok,"Profiles must all be 3d or must all be 2d, with matching length z parameter.") assert(is_undef(z) || profile_dim==2, "Do not specify z with 3d profiles") assert(profile_dim==3 || len(z)==len(profiles),"Length of z does not match length of profiles.") @@ -470,7 +439,6 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close let( pair = method[i]=="distance" ? _skin_distance_match(profiles[i],select(profiles,i+1)) : - method[i]=="fast_distance" ? _skin_aligned_distance_match(profiles[i], select(profiles,i+1)) : method[i]=="tangent" ? _skin_tangent_match(profiles[i],select(profiles,i+1)) : /*method[i]=="reindex" || method[i]=="direct" ?*/ let( p1 = subdivide_path(profiles[i],max_list[i], method=sampling), @@ -542,9 +510,8 @@ function _skin_core(profiles, caps) = // Function: subdivide_and_slice() -// Topics: Paths, Path Subdivision // Usage: -// newprof = subdivide_and_slice(profiles, slices, , , ); +// subdivide_and_slice(profiles, slices, [numpoints], [method], [closed]) // Description: // Subdivides the input profiles to have length `numpoints` where `numpoints` must be at least as // big as the largest input profile. By default `numpoints` is set equal to the length of the @@ -570,41 +537,9 @@ function subdivide_and_slice(profiles, slices, numpoints, method="length", close slice_profiles(fixpoly, slices, closed); -// Function: subdivide_long_segments() -// Topics: Paths, Path Subdivision -// See Also: subdivide_path(), subdivide_and_slice(), path_add_jitter(), jittered_poly() -// Usage: -// spath = subdivide_long_segments(path, maxlen, ); -// Description: -// Evenly subdivides long `path` segments until they are all shorter than `maxlen`. -// Arguments: -// path = The path to subdivide. -// maxlen = The maximum allowed path segment length. -// --- -// closed = If true, treat path like a closed polygon. Default: true -// Example: -// path = pentagon(d=100); -// spath = subdivide_long_segments(path, 10, closed=true); -// stroke(path); -// color("lightgreen") move_copies(path) circle(d=5,$fn=12); -// color("blue") move_copies(spath) circle(d=3,$fn=12); -function subdivide_long_segments(path, maxlen, closed=false) = - assert(is_path(path)) - assert(is_finite(maxlen)) - assert(is_bool(closed)) - [ - for (p=pair(path,closed)) let( - steps = ceil(norm(p[1]-p[0])/maxlen) - ) each lerp(p[0],p[1],[0:1/steps:1-EPSILON]), - if (!closed) last(path) - ]; - - - // Function: slice_profiles() -// Topics: Paths, Path Subdivision // Usage: -// profs = slice_profiles(profiles, slices, ); +// profs = slice_profiles(profiles,slices,); // Description: // Given an input list of profiles, linearly interpolate between each pair to produce a // more finely sampled list. The parameters `slices` specifies the number of slices to @@ -730,18 +665,18 @@ function _dp_extract_map(map) = if (i==0 && j==0) each [smallmap,bigmap]]; -/// Internal Function: _skin_distance_match(poly1,poly2) -/// Usage: -/// polys = _skin_distance_match(poly1,poly2); -/// Description: -/// Find a way of associating the vertices of poly1 and vertices of poly2 -/// that minimizes the sum of the length of the edges that connect the two polygons. -/// Polygons can be in 2d or 3d. The algorithm has cubic run time, so it can be -/// slow if you pass large polygons. The output is a pair of polygons with vertices -/// duplicated as appropriate to be used as input to `skin()`. -/// Arguments: -/// poly1 = first polygon to match -/// poly2 = second polygon to match +// Internal Function: _skin_distance_match(poly1,poly2) +// Usage: +// polys = _skin_distance_match(poly1,poly2); +// Description: +// Find a way of associating the vertices of poly1 and vertices of poly2 +// that minimizes the sum of the length of the edges that connect the two polygons. +// Polygons can be in 2d or 3d. The algorithm has cubic run time, so it can be +// slow if you pass large polygons. The output is a pair of polygons with vertices +// duplicated as appropriate to be used as input to `skin()`. +// Arguments: +// poly1 = first polygon to match +// poly2 = second polygon to match function _skin_distance_match(poly1,poly2) = let( swap = len(poly1)>len(poly2), @@ -774,37 +709,21 @@ function _skin_distance_match(poly1,poly2) = newbig = polygon_shift(repeat_entries(map_poly[1],unique_count(bigmap)[1]),bigshift) ) swap ? [newbig, newsmall] : [newsmall,newbig]; - - -// This function associates vertices but with the assumption that index 0 is associated between the -// two inputs. This gives only quadratic run time. As above, output is pair of polygons with -// vertices duplicated as suited to use as input to skin(). - -function _skin_aligned_distance_match(poly1, poly2) = - let( - result = _dp_distance_array(poly1, poly2, abort_thresh=1/0), - map = _dp_extract_map(result[1]), - shift0 = len(map[0]) - max(max_index(map[0],all=true))-1, - shift1 = len(map[1]) - max(max_index(map[1],all=true))-1, - new0 = polygon_shift(repeat_entries(poly1,unique_count(map[0])[1]),shift0), - new1 = polygon_shift(repeat_entries(poly2,unique_count(map[1])[1]),shift1) - ) - [new0,new1]; - - +// ////////////////////////////////////////////////////////////////////////////////////////////////////////////// -/// Internal Function: _skin_tangent_match() -/// Usage: -/// x = _skin_tangent_match(poly1, poly2) -/// Description: -/// Finds a mapping of the vertices of the larger polygon onto the smaller one. Whichever input is the -/// shorter path is the polygon, and the longer input is the curve. For every edge of the polygon, the algorithm seeks a plane that contains that -/// edge and is tangent to the curve. There will be more than one such point. To choose one, the algorithm centers the polygon and curve on their centroids -/// and chooses the closer tangent point. The algorithm works its way around the polygon, computing a series of tangent points and then maps all of the -/// points on the curve between two tangent points into one vertex of the polygon. This algorithm can fail if the curve has too few points or if it is concave. -/// Arguments: -/// poly1 = input polygon -/// poly2 = input polygon +// +// Internal Function: _skin_tangent_match() +// Usage: +// x = _skin_tangent_match(poly1, poly2) +// Description: +// Finds a mapping of the vertices of the larger polygon onto the smaller one. Whichever input is the +// shorter path is the polygon, and the longer input is the curve. For every edge of the polygon, the algorithm seeks a plane that contains that +// edge and is tangent to the curve. There will be more than one such point. To choose one, the algorithm centers the polygon and curve on their centroids +// and chooses the closer tangent point. The algorithm works its way around the polygon, computing a series of tangent points and then maps all of the +// points on the curve between two tangent points into one vertex of the polygon. This algorithm can fail if the curve has too few points or if it is concave. +// Arguments: +// poly1 = input polygon +// poly2 = input polygon function _skin_tangent_match(poly1, poly2) = let( swap = len(poly1)>len(poly2), @@ -838,7 +757,7 @@ function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) = // Function: associate_vertices() // Usage: -// newpoly = associate_vertices(polygons, split); +// associate_vertices(polygons, split) // Description: // Takes as input a list of polygons and duplicates specified vertices in each polygon in the list through the series so // that the input can be passed to `skin()`. This allows you to decide how the vertices are linked up rather than accepting @@ -854,26 +773,26 @@ function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) = // Arguments: // polygons = list of polygons to split // split = list of lists of split vertices -// Example(FlatSpin,VPD=17,VPT=[0,0,2]): If you skin together a square and hexagon using the optimal distance method you get two triangular faces on opposite sides: +// Example(FlatSpin): If you skin together a square and hexagon using the optimal distance method you get two triangular faces on opposite sides: // sq = regular_ngon(4,side=2); // hex = apply(rot(15),hexagon(side=2)); // skin([sq,hex], slices=10, refine=10, method="distance", z=[0,4]); -// Example(FlatSpin,VPD=17,VPT=[0,0,2]): Using associate_vertices you can change the location of the triangular faces. Here they are connect to two adjacent vertices of the square: +// Example(FlatSpin): Using associate_vertices you can change the location of the triangular faces. Here they are connect to two adjacent vertices of the square: // sq = regular_ngon(4,side=2); // hex = apply(rot(15),hexagon(side=2)); // skin(associate_vertices([sq,hex],[[1,2]]), slices=10, refine=10, sampling="segment", z=[0,4]); -// Example(FlatSpin,VPD=17,VPT=[0,0,2]): Here the two triangular faces connect to a single vertex on the square. Note that we had to rotate the hexagon to line them up because the vertices match counting forward, so in this case vertex 0 of the square matches to vertices 0, 1, and 2 of the hexagon. +// Example(FlatSpin): Here the two triangular faces connect to a single vertex on the square. Note that we had to rotate the hexagon to line them up because the vertices match counting forward, so in this case vertex 0 of the square matches to vertices 0, 1, and 2 of the hexagon. // sq = regular_ngon(4,side=2); // hex = apply(rot(60),hexagon(side=2)); // skin(associate_vertices([sq,hex],[[0,0]]), slices=10, refine=10, sampling="segment", z=[0,4]); -// Example(3D): This example shows several polygons, with only a single vertex split at each step: +// Example: This example shows several polygons, with only a single vertex split at each step: // sq = regular_ngon(4,side=2); // pent = pentagon(side=2); // hex = hexagon(side=2); // sep = regular_ngon(7,side=2); // profiles = associate_vertices([sq,pent,hex,sep], [1,3,4]); // skin(profiles ,slices=10, refine=10, method="distance", z=[0,2,4,6]); -// Example(3D): The polygons cannot shrink, so if you want to have decreasing polygons you'll need to concatenate multiple results. Note that it is perfectly ok to duplicate a profile as shown here, where the pentagon is duplicated: +// Example: The polygons cannot shrink, so if you want to have decreasing polygons you'll need to concatenate multiple results. Note that it is perfectly ok to duplicate a profile as shown here, where the pentagon is duplicated: // sq = regular_ngon(4,side=2); // pent = pentagon(side=2); // grow = associate_vertices([sq,pent], [1]); @@ -883,7 +802,8 @@ function associate_vertices(polygons, split, curpoly=0) = curpoly==len(polygons)-1 ? polygons : let( polylen = len(polygons[curpoly]), - cursplit = force_list(split[curpoly]) + cursplit = force_list(split[curpoly]), + fdsa= echo(cursplit=cursplit) ) assert(len(split)==len(polygons)-1,str(split,"Split list length mismatch: it has length ", len(split)," but must have length ",len(polygons)-1)) assert(polylen<=len(polygons[curpoly+1]),str("Polygon ",curpoly," has more vertices than the next one.")) @@ -903,7 +823,7 @@ function associate_vertices(polygons, split, curpoly=0) = // Function&Module: sweep() // Usage: As Module -// sweep(shape, transforms, , , , , , , ) ; +// sweep(shape, transforms, , ) // Usage: As Function // vnf = sweep(shape, transforms, , ); // Description: @@ -925,7 +845,6 @@ function associate_vertices(polygons, split, curpoly=0) = // transforms = list of 4x4 matrices to apply // closed = set to true to form a closed (torus) model. Default: false // caps = true to create endcap faces when closed is false. Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array. Default is true if closed is false. -// --- // convexity = convexity setting for use with polyhedron. (module only) Default: 10 // anchor = Translate so anchor point is at the origin. (module only) Default: "origin" // spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 @@ -995,11 +914,10 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // Function&Module: path_sweep() -// Usage: As module -// path_sweep(shape, path, , , , , , , , , , , , , , , , , ) ; -// vnf = path_sweep(shape, path, , , , , , , , , , , , ); +// Usage: +// path_sweep(shape, path, [method], [normal], [closed], [twist], [twist_by_length], [symmetry], [last_normal], [tangent], [relaxed], [caps], [convexity], [transforms]) // Description: -// Takes as input a 2D polygon path, and a 2d or 3d path and constructs a polyhedron by sweeping the shape along the path. +// Takes as input a 2D polygon path or region, and a 2d or 3d path and constructs a polyhedron by sweeping the shape along the path. // When run as a module returns the polyhedron geometry. When run as a function returns a VNF by default or if you set `transforms=true` // then it returns a list of transformations suitable as input to `sweep`. // . @@ -1049,7 +967,6 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // shape = A 2D polygon path or region describing the shape to be swept. // path = 2D or 3D path giving the path to sweep over // method = one of "incremental", "natural" or "manual". Default: "incremental" -// --- // normal = normal vector for initializing the incremental method, or for setting normals with method="manual". Default: UP if the path makes an angle lower than 45 degrees to the xy plane, BACK otherwise. // closed = path is a closed loop. Default: false // twist = amount of twist to add in degrees. For closed sweeps must be a multiple of 360/symmetry. Default: 0 @@ -1223,7 +1140,7 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; // path_sweep(ushape, knot_path, closed=true, method="natural"); // Example: knot with twist. Note if you twist it the other direction the center section untwists because of the natural twist there. Also compare to the "incremental" method which has less twist in the center. -// function knot(a,b,t) = // rolling knot +// function knot(a,b,t) = // rolling knot // [ a * cos (3 * t) / (1 - b* sin (2 *t)), // a * sin( 3 * t) / (1 - b* sin (2 *t)), // 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))]; @@ -1236,10 +1153,10 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // [ a * cos (3 * t) / (1 - b* sin (2 *t)), // a * sin( 3 * t) / (1 - b* sin (2 *t)), // 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))]; -// a = 0.8; b = sqrt (1 - a * a); -// ksteps = 400; -// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)]; -// path_sweep(subdivide_path(pentagon(r=12),30), knot_path, closed=true, twist=-360*8, symmetry=5, method="natural", twist_by_length=false); +// a = 0.8; b = sqrt (1 - a * a); +// ksteps = 400; +// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)]; +// path_sweep(subdivide_path(pentagon(r=12),30), knot_path, closed=true, twist=-360*8, symmetry=5, method="natural", twist_by_length=false); // Example: This torus knot example comes from list-comprehension-demos. The knot lies on the surface of a torus. When we use the "natural" method the swept figure is angled compared to the surface of the torus because the curve doesn't follow geodesics of the torus. // function knot(phi,R,r,p,q) = // [ (r * cos(q * phi) + R) * cos(p * phi), @@ -1249,7 +1166,7 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // points = 50; // points per loop // R = 400; r = 150; // Torus size // p = 2; q = 5; // Knot parameters -// %torus(r_maj=R,r_min=r); +// %torus(r=R,r2=r); // k = max(p,q) / gcd(p,q) * points; // knot_path = [ for (i=[0:k-1]) knot(360*i/k/gcd(p,q),R,r,p,q) ]; // path_sweep(rot(90,p=ushape),knot_path, method="natural", closed=true); @@ -1266,7 +1183,7 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // points = 50; // points per loop // R = 400; r = 150; // Torus size // p = 2; q = 5; // Knot parameters -// %torus(r_maj=R,r_min=r); +// %torus(r=R,r2=r); // k = max(p,q) / gcd(p,q) * points; // knot_path = [ for (i=[0:k-1]) knot(360*i/k/gcd(p,q),R,r,p,q) ]; // normals = [ for (i=[0:k-1]) knot_normal(360*i/k/gcd(p,q),R,r,p,q) ]; @@ -1278,18 +1195,6 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))]; // inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))]; // sweep(shape, concat(outside,inside),closed=true); -// Example: Using path_sweep on a region -// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)]; -// rgn2 = [square(30,center=false)]; -// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))]; -// mrgn = union(rgn1,rgn2); -// orgn = difference(mrgn,rgn3); -// path_sweep(orgn,arc(r=40,angle=180)); -// Example: A region with a twist -// region = [for(i=pentagon(5)) move(i,p=circle(r=2,$fn=25))]; -// path_sweep(region, -// circle(r=16,$fn=75),closed=true, -// twist=360/5*2,symmetry=5); module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10, anchor="origin",cp,spin=0, orient=UP, extent=false) @@ -1309,7 +1214,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi assert(!closed || twist % (360/symmetry)==0, str("For a closed sweep, twist must be a multiple of 360/symmetry = ",360/symmetry)) assert(closed || symmetry==1, "symmetry must be 1 when closed is false") assert(is_integer(symmetry) && symmetry>0, "symmetry must be a positive integer") -// let(shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape")) + assert(is_path(shape,2) || is_region(shape), "shape must be a 2d path or region.") assert(is_path(path), "input path is not a path") assert(!closed || !approx(path[0],select(path,-1)), "Closed path includes start point at the end") let( @@ -1336,7 +1241,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi let(rotations = [for( i = 0, ynormal = normal - (normal * tangents[0])*tangents[0], - rotation = affine3d_frame_map(y=ynormal, z=tangents[0]) + rotation = affine_frame_map(y=ynormal, z=tangents[0]) ; i < len(tangents) + (closed?1:0) ; rotation = i, , , , , , , , ) ; -// Usage: as function -// vnf = path_sweep2d(shape, path, , , ); +// Usage: +// path_sweep2d(shape, path, , ) // Description: // Takes an input 2D polygon (the shape) and a 2d path and constructs a polyhedron by sweeping the shape along the path. // When run as a module returns the polyhedron geometry. When run as a function returns a VNF. @@ -1417,7 +1320,6 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi // closed = path is a closed loop. Default: false // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. // quality = quality of offset used in calculation. Default: 1 -// --- // convexity = convexity parameter for polyhedron (module only) Default: 10 // anchor = Translate so anchor point is at the origin. (module only) Default: "origin" // spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 @@ -1445,8 +1347,7 @@ function path_sweep2d(shape, path, closed=false, caps, quality=1) = caps = is_def(caps) ? caps : closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), - fullcaps = is_bool(caps) ? [caps,caps] : caps, - shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape") + fullcaps = is_bool(caps) ? [caps,caps] : caps ) assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") diff --git a/tests/test_all.scad b/tests/test_all.scad new file mode 100644 index 0000000..72a552d --- /dev/null +++ b/tests/test_all.scad @@ -0,0 +1,29 @@ + +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include + + diff --git a/tests/test_common.scad b/tests/test_common.scad index 3d150d7..2121489 100644 --- a/tests/test_common.scad +++ b/tests/test_common.scad @@ -221,10 +221,13 @@ module test_is_consistent() { assert(is_consistent([])); assert(is_consistent([[],[]])); assert(is_consistent([3,4,5])); + assert(is_consistent([3,4,5], 1)); assert(is_consistent([[3,4],[4,5],[6,7]])); assert(is_consistent([[[3],4],[[4],5]])); assert(!is_consistent(5)); assert(!is_consistent(undef)); + assert(!is_consistent([3,4,undef], 0)); + assert(!is_consistent([[3,4,4],[1,2,3], [0,4,5]], [1,1])); assert(!is_consistent([[3,4,5],[3,4]])); assert(is_consistent([[3,[3,4,[5]]], [5,[2,9,[9]]]])); assert(!is_consistent([[3,[3,4,[5]]], [5,[2,9,9]]])); diff --git a/vectors.scad b/vectors.scad index f8bc2f5..9110c47 100644 --- a/vectors.scad +++ b/vectors.scad @@ -36,7 +36,7 @@ // is_vector([1,1,1],all_nonzero=false); // Returns true // is_vector([],zero=false); // Returns false function is_vector(v, length, zero, all_nonzero=false, eps=EPSILON) = - is_list(v) && len(v)>0 && []==[for(vi=v) if(!is_num(vi)) 0] + is_list(v) && len(v)>0 && 0*v==[for(vi=v) 0] && (is_undef(length) || len(v)==length) && (is_undef(zero) || ((norm(v) >= eps) == !zero)) && (!all_nonzero || all_nonzero(v)) ; @@ -128,8 +128,11 @@ function vceil(v) = // unit([0,0,0]); // Asserts an error. function unit(v, error=[[["ASSERT"]]]) = assert(is_vector(v), str("Expected a vector. Got: ",v)) - norm(v)=EPSILON,"Tried to normalize a zero vector") : error) : - v/norm(v); + norm(v)=EPSILON,"Tried to normalize a zero vector") + : error + : v/norm(v); // Function: vector_angle() diff --git a/vnf.scad b/vnf.scad index 707f1c4..f94b352 100644 --- a/vnf.scad +++ b/vnf.scad @@ -766,6 +766,7 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) = sface2 = list_rotate(face2,min2) ) if (sface1 == sface2) _vnf_validate_err("DUP_FACE", [for (i=sface1) varr[i]]) + ], issues = concat(issues, repeated_faces) ) issues? issues : @@ -895,6 +896,7 @@ function _vnf_validate_err(name, extra) = ) concat(info, [extra]); + function _pts_not_reported(pts, varr, reports) = [ for (i = pts, report = reports, pt = report[3]) @@ -940,17 +942,12 @@ module vnf_validate(vnf, size=1, show_warns=true, check_isects=false) { color([0.5,0.5,0.5,0.67]) vnf_polyhedron(vnf); } - + + // Section: VNF Transformations -// Function: vnf_halfspace() -// Usage: -// vnf_halfspace([a,b,c,d], vnf) -// Description: -// returns the intersection of the VNF with the given half-space. -// Arguments: -// halfspace = half-space to intersect with, given as the four coefficients of the affine inequation a\*x+b\*y+c\*z≥ d. +/// Internal functions function _vnf_halfspace_pts(halfspace, points, faces, inside=undef, coords=[], map=[]) = /* Recursive function to compute the intersection of points (and edges, @@ -991,6 +988,8 @@ function _vnf_halfspace_pts(halfspace, points, faces, (zi*points[j]-zj*pi)/(zi-zj)]), // map: we add the info concat(map, [[for(y=enumerate(adj2)) [y[1], n+y[0]]]])); + + function _vnf_halfspace_face(face, map, inside, i=0, newface=[], newedge=[], exit) = /* Recursive function to intersect a face of the VNF with the half-plane. @@ -1029,6 +1028,8 @@ function _vnf_halfspace_face(face, map, inside, i=0, concat(newface0, [inter]), concat(newedge, [inter]), is_undef(exit) ? inside[p] : exit); + + function _vnf_halfspace_path_search_edge(edge, paths, i=0, ret=[undef,undef]) = /* given an oriented edge [x,y] and a set of oriented paths, * returns the indices [i,j] of paths [before, after] given edge @@ -1038,6 +1039,8 @@ function _vnf_halfspace_path_search_edge(edge, paths, i=0, ret=[undef,undef]) = _vnf_halfspace_path_search_edge(edge, paths, i+1, [last(paths[i]) == edge[0] ? i : ret[0], paths[i][0] == edge[1] ? i : ret[1]]); + + function _vnf_halfspace_paths(edges, i=0, paths=[]) = /* given a set of oriented edges [x,y], returns all paths [x,y,z,..] that may be formed from these edges. @@ -1060,6 +1063,15 @@ function _vnf_halfspace_paths(edges, i=0, paths=[]) = s[0] != s[1] ? [concat(paths[s[0]], paths[s[1]])] : // edge closes a loop [concat(paths[s[0]], [e[1]])])); + + +// Function: vnf_halfspace() +// Usage: +// vnf_halfspace([a,b,c,d], vnf) +// Description: +// returns the intersection of the VNF with the given half-space. +// Arguments: +// halfspace = half-space to intersect with, given as the four coefficients of the affine inequation a\*x+b\*y+c\*z≥ d. function vnf_halfspace(_arg1=_undef, _arg2=_undef, halfspace=_undef, vnf=_undef) = // here is where we wish that OpenSCAD had array lvalues... From 11cb12b0d6d4862c90e8f4049a02ba8b09fd9505 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 28 Mar 2021 17:09:07 +0100 Subject: [PATCH 08/17] Revert "teste2" This reverts commit 46b0f03af35a8fa957b708d0a7b3e2a2b30f1906. --- affine.scad | 27 ++- arrays.scad | 35 ++-- beziers.scad | 25 +-- common.scad | 11 +- geometry.scad | 4 +- math.scad | 79 ++++----- paths.scad | 13 +- skin.scad | 373 ++++++++++++++++++++++++++--------------- tests/test_all.scad | 29 ---- tests/test_common.scad | 3 - vectors.scad | 9 +- vnf.scad | 28 +--- 12 files changed, 336 insertions(+), 300 deletions(-) delete mode 100644 tests/test_all.scad diff --git a/affine.scad b/affine.scad index 2a363e8..55c3ea3 100644 --- a/affine.scad +++ b/affine.scad @@ -82,8 +82,8 @@ function is_affine(x,dim=[2,3]) = // b = is_2d_transform(move([10,20,30])); // Returns: false // b = is_2d_transform(scale([2,3,4])); // Returns: true function is_2d_transform(t) = // z-parameters are zero, except we allow t[2][2]!=1 so scale() works - t[2][0]==0 && t[2][1]==0 && t[2][3]==0 && t[0][2] == 0 && t[1][2]==0 && - (t[2][2]==1 || !(t[0][0]==1 && t[0][1]==0 && t[1][0]==0 && t[1][1]==1)); // But rule out zscale() + t[2][0]==0 && t[2][1]==0 && t[2][3]==0 && t[0][2] == 0 && t[1][2]==0 && + (t[2][2]==1 || !(t[0][0]==1 && t[0][1]==0 && t[1][0]==0 && t[1][1]==1)); // But rule out zscale() // Function: affine2d_to_3d() @@ -234,6 +234,7 @@ function rot_decode(M) = + // Section: Affine2d 3x3 Transformation Matrices @@ -631,11 +632,19 @@ function affine3d_rot_from_to(from, to) = let( from = unit(point3d(from)), to = unit(point3d(to)) - ) - approx(from,[0,0,0]) - || approx(to,[0,0,0]) - || approx(from+to,[0,0,0])? affine3d_identity() : - affine3d_mirror(from+to) * affine3d_mirror(from); + ) approx(from,to)? affine3d_identity() : + let( + u = vector_axis(from,to), + ang = vector_angle(from,to), + c = cos(ang), + c2 = 1-c, + s = sin(ang) + ) [ + [u.x*u.x*c2+c , u.x*u.y*c2-u.z*s, u.x*u.z*c2+u.y*s, 0], + [u.y*u.x*c2+u.z*s, u.y*u.y*c2+c , u.y*u.z*c2-u.x*s, 0], + [u.z*u.x*c2-u.y*s, u.z*u.y*c2+u.x*s, u.z*u.z*c2+c , 0], + [ 0, 0, 0, 1] + ]; // Function: affine3d_frame_map() @@ -696,7 +705,8 @@ function affine3d_frame_map(x,y,z, reverse=false) = assert(ocheck, "Inputs must be orthogonal when reverse==true") [for (r=map) [for (c=r) c, 0], [0,0,0,1]] ) : [for (r=transpose(map)) [for (c=r) c, 0], [0,0,0,1]]; - + + // Function: affine3d_mirror() // Usage: @@ -865,4 +875,5 @@ function affine3d_skew_yz(ya=0, za=0) = ]; + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/arrays.scad b/arrays.scad index e6e39c4..3fd8bbc 100644 --- a/arrays.scad +++ b/arrays.scad @@ -39,11 +39,9 @@ function is_homogeneous(l, depth=10) = !is_list(l) || l==[] ? false : let( l0=l[0] ) [] == [for(i=[1:len(l)-1]) if( ! _same_type(l[i],l0, depth+1) ) 0 ]; - function is_homogenous(l, depth=10) = is_homogeneous(l, depth); - function _same_type(a,b, depth) = (depth==0) || (is_undef(a) && is_undef(b)) || @@ -52,7 +50,7 @@ function _same_type(a,b, depth) = (is_string(a) && is_string(b)) || (is_list(a) && is_list(b) && len(a)==len(b) && []==[for(i=idx(a)) if( ! _same_type(a[i],b[i],depth-1) ) 0] ); - + // Function: select() // Description: @@ -229,8 +227,6 @@ function in_list(val,list,idx) = // Usage: // idx = find_first_match(val, list, , ); // indices = find_first_match(val, list, all=true, , ); -// Topics: List Handling -// See Also: max_index(), list_increasing(), list_decreasing() // Description: // Finds the first item in `list` that matches `val`, returning the index. // Arguments: @@ -364,6 +360,7 @@ function repeat(val, n, i=0) = // If both `n` and `e` are given, returns `n` values evenly spread from `s` // to `e`, and `step` is ignored. // Arguments: +// --- // n = Desired number of values in returned list, if given. // s = Starting value. Default: 0 // e = Ending value to stop at, if given. @@ -491,7 +488,6 @@ function deduplicate(list, closed=false, eps=EPSILON) = // closed = If true, drops trailing indices if what they index matches what the first index indexes. // eps = The maximum difference to allow between numbers or vectors. // Examples: -// list = [0,1,2,3]; // a = deduplicate_indexed([8,6,4,6,3], [1,4,3,1,2,2,0,1]); // Returns: [1,4,3,2,0,1] // b = deduplicate_indexed([8,6,4,6,3], [1,4,3,1,2,2,0,1], closed=true); // Returns: [1,4,3,2,0] // c = deduplicate_indexed([[7,undef],[7,undef],[1,4],[1,4],[1,4+1e-12]],eps=0); // Returns: [0,2,4] @@ -621,7 +617,7 @@ function list_set(list=[],indices,values,dflt=0,minlen=0) = // b = list_insert([3,6,9,12],[1,3],[5,11]); // Returns [3,5,6,9,11,12] function list_insert(list, indices, values) = assert(is_list(list)) - !is_list(indices) ? + !is_list(indices) ? assert( is_finite(indices) && is_finite(values), "Invalid indices/values." ) assert( indices<=len(list), "Indices must be <= len(list) ." ) [ @@ -1291,8 +1287,6 @@ function permutations(l,n=2) = // pairs = zip(a,b); // triples = zip(a,b,c); // quads = zip([LIST1,LIST2,LIST3,LIST4]); -// Topics: List Handling, Iteration -// See Also: zip_long() // Description: // Zips together two or more lists into a single list. For example, if you have two // lists [3,4,5], and [8,7,6], and zip them together, you get [[3,8],[4,7],[5,6]]. @@ -1318,8 +1312,6 @@ function zip(a,b,c) = // pairs = zip_long(a,b); // triples = zip_long(a,b,c); // quads = zip_long([LIST1,LIST2,LIST3,LIST4]); -// Topics: List Handling, Iteration -// See Also: zip() // Description: // Zips together two or more lists into a single list. For example, if you have two // lists [3,4,5], and [8,7,6], and zip them together, you get [[3,8],[4,7],[5,6]]. @@ -1341,9 +1333,9 @@ function zip_long(a,b,c,fill) = b!=undef? zip_long([a,b,if (c!=undef) c],fill=fill) : let(n = list_longest(a)) [for (i=[0:1:n-1]) [for (x=a) i); @@ -1641,11 +1633,11 @@ function submatrix_set(M,A,m=0,n=0) = // Function: array_group() // Usage: // groups = array_group(v, , ); -// Topics: Matrices, Array Handling -// See Also: subindex(), submatrix(), hstack(), flatten(), full_flatten() // Description: // Takes a flat array of values, and groups items in sets of `cnt` length. // The opposite of this is `flatten()`. +// Topics: Matrices, Array Handling +// See Also: subindex(), submatrix(), hstack(), flatten(), full_flatten() // Arguments: // v = The list of items to group. // cnt = The number of items to put in each grouping. Default:2 @@ -1669,7 +1661,6 @@ function array_group(v, cnt=2, dflt=0) = // Arguments: // l = List to flatten. // Example: -// flatten([[1,2,3], [4,5,[6,7,8]]]) returns [1,2,3,4,5,[6,7,8]] // l = flatten([[1,2,3], [4,5,[6,7,8]]]); // returns [1,2,3,4,5,[6,7,8]] function flatten(l) = !is_list(l)? l : @@ -1819,7 +1810,7 @@ function transpose(arr, reverse=false) = // A = matrix to test // eps = epsilon for comparing equality. Default: 1e-12 function is_matrix_symmetric(A,eps=1e-12) = - approx(A,transpose(A), eps); - - + approx(A,transpose(A)); + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/beziers.scad b/beziers.scad index 3c3be95..2a74ac3 100644 --- a/beziers.scad +++ b/beziers.scad @@ -6,7 +6,6 @@ // include ////////////////////////////////////////////////////////////////////// - // Terminology: // Path = A series of points joined by straight line segements. // Bezier Curve = A mathematical curve that joins two endpoints, following a curve determined by one or more control points. @@ -378,6 +377,7 @@ function bezier_tangent(curve, u) = [for (v=res) unit(v)]; + // Function: bezier_curvature() // Usage: // crv = bezier_curvature(curve, u); @@ -407,7 +407,6 @@ function bezier_curvature(curve, u) = ]; - // Function: bezier_curve() // Usage: // path = bezier_curve(curve, n, ); @@ -417,10 +416,9 @@ function bezier_curvature(curve, u) = // Takes a list of bezier curve control points and generates n points along the bezier path. // Points start at the first control point and are sampled every `1/n`th // of the way along the bezier parameter, ending *before* the final control point by default. -// If you wish to add the endpoint you can set `endpoint` to true. -// The distance between the points will *not* be equidistant. The points are usually more -// concentrated where the curve has a greater curvature. The degree of the bezier curve is -// one less than the number of points in `curve`. +// The distance between the points will *not* be equidistant. If you wish to add the +// endpoint you can set `endpoint` to true. The degree of the bezier curve is one +// less than the number of points in `curve`. // Arguments: // curve = The list of endpoints and control points for this bezier segment. // n = The number of points to generate along the bezier curve. @@ -437,11 +435,9 @@ function bezier_curvature(curve, u) = // bez = [[0,0], [5,15], [40,20], [60,-15], [80,0]]; // move_copies(bezier_curve(bez, 8)) sphere(r=1.5, $fn=12); // trace_bezier(bez, N=len(bez)-1); -function bezier_curve(curve,n,endpoint=false) = - [each bezier_points(curve, [0:1/n:(n-0.5)/n]), - if (endpoint) curve[len(curve)-1] - ]; - +function bezier_curve(curve,n,endpoint) = [each bezier_points(curve, [0:1/n:(n-0.5)/n]), + if (endpoint) curve[len(curve)-1] + ]; // Function: bezier_segment_closest_point() // Usage: @@ -528,6 +524,7 @@ function bezier_segment_length(curve, start_u=0, end_u=1, max_deflect=0.01) = ]); + // Function: bezier_line_intersection() // Usage: // u = bezier_line_intersection(curve, line); @@ -551,6 +548,7 @@ function bezier_line_intersection(curve, line) = [for(u=real_roots(q)) if (u>=0 && u<=1) u]; + // Function: fillet3pts() // Usage: // bez_path_pts = fillet3pts(p0, p1, p2, r); @@ -663,6 +661,7 @@ function bezier_path_closest_point(path, pt, N=3, max_err=0.01, seg=0, min_seg=u ); + // Function: bezier_path_length() // Usage: // plen = bezier_path_length(path, , ); @@ -947,6 +946,7 @@ module bezier_polygon(bezier, splinesteps=16, N=3) { } + // Module: trace_bezier() // Usage: // trace_bezier(bez, , ) { @@ -1294,7 +1294,6 @@ function patch_reverse(patch) = [for (row=patch) reverse(row)]; - // Section: Bezier Surface Modules @@ -1339,6 +1338,8 @@ function bezier_surface(patches=[], splinesteps=16, vnf=EMPTY_VNF, style="defaul bezier_surface(patches=patches, splinesteps=splinesteps, vnf=vnf, style=style, i=i+1); + + // Module: trace_bezier_patches() // Usage: // trace_bezier_patches(patches, [size], [splinesteps], [showcps], [showdots], [showpatch], [convexity], [style]); diff --git a/common.scad b/common.scad index af380e4..dc2333a 100644 --- a/common.scad +++ b/common.scad @@ -125,7 +125,7 @@ function is_integer(n) = is_finite(n) && n == round(n); // bool = is_nan("foo"); // Returns: false // bool = is_nan(NAN); // Returns: true function is_nan(x) = (x!=x); - + // Function: is_finite() // Usage: @@ -214,14 +214,7 @@ function is_consistent(list, pattern) = is_list(list) && (len(list)==0 || (let(pattern = is_undef(pattern) ? _list_pattern(list[0]): _list_pattern(pattern) ) - []==[for(entry=0*list) if (entry != pattern) 0])); -// -// Note: in the event that 0*list produces a warning for non numeric /list/ in a future version, -// the last line above may be rewriten as: -// []==[for(entry=_list_pattern(list)) if (entry != pattern) entry])); - -/* -*/ + []==[for(entry=0*list) if (entry != pattern) entry])); //Internal function //Creates a list with the same structure of `list` with each of its elements replaced by 0. diff --git a/geometry.scad b/geometry.scad index b1ac0cc..b145c6f 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1072,7 +1072,6 @@ function distance_from_plane(plane, point) = let( plane = normalize_plane(plane) ) point3d(plane)* point - plane[3]; - // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. @@ -1595,6 +1594,7 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = ]; + // Function: circle_line_intersection() // Usage: // isect = circle_line_intersection(c,r,line,,); @@ -1627,6 +1627,7 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = let( offset = sqrt(r*r-d*d), uvec=unit(line[1]-line[0]) ) [closest-offset*uvec, closest+offset*uvec] + ) [for(p=isect) if ((!bounded[0] || (p-line[0])*(line[1]-line[0])>=0) @@ -1634,6 +1635,7 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = + // Section: Pointlists diff --git a/math.scad b/math.scad index 34ce5ca..bf676e4 100644 --- a/math.scad +++ b/math.scad @@ -22,7 +22,8 @@ INF = 1/0; // Constant: NAN // Description: The value `nan`, useful for comparisons. -NAN = 0/0; +NAN = acos(2); + // Section: Simple math @@ -587,9 +588,7 @@ function sum(v, dflt=0) = is_vector(v) || is_matrix(v) ? [for(i=[1:len(v)]) 1]*v : _sum(v,v[0]*0); -function _sum(v,_total,_i=0) = - _i>=len(v) ? _total : _sum(v,_total+v[_i], _i+1); - +function _sum(v,_total,_i=0) = _i>=len(v) ? _total : _sum(v,_total+v[_i], _i+1); // Function: cumsum() // Usage: @@ -986,18 +985,20 @@ function determinant(M) = // n = Is given, requires the matrix to have the given width. // square = If true, requires the matrix to have a width equal to its height. Default: false function is_matrix(A,m,n,square=false) = - is_consistent(A) - && len(A)>0 - && is_vector(A[0],n) + is_list(A) + && (( is_undef(m) && len(A) ) || len(A)==m) + && is_list(A[0]) + && (( is_undef(n) && len(A[0]) ) || len(A[0])==n) && (!square || len(A) == len(A[0])) - && ( is_undef(m) || len(A)==m ); - + && is_vector(A[0]) + && is_consistent(A); + // Function: norm_fro() // Usage: // norm_fro(A) // Description: -// Computes Frobenius norm of input matrix. The Frobenius norm is the square root of the sum of the +// Computes frobenius norm of input matrix. The frobenius norm is the square root of the sum of the // squares of all of the entries of the matrix. On vectors it is the same as the usual 2-norm. // This is an easily computed norm that is convenient for comparing two matrices. function norm_fro(A) = @@ -1005,17 +1006,6 @@ function norm_fro(A) = norm(flatten(A)); -// Function: norm_max() -// Usage: -// norm_max(v) -// Description: -// Computes maximum norm of input vector. -// The maximum norm is the maximum of the absolute values of the coordinates of the vector. -function norm_max(v) = - assert(is_vector(v) && len(v)>0, "The norm_max requires a vector with length greater than 0.") - max(max(v),max(-v)); - - // Function: matrix_trace() // Usage: // matrix_trace(M) @@ -1175,7 +1165,7 @@ function all_nonnegative(x) = // Arguments: // a = First value. // b = Second value. -// eps = The maximum allowed difference between `a` and `b` that will return true. Default: EPSILON. +// eps = The maximum allowed difference between `a` and `b` that will return true. // Example: // approx(-0.3333333333,-1/3); // Returns: true // approx(0.3333333333,1/3); // Returns: true @@ -1185,7 +1175,6 @@ function all_nonnegative(x) = function approx(a,b,eps=EPSILON) = (a==b && is_bool(a) == is_bool(b)) || (is_num(a) && is_num(b) && abs(a-b) <= eps) || - (is_vector(a) && is_vector(b,len(a)) && norm_max(a-b) <= eps ) || (is_list(a) && is_list(b) && len(a) == len(b) && [] == [for (i=idx(a)) if (!approx(a[i],b[i],eps=eps)) 1]); @@ -1496,15 +1485,9 @@ function complex(list) = // Multiplies two complex numbers, vectors or matrices, where complex numbers // or entries are represented as vectors: [REAL, IMAGINARY]. Note that all // entries in both arguments must be complex. -// Multiplies two complex numbers represented by 2D vectors. -// Returns a complex number as a 2D vector [REAL, IMAGINARY]. // Arguments: // z1 = First complex number, vector or matrix // z2 = Second complex number, vector or matrix -function c_mul(z1,z2) = - is_matrix([z1,z2],2,2) ? _c_mul(z1,z2) : - _combine_complex(_c_mul(_split_complex(z1), _split_complex(z2))); - function _split_complex(data) = is_vector(data,2) ? data @@ -1514,7 +1497,6 @@ function _split_complex(data) = [for(vec=data) vec * [0,1]] ]; - function _combine_complex(data) = is_vector(data,2) ? data : is_num(data[0][0]) ? [for(i=[0:len(data[0])-1]) [data[0][i],data[1][i]]] @@ -1522,10 +1504,13 @@ function _combine_complex(data) = [for(j=[0:1:len(data[0][0])-1]) [data[0][i][j], data[1][i][j]]]]; - function _c_mul(z1,z2) = [ z1.x*z2.x - z1.y*z2.y, z1.x*z2.y + z1.y*z2.x ]; +function c_mul(z1,z2) = + is_matrix([z1,z2],2,2) ? _c_mul(z1,z2) : + _combine_complex(_c_mul(_split_complex(z1), _split_complex(z2))); + // Function: c_div() // Usage: @@ -1553,7 +1538,6 @@ function c_conj(z) = is_vector(z,2) ? [z.x,-z.y] : [for(entry=z) c_conj(entry)]; - // Function: c_real() // Usage: // x = c_real(z) @@ -1564,7 +1548,6 @@ function c_real(z) = : is_num(z[0][0]) ? z*[1,0] : [for(vec=z) vec * [1,0]]; - // Function: c_imag() // Usage: // x = c_imag(z) @@ -1583,7 +1566,6 @@ function c_imag(z) = // Produce an n by n complex identity matrix function c_ident(n) = [for (i = [0:1:n-1]) [for (j = [0:1:n-1]) (i==j)?[1,0]:[0,0]]]; - // Function: c_norm() // Usage: // n = c_norm(z) @@ -1636,12 +1618,12 @@ function quadratic_roots(a,b,c,real=false) = // where a_n is the z^n coefficient. Polynomial coefficients are real. // The result is a number if `z` is a number and a complex number otherwise. function polynomial(p,z,k,total) = -    is_undef(k) -    ? assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) -        assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) -        polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) -    : k==len(p) ? total -    : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : c_mul(total,z)+[p[k],0]); + is_undef(k) + ? assert( is_vector(p) , "Input polynomial coefficients must be a vector." ) + assert( is_finite(z) || is_vector(z,2), "The value of `z` must be a real or a complex number." ) + polynomial( _poly_trim(p), z, 0, is_num(z) ? 0 : [0,0]) + : k==len(p) ? total + : polynomial(p,z,k+1, is_num(z) ? total*z+p[k] : c_mul(total,z)+[p[k],0]); // Function: poly_mult() // Usage: @@ -1651,14 +1633,15 @@ function polynomial(p,z,k,total) = // Given a list of polynomials represented as real coefficient lists, with the highest degree coefficient first, // computes the coefficient list of the product polynomial. function poly_mult(p,q) = -    is_undef(q) ? -        len(p)==2 - ? poly_mult(p[0],p[1]) -        : poly_mult(p[0], poly_mult(select(p,1,-1))) -    : assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult") - p*p==0 || q*q==0 - ? [0] - : _poly_trim(convolve(p,q)); + is_undef(q) ? + len(p)==2 + ? poly_mult(p[0],p[1]) + : poly_mult(p[0], poly_mult(select(p,1,-1))) + : + assert( is_vector(p) && is_vector(q),"Invalid arguments to poly_mult") + p*p==0 || q*q==0 + ? [0] + : _poly_trim(convolve(p,q)); // Function: poly_div() diff --git a/paths.scad b/paths.scad index 98a6892..5d391c3 100644 --- a/paths.scad +++ b/paths.scad @@ -989,6 +989,7 @@ module jittered_poly(path, dist=1/512) { + // Section: 3D Modules @@ -1007,19 +1008,20 @@ module jittered_poly(path, dist=1/512) { // xcopies(3) circle(3, $fn=32); // } module extrude_from_to(pt1, pt2, convexity, twist, scale, slices) { + rtp = xyz_to_spherical(pt2-pt1); translate(pt1) { - rot(from=[0,0,1],to=pt2-pt1) { - h = norm(pt2-pt1); - if (h > 0) { - linear_extrude(height=h, convexity=convexity, center=false, slices=slices, twist=twist, scale=scale) { + rotate([0, rtp[2], rtp[1]]) { + if (rtp[0] > 0) { + linear_extrude(height=rtp[0], convexity=convexity, center=false, slices=slices, twist=twist, scale=scale) { children(); } } } - } + } } + // Module: spiral_sweep() // Description: // Takes a closed 2D polygon path, centered on the XY plane, and sweeps/extrudes it along a 3D spiral path. @@ -1441,6 +1443,7 @@ function path_cut(path,cutdist,closed) = ]; + // Input `data` is a list that sums to an integer. // Returns rounded version of input data so that every // entry is rounded to an integer and the sum is the same as diff --git a/skin.scad b/skin.scad index 9f0f25c..e53def9 100644 --- a/skin.scad +++ b/skin.scad @@ -1,13 +1,11 @@ ////////////////////////////////////////////////////////////////////// // LibFile: skin.scad // Functions to skin arbitrary 2D profiles/paths in 3-space. -// To use, add the following line to the beginning of your file: -// ``` -// include -// include -// ``` // Inspired by list-comprehension-demos skin(): // - https://github.com/openscad/list-comprehension-demos/blob/master/skin.scad +// Includes: +// include +// include ////////////////////////////////////////////////////////////////////// @@ -15,16 +13,15 @@ // Function&Module: skin() // Usage: As module: -// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z], [convexity], -// [anchor],[cp],[spin],[orient],[extent]); +// skin(profiles, slices, , , , , , , , ,,,,) ; // Usage: As function: -// vnf = skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); +// vnf = skin(profiles, slices, , , , , , ); // Description: // Given a list of two or more path `profiles` in 3d space, produces faces to skin a surface between // the profiles. Optionally the first and last profiles can have endcaps, or the first and last profiles // can be connected together. Each profile should be roughly planar, but some variation is allowed. // Each profile must rotate in the same clockwise direction. If called as a function, returns a -// [VNF structure](vnf.scad) like `[VERTICES, FACES]`. If called as a module, creates a polyhedron +// [VNF structure](vnf.scad) `[VERTICES, FACES]`. If called as a module, creates a polyhedron // of the skinned profiles. // . // The profiles can be specified either as a list of 3d curves or they can be specified as @@ -36,45 +33,47 @@ // For this operation to be well-defined, the profiles must all have the same vertex count and // we must assume that profiles are aligned so that vertex `i` links to vertex `i` on all polygons. // Many interesting cases do not comply with this restriction. Two basic methods can handle -// these cases: either add points to edges (resample) so that the profiles are compatible, -// or repeat vertices. Repeating vertices allows two edges to terminate at the same point, creating -// triangular faces. You can adjust non-matching profiles yourself +// these cases: either subdivide edges (insert additional points along edges) +// or duplicate vertcies (insert edges of length 0) so that both polygons have +// the same number of points. +// Duplicating vertices allows two distinct points in one polygon to connect to a single point +// in the other one, creating +// triangular faces. You can adjust non-matching polygons yourself // either by resampling them using `subdivide_path` or by duplicating vertices using -// `repeat_entries`. It is OK to pass a profile that has the same vertex repeated, such as +// `repeat_entries`. It is OK to pass a polygon that has the same vertex repeated, such as // a square with 5 points (two of which are identical), so that it can match up to a pentagon. // Such a combination would create a triangular face at the location of the duplicated vertex. -// Alternatively, `skin` provides methods (described below) for matching up incompatible paths. +// Alternatively, `skin` provides methods (described below) for inserting additional vertices +// automatically to make incompatible paths match. // . // In order for skinned surfaces to look good it is usually necessary to use a fine sampling of // points on all of the profiles, and a large number of extra interpolated slices between the // profiles that you specify. It is generally best if the triangles forming your polyhedron // are approximately equilateral. The `slices` parameter specifies the number of slices to insert // between each pair of profiles, either a scalar to insert the same number everywhere, or a vector -// to insert a different number between each pair. To resample the profiles you can use set -// `refine=N` which will place `N` points on each edge of your profile. This has the effect of -// multiplying the number of points by N, so a profile with 8 points will have 8*N points after -// refinement. Note that when dealing with continuous curves it is always better to adjust the +// to insert a different number between each pair. +// . +// Resampling may occur, depending on the `method` parameter, to make profiles compatible. +// To force (possibly additional) resampling of the profiles to increase the point density you can set `refine=N`, which +// will multiply the number of points on your profile by `N`. You can choose between two resampling +// schemes using the `sampling` option, which you can set to `"length"` or `"segment"`. +// The length resampling method resamples proportional to length. +// The segment method divides each segment of a profile into the same number of points. +// This means that if you refine a profile with the "segment" method you will get N points +// on each edge, but if you refine a profile with the "length" method you will get new points +// distributed around the profile based on length, so small segments will get fewer new points than longer ones. +// A uniform division may be impossible, in which case the code computes an approximation, which may result +// in arbitrary distribution of extra points. See `subdivide_path` for more details. +// Note that when dealing with continuous curves it is always better to adjust the // sampling in your code to generate the desired sampling rather than using the `refine` argument. // . -// Two methods are available for resampling, `"length"` and `"segment"`. Specify them using -// the `sampling` argument. The length resampling method resamples proportional to length. -// The segment method divides each segment of a profile into the same number of points. -// A uniform division may be impossible, in which case the code computes an approximation. -// See `subdivide_path` for more details. -// -// You can choose from four methods for specifying alignment for incommensurate profiles. -// The available methods are `"distance"`, `"tangent"`, `"direct"` and `"reindex"`. +// You can choose from five methods for specifying alignment for incommensurate profiles. +// The available methods are `"distance"`, `"fast_distance"`, `"tangent"`, `"direct"` and `"reindex"`. // It is useful to distinguish between continuous curves like a circle and discrete profiles // like a hexagon or star, because the algorithms' suitability depend on this distinction. // . -// The "direct" and "reindex" methods work by resampling the profiles if necessary. As noted above, -// for continuous input curves, it is better to generate your curves directly at the desired sample size, -// but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled -// to match the circle. You can do this in two different ways using the `sampling` parameter. The default -// of `sampling="length"` approximates a uniform length sampling of the profile. The other option -// is `sampling="segment"` which attempts to place the same number of new points on each segment. -// If the segments are of varying length, this will produce a different result. Note that "direct" is -// the default method. If you simply supply a list of compatible profiles it will link them up +// The default method for aligning profiles is `method="direct"`. +// If you simply supply a list of compatible profiles it will link them up // exactly as you have provided them. You may find that profiles you want to connect define the // right shapes but the point lists don't start from points that you want aligned in your skinned // polyhedron. You can correct this yourself using `reindex_polygon`, or you can use the "reindex" @@ -82,22 +81,51 @@ // in the polyhedron---in will produce the least twisted possible result. This algorithm has quadratic // run time so it can be slow with very large profiles. // . -// The "distance" and "tangent" methods are work by duplicating vertices to create -// triangular faces. The "distance" method finds the global minimum distance method for connecting two -// profiles. This algorithm generally produces a good result when both profiles are discrete ones with +// When the profiles are incommensurate, the "direct" and "reindex" resample them to match. As noted above, +// for continuous input curves, it is better to generate your curves directly at the desired sample size, +// but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled +// to match the circle. When you use "direct" or "reindex" the default `sampling` value is +// of `sampling="length"` to approximate a uniform length sampling of the profile. This will generally +// produce the natural result for connecting two continuously sampled profiles or a continuous +// profile and a polygonal one. However depending on your particular case, +// `sampling="segment"` may produce a more pleasing result. These two approaches differ only when +// the segments of your input profiles have unequal length. +// . +// The "distance", "fast_distance" and "tangent" methods work by duplicating vertices to create +// triangular faces. In the skined object created by two polygons, every vertex of a polygon must +// have an edge that connects to some vertex on the other one. If you connect two squares this can be +// accomplished with four edges, but if you want to connect a square to a pentagon you must add a +// fifth edge for the "extra" vertex on the pentagon. You must now decide which vertex on the square to +// connect the "extra" edge to. How do you decide where to put that fifth edge? The "distance" method answers this +// question by using an optimization: it minimizes the total length of all the edges connecting +// the two polygons. This algorithm generally produces a good result when both profiles are discrete ones with // a small number of vertices. It is computationally intensive (O(N^3)) and may be -// slow on large inputs. The resulting surfaces generally have curves faces, so be -// sure to select a sufficiently large value for `slices` and `refine`. +// slow on large inputs. The resulting surfaces generally have curved faces, so be +// sure to select a sufficiently large value for `slices` and `refine`. Note that for +// this method, `sampling` must be set to `"segment"`, and hence this is the default setting. +// Using sampling by length would ignore the repeated vertices and ruin the alignment. +// The "fast_distance" method restricts the optimization by assuming that an edge should connect +// vertex 0 of the two polygons. This reduces the run time to O(N^2) and makes +// the method usable on profiles with more points if you take care to index the inputs to match. +// . // The `"tangent"` method generally produces good results when -// connecting a discrete polygon to a convex, finely sampled curve. It works by finding -// a plane that passed through each edge of the polygon that is tangent to -// the curve. It may fail if the curved profile is non-convex, or doesn't have enough points to distinguish -// all of the tangent points from each other. It connects all of the points of the curve to the corners of the discrete -// polygon using triangular faces. Using `refine` with this method will have little effect on the model, so +// connecting a discrete polygon to a convex, finely sampled curve. Given a polygon and a curve, consider one edge +// on the polygon. Find a plane passing through the edge that is tangent to the curve. The endpoints of the edge and +// the point of tangency define a triangular face in the output polyhedron. If you work your way around the polygon +// edges, you can establish a series of triangular faces in this way, with edges linking the polygon to the curve. +// You can then complete the edge assignment by connecting all the edges in between the triangular faces together, +// with many edges meeting at each polygon vertex. The result is an alternation of flat triangular faces with conical +// curves joining them. Another way to think about it is that it splits the points on the curve up into groups and +// connects all the points in one group to the same vertex on the polygon. +// . +// The "tangent" method may fail if the curved profile is non-convex, or doesn't have enough points to distinguish +// all of the tangent points from each other. The algorithm treats whichever input profile has fewer points as the polygon +// and the other one as the curve. Using `refine` with this method will have little effect on the model, so // you should do it only for agreement with other profiles, and these models are linear, so extra slices also -// have no effect. For best efficiency set `refine=1` and `slices=0`. When you use refinement with either -// of these methods, it is always the "segment" based resampling described above. This is necessary because -// sampling by length will ignore the repeated vertices and break the alignment. +// have no effect. For best efficiency set `refine=1` and `slices=0`. As with the "distance" method, refinement +// must be done using the "segment" sampling scheme to preserve alignment across duplicated points. +// Note that the "tangent" method produces similar results to the "distance" method on curved inputs. If this +// method fails due to concavity, "fast_distance" may be a good option. // . // It is possible to specify `method` and `refine` as arrays, but it is important to observe // matching rules when you do this. If a pair of profiles is connected using "tangent" or "distance" @@ -111,11 +139,12 @@ // Arguments: // profiles = list of 2d or 3d profiles to be skinned. (If 2d must also give `z`.) // slices = scalar or vector number of slices to insert between each pair of profiles. Set to zero to use only the profiles you provided. Recommend starting with a value around 10. -// refine = resample profiles to this number of points per edge. Can be a list to give a refinement for each profile. Recommend using a value above 10 when using the "distance" method. Default: 1. -// sampling = sampling method to use with "direct" and "reindex" methods. Can be "length" or "segment". Ignored if any profile pair uses either the "distance" or "tangent" methods. Default: "length". +// --- +// refine = resample profiles to this number of points per edge. Can be a list to give a refinement for each profile. Recommend using a value above 10 when using the "distance" or "fast_distance" methods. Default: 1. +// sampling = sampling method to use with "direct" and "reindex" methods. Can be "length" or "segment". Ignored if any profile pair uses either the "distance", "fast_distance", or "tangent" methods. Default: "length". // closed = set to true to connect first and last profile (to make a torus). Default: false // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. -// method = method for connecting profiles, one of "distance", "tangent", "direct" or "reindex". Default: "direct". +// method = method for connecting profiles, one of "distance", "fast_distance", "tangent", "direct" or "reindex". Default: "direct". // z = array of height values for each profile if the profiles are 2d // convexity = convexity setting for use with polyhedron. (module only) Default: 10 // anchor = Translate so anchor point is at the origin. (module only) Default: "origin" @@ -132,21 +161,21 @@ // Example: Offsetting the starting edge connects to circles in an interesting way: // circ = circle($fn=80, r=3); // skin([circ, rot(110,p=circ)], z=[0,5], slices=20); -// Example(FlatSpin): +// Example(FlatSpin,VPD=20): // skin([ yrot(37,p=path3d(circle($fn=128, r=4))), path3d(square(3),3)], method="reindex",slices=10); -// Example(FlatSpin): Ellipses connected with twist +// Example(FlatSpin,VPD=16): Ellipses connected with twist // ellipse = xscale(2.5,p=circle($fn=80)); // skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10); -// Example(FlatSpin): Ellipses connected without a twist. (Note ellipses stay in the same position: just the connecting edges are different.) +// Example(FlatSpin,VPD=16): Ellipses connected without a twist. (Note ellipses stay in the same position: just the connecting edges are different.) // ellipse = xscale(2.5,p=circle($fn=80)); // skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10, method="reindex"); -// Example(FlatSpin): +// Example(FlatSpin,VPD=500): // $fn=24; // skin([ // yrot(0, p=yscale(2,p=path3d(circle(d=75)))), // [[40,0,100], [35,-15,100], [20,-30,100],[0,-40,100],[-40,0,100],[0,40,100],[20,30,100], [35,15,100]] // ],slices=10); -// Example(FlatSpin): +// Example(FlatSpin,VPD=600): // $fn=48; // skin([ // for (b=[0,90]) [ @@ -200,14 +229,14 @@ // skin([for (i=[0:layers-1]) zrot(-30*i,p=path3d(hexagon(ir=r),i*height/layers))],slices=0); // up(height/layers) cylinder(r=holeradius, h=height); // } -// Example(FlatSpin): A box that is octagonal on the outside and circular on the inside +// Example(FlatSpin,VPD=300): A box that is octagonal on the outside and circular on the inside // height = 45; // sub_base = octagon(d=71, rounding=2, $fn=128); // base = octagon(d=75, rounding=2, $fn=128); // interior = regular_ngon(n=len(base), d=60); // right_half() // skin([ sub_base, base, base, sub_base, interior], z=[0,2,height, height, 2], slices=0, refine=1, method="reindex"); -// Example: Connecting a pentagon and circle with the "tangent" method produces triangular faces. +// Example: Connecting a pentagon and circle with the "tangent" method produces large triangular faces and cone shaped corners. // skin([pentagon(4), circle($fn=80,r=2)], z=[0,3], slices=10, method="tangent"); // Example: rounding corners of a square. Note that `$fn` makes the number of points constant, and avoiding the `rounding=0` case keeps everything simple. In this case, the connections between profiles are linear, so there is no benefit to setting `slices` bigger than zero. // shapes = [for(i=[.01:.045:2])zrot(-i*180/2,cp=[-8,0,0],p=xrot(90,p=path3d(regular_ngon(n=4, side=4, rounding=i, $fn=64))))]; @@ -218,53 +247,53 @@ // Example: You can fix it by specifying "tangent" for the first method, but you still need "direct" for the rest. // shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)]; // skin(shapes, slices=0, method=concat(["tangent"],repeat("direct",len(shapes)-2))); -// Example(FlatSpin): Connecting square to pentagon using "direct" method. +// Example(FlatSpin,VPD=35): Connecting square to pentagon using "direct" method. // skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10); -// Example(FlatSpin): Connecting square to shifted pentagon using "direct" method. +// Example(FlatSpin,VPD=35): Connecting square to shifted pentagon using "direct" method. // skin([regular_ngon(n=4, r=4), right(4,p=regular_ngon(n=5,r=5))], z=[0,4], refine=10, slices=10); -// Example(FlatSpin): To improve the look, you can actually rotate the polygons for a more symmetric pattern of lines. You have to resample yourself before calling `align_polygon` and you should choose a length that is a multiple of both polygon lengths. +// Example(FlatSpin,VPD=35): To improve the look, you can actually rotate the polygons for a more symmetric pattern of lines. You have to resample yourself before calling `align_polygon` and you should choose a length that is a multiple of both polygon lengths. // sq = subdivide_path(regular_ngon(n=4, r=4),40); // pent = subdivide_path(regular_ngon(n=5,r=5),40); // skin([sq, align_polygon(sq,pent,[0:1:360/5])], z=[0,4], slices=10); -// Example(FlatSpin): For the shifted pentagon we can also align, making sure to pass an appropriate centerpoint to `align_polygon`. +// Example(FlatSpin,VPD=35): For the shifted pentagon we can also align, making sure to pass an appropriate centerpoint to `align_polygon`. // sq = subdivide_path(regular_ngon(n=4, r=4),40); // pent = right(4,p=subdivide_path(regular_ngon(n=5,r=5),40)); // skin([sq, align_polygon(sq,pent,[0:1:360/5],cp=[4,0])], z=[0,4], refine=10, slices=10); -// Example(FlatSpin): The "distance" method is a completely different approach. +// Example(FlatSpin,VPD=35): The "distance" method is a completely different approach. // skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10, method="distance"); -// Example(FlatSpin): Connecting pentagon to heptagon inserts two triangular faces on each side +// Example(FlatSpin,VPD=35,VPT=[0,0,4]): Connecting pentagon to heptagon inserts two triangular faces on each side // small = path3d(circle(r=3, $fn=5)); // big = up(2,p=yrot( 0,p=path3d(circle(r=3, $fn=7), 6))); // skin([small,big],method="distance", slices=10, refine=10); -// Example(FlatSpin): But just a slight rotation of the top profile moves the two triangles to one end +// Example(FlatSpin,VPD=35,VPT=[0,0,4]): But just a slight rotation of the top profile moves the two triangles to one end // small = path3d(circle(r=3, $fn=5)); // big = up(2,p=yrot(14,p=path3d(circle(r=3, $fn=7), 6))); // skin([small,big],method="distance", slices=10, refine=10); -// Example(FlatSpin): Another "distance" example: +// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Another "distance" example: // off = [0,2]; // shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]); // rshape = rot(180,cp=centroid(shape)+off, p=shape); // skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15); -// Example(FlatSpin): Slightly shifting the profile changes the optimal linkage +// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Slightly shifting the profile changes the optimal linkage // off = [0,1]; // shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]); // rshape = rot(180,cp=centroid(shape)+off, p=shape); // skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15); -// Example(FlatSpin): This optimal solution doesn't look terrible: +// Example(FlatSpin,VPD=444,VPT=[0,0,50]): This optimal solution doesn't look terrible: // prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]); // prof2 = path3d(regular_ngon(n=7, r=50),100); // skin([prof1, prof2], method="distance", slices=10, refine=10); -// Example(FlatSpin): But this one looks better. The "distance" method doesn't find it because it uses two more edges, so it clearly has a higher total edge distance. We force it by doubling the first two vertices of one of the profiles. +// Example(FlatSpin,VPD=444,VPT=[0,0,50]): But this one looks better. The "distance" method doesn't find it because it uses two more edges, so it clearly has a higher total edge distance. We force it by doubling the first two vertices of one of the profiles. // prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]); // prof2 = path3d(regular_ngon(n=7, r=50),100); // skin([repeat_entries(prof1,[2,2,1,1,1,1,1]), // prof2], // method="distance", slices=10, refine=10); -// Example(FlatSpin): The "distance" method will often produces results similar to the "tangent" method if you use it with a polygon and a curve, but the results can also look like this: +// Example(FlatSpin,VPD=80,VPT=[0,0,7]): The "distance" method will often produces results similar to the "tangent" method if you use it with a polygon and a curve, but the results can also look like this: // skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))], method="distance", slices=0); -// Example(FlatSpin): Using the "tangent" method produces: +// Example(FlatSpin,VPD=80,VPT=[0,0,7]): Using the "tangent" method produces: // skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))], method="tangent", slices=0); -// Example(FlatSpin): Torus using hexagons and pentagons, where `closed=true` +// Example(FlatSpin,VPD=74): Torus using hexagons and pentagons, where `closed=true` // hex = right(7,p=path3d(hexagon(r=3))); // pent = right(7,p=path3d(pentagon(r=3))); // N=5; @@ -287,7 +316,7 @@ // rot(17,p=regular_ngon(n=6, r=3)), // rot(37,p=regular_ngon(n=4, r=3))], // z=[0,2,4,6,9], method="distance", slices=10, refine=10); -// Example(FlatSpin): Vertex count of the polygon changes at every profile +// Example(FlatSpin,VPD=935,VPT=[75,0,123]): Vertex count of the polygon changes at every profile // skin([ // for (ang = [0:10:90]) // rot([0,ang,0], cp=[200,0,0], p=path3d(circle(d=100,$fn=12-(ang/10)))) @@ -366,7 +395,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close assert(len(bad)==0, str("Profiles ",bad," are not a paths or have length less than 3")) let( profcount = len(profiles) - (closed?0:1), - legal_methods = ["direct","reindex","distance","tangent"], + legal_methods = ["direct","reindex","distance","fast_distance","tangent"], caps = is_def(caps) ? caps : closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), @@ -394,13 +423,15 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close assert(methodlistok==[], str("method list contains invalid method at ",methodlistok)) assert(len(method) == profcount,"Method list is the wrong length") assert(in_list(sampling,["length","segment"]), "sampling must be set to \"length\" or \"segment\"") - assert(sampling=="segment" || (!in_list("distance",method) && !in_list("tangent",method)), "sampling is set to \"length\" which is only allowed iwith methods \"direct\" and \"reindex\"") + assert(sampling=="segment" || (!in_list("distance",method) && !in_list("fast_distance",method) && !in_list("tangent",method)), "sampling is set to \"length\" which is only allowed with methods \"direct\" and \"reindex\"") assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") let( profile_dim=array_dim(profiles,2), + profiles_zcheck = (profile_dim != 2) || (profile_dim==2 && is_list(z) && len(z)==len(profiles)), profiles_ok = (profile_dim==2 && is_list(z) && len(z)==len(profiles)) || profile_dim==3 ) + assert(profiles_zcheck, "z parameter is invalid or has the wrong length.") assert(profiles_ok,"Profiles must all be 3d or must all be 2d, with matching length z parameter.") assert(is_undef(z) || profile_dim==2, "Do not specify z with 3d profiles") assert(profile_dim==3 || len(z)==len(profiles),"Length of z does not match length of profiles.") @@ -439,6 +470,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close let( pair = method[i]=="distance" ? _skin_distance_match(profiles[i],select(profiles,i+1)) : + method[i]=="fast_distance" ? _skin_aligned_distance_match(profiles[i], select(profiles,i+1)) : method[i]=="tangent" ? _skin_tangent_match(profiles[i],select(profiles,i+1)) : /*method[i]=="reindex" || method[i]=="direct" ?*/ let( p1 = subdivide_path(profiles[i],max_list[i], method=sampling), @@ -510,8 +542,9 @@ function _skin_core(profiles, caps) = // Function: subdivide_and_slice() +// Topics: Paths, Path Subdivision // Usage: -// subdivide_and_slice(profiles, slices, [numpoints], [method], [closed]) +// newprof = subdivide_and_slice(profiles, slices, , , ); // Description: // Subdivides the input profiles to have length `numpoints` where `numpoints` must be at least as // big as the largest input profile. By default `numpoints` is set equal to the length of the @@ -537,9 +570,41 @@ function subdivide_and_slice(profiles, slices, numpoints, method="length", close slice_profiles(fixpoly, slices, closed); -// Function: slice_profiles() +// Function: subdivide_long_segments() +// Topics: Paths, Path Subdivision +// See Also: subdivide_path(), subdivide_and_slice(), path_add_jitter(), jittered_poly() // Usage: -// profs = slice_profiles(profiles,slices,); +// spath = subdivide_long_segments(path, maxlen, ); +// Description: +// Evenly subdivides long `path` segments until they are all shorter than `maxlen`. +// Arguments: +// path = The path to subdivide. +// maxlen = The maximum allowed path segment length. +// --- +// closed = If true, treat path like a closed polygon. Default: true +// Example: +// path = pentagon(d=100); +// spath = subdivide_long_segments(path, 10, closed=true); +// stroke(path); +// color("lightgreen") move_copies(path) circle(d=5,$fn=12); +// color("blue") move_copies(spath) circle(d=3,$fn=12); +function subdivide_long_segments(path, maxlen, closed=false) = + assert(is_path(path)) + assert(is_finite(maxlen)) + assert(is_bool(closed)) + [ + for (p=pair(path,closed)) let( + steps = ceil(norm(p[1]-p[0])/maxlen) + ) each lerp(p[0],p[1],[0:1/steps:1-EPSILON]), + if (!closed) last(path) + ]; + + + +// Function: slice_profiles() +// Topics: Paths, Path Subdivision +// Usage: +// profs = slice_profiles(profiles, slices, ); // Description: // Given an input list of profiles, linearly interpolate between each pair to produce a // more finely sampled list. The parameters `slices` specifies the number of slices to @@ -665,18 +730,18 @@ function _dp_extract_map(map) = if (i==0 && j==0) each [smallmap,bigmap]]; -// Internal Function: _skin_distance_match(poly1,poly2) -// Usage: -// polys = _skin_distance_match(poly1,poly2); -// Description: -// Find a way of associating the vertices of poly1 and vertices of poly2 -// that minimizes the sum of the length of the edges that connect the two polygons. -// Polygons can be in 2d or 3d. The algorithm has cubic run time, so it can be -// slow if you pass large polygons. The output is a pair of polygons with vertices -// duplicated as appropriate to be used as input to `skin()`. -// Arguments: -// poly1 = first polygon to match -// poly2 = second polygon to match +/// Internal Function: _skin_distance_match(poly1,poly2) +/// Usage: +/// polys = _skin_distance_match(poly1,poly2); +/// Description: +/// Find a way of associating the vertices of poly1 and vertices of poly2 +/// that minimizes the sum of the length of the edges that connect the two polygons. +/// Polygons can be in 2d or 3d. The algorithm has cubic run time, so it can be +/// slow if you pass large polygons. The output is a pair of polygons with vertices +/// duplicated as appropriate to be used as input to `skin()`. +/// Arguments: +/// poly1 = first polygon to match +/// poly2 = second polygon to match function _skin_distance_match(poly1,poly2) = let( swap = len(poly1)>len(poly2), @@ -709,21 +774,37 @@ function _skin_distance_match(poly1,poly2) = newbig = polygon_shift(repeat_entries(map_poly[1],unique_count(bigmap)[1]),bigshift) ) swap ? [newbig, newsmall] : [newsmall,newbig]; -// + + +// This function associates vertices but with the assumption that index 0 is associated between the +// two inputs. This gives only quadratic run time. As above, output is pair of polygons with +// vertices duplicated as suited to use as input to skin(). + +function _skin_aligned_distance_match(poly1, poly2) = + let( + result = _dp_distance_array(poly1, poly2, abort_thresh=1/0), + map = _dp_extract_map(result[1]), + shift0 = len(map[0]) - max(max_index(map[0],all=true))-1, + shift1 = len(map[1]) - max(max_index(map[1],all=true))-1, + new0 = polygon_shift(repeat_entries(poly1,unique_count(map[0])[1]),shift0), + new1 = polygon_shift(repeat_entries(poly2,unique_count(map[1])[1]),shift1) + ) + [new0,new1]; + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Internal Function: _skin_tangent_match() -// Usage: -// x = _skin_tangent_match(poly1, poly2) -// Description: -// Finds a mapping of the vertices of the larger polygon onto the smaller one. Whichever input is the -// shorter path is the polygon, and the longer input is the curve. For every edge of the polygon, the algorithm seeks a plane that contains that -// edge and is tangent to the curve. There will be more than one such point. To choose one, the algorithm centers the polygon and curve on their centroids -// and chooses the closer tangent point. The algorithm works its way around the polygon, computing a series of tangent points and then maps all of the -// points on the curve between two tangent points into one vertex of the polygon. This algorithm can fail if the curve has too few points or if it is concave. -// Arguments: -// poly1 = input polygon -// poly2 = input polygon +/// Internal Function: _skin_tangent_match() +/// Usage: +/// x = _skin_tangent_match(poly1, poly2) +/// Description: +/// Finds a mapping of the vertices of the larger polygon onto the smaller one. Whichever input is the +/// shorter path is the polygon, and the longer input is the curve. For every edge of the polygon, the algorithm seeks a plane that contains that +/// edge and is tangent to the curve. There will be more than one such point. To choose one, the algorithm centers the polygon and curve on their centroids +/// and chooses the closer tangent point. The algorithm works its way around the polygon, computing a series of tangent points and then maps all of the +/// points on the curve between two tangent points into one vertex of the polygon. This algorithm can fail if the curve has too few points or if it is concave. +/// Arguments: +/// poly1 = input polygon +/// poly2 = input polygon function _skin_tangent_match(poly1, poly2) = let( swap = len(poly1)>len(poly2), @@ -757,7 +838,7 @@ function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) = // Function: associate_vertices() // Usage: -// associate_vertices(polygons, split) +// newpoly = associate_vertices(polygons, split); // Description: // Takes as input a list of polygons and duplicates specified vertices in each polygon in the list through the series so // that the input can be passed to `skin()`. This allows you to decide how the vertices are linked up rather than accepting @@ -773,26 +854,26 @@ function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) = // Arguments: // polygons = list of polygons to split // split = list of lists of split vertices -// Example(FlatSpin): If you skin together a square and hexagon using the optimal distance method you get two triangular faces on opposite sides: +// Example(FlatSpin,VPD=17,VPT=[0,0,2]): If you skin together a square and hexagon using the optimal distance method you get two triangular faces on opposite sides: // sq = regular_ngon(4,side=2); // hex = apply(rot(15),hexagon(side=2)); // skin([sq,hex], slices=10, refine=10, method="distance", z=[0,4]); -// Example(FlatSpin): Using associate_vertices you can change the location of the triangular faces. Here they are connect to two adjacent vertices of the square: +// Example(FlatSpin,VPD=17,VPT=[0,0,2]): Using associate_vertices you can change the location of the triangular faces. Here they are connect to two adjacent vertices of the square: // sq = regular_ngon(4,side=2); // hex = apply(rot(15),hexagon(side=2)); // skin(associate_vertices([sq,hex],[[1,2]]), slices=10, refine=10, sampling="segment", z=[0,4]); -// Example(FlatSpin): Here the two triangular faces connect to a single vertex on the square. Note that we had to rotate the hexagon to line them up because the vertices match counting forward, so in this case vertex 0 of the square matches to vertices 0, 1, and 2 of the hexagon. +// Example(FlatSpin,VPD=17,VPT=[0,0,2]): Here the two triangular faces connect to a single vertex on the square. Note that we had to rotate the hexagon to line them up because the vertices match counting forward, so in this case vertex 0 of the square matches to vertices 0, 1, and 2 of the hexagon. // sq = regular_ngon(4,side=2); // hex = apply(rot(60),hexagon(side=2)); // skin(associate_vertices([sq,hex],[[0,0]]), slices=10, refine=10, sampling="segment", z=[0,4]); -// Example: This example shows several polygons, with only a single vertex split at each step: +// Example(3D): This example shows several polygons, with only a single vertex split at each step: // sq = regular_ngon(4,side=2); // pent = pentagon(side=2); // hex = hexagon(side=2); // sep = regular_ngon(7,side=2); // profiles = associate_vertices([sq,pent,hex,sep], [1,3,4]); // skin(profiles ,slices=10, refine=10, method="distance", z=[0,2,4,6]); -// Example: The polygons cannot shrink, so if you want to have decreasing polygons you'll need to concatenate multiple results. Note that it is perfectly ok to duplicate a profile as shown here, where the pentagon is duplicated: +// Example(3D): The polygons cannot shrink, so if you want to have decreasing polygons you'll need to concatenate multiple results. Note that it is perfectly ok to duplicate a profile as shown here, where the pentagon is duplicated: // sq = regular_ngon(4,side=2); // pent = pentagon(side=2); // grow = associate_vertices([sq,pent], [1]); @@ -802,8 +883,7 @@ function associate_vertices(polygons, split, curpoly=0) = curpoly==len(polygons)-1 ? polygons : let( polylen = len(polygons[curpoly]), - cursplit = force_list(split[curpoly]), - fdsa= echo(cursplit=cursplit) + cursplit = force_list(split[curpoly]) ) assert(len(split)==len(polygons)-1,str(split,"Split list length mismatch: it has length ", len(split)," but must have length ",len(polygons)-1)) assert(polylen<=len(polygons[curpoly+1]),str("Polygon ",curpoly," has more vertices than the next one.")) @@ -823,7 +903,7 @@ function associate_vertices(polygons, split, curpoly=0) = // Function&Module: sweep() // Usage: As Module -// sweep(shape, transforms, , ) +// sweep(shape, transforms, , , , , , , ) ; // Usage: As Function // vnf = sweep(shape, transforms, , ); // Description: @@ -845,6 +925,7 @@ function associate_vertices(polygons, split, curpoly=0) = // transforms = list of 4x4 matrices to apply // closed = set to true to form a closed (torus) model. Default: false // caps = true to create endcap faces when closed is false. Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array. Default is true if closed is false. +// --- // convexity = convexity setting for use with polyhedron. (module only) Default: 10 // anchor = Translate so anchor point is at the origin. (module only) Default: "origin" // spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 @@ -914,10 +995,11 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // Function&Module: path_sweep() -// Usage: -// path_sweep(shape, path, [method], [normal], [closed], [twist], [twist_by_length], [symmetry], [last_normal], [tangent], [relaxed], [caps], [convexity], [transforms]) +// Usage: As module +// path_sweep(shape, path, , , , , , , , , , , , , , , , , ) ; +// vnf = path_sweep(shape, path, , , , , , , , , , , , ); // Description: -// Takes as input a 2D polygon path or region, and a 2d or 3d path and constructs a polyhedron by sweeping the shape along the path. +// Takes as input a 2D polygon path, and a 2d or 3d path and constructs a polyhedron by sweeping the shape along the path. // When run as a module returns the polyhedron geometry. When run as a function returns a VNF by default or if you set `transforms=true` // then it returns a list of transformations suitable as input to `sweep`. // . @@ -967,6 +1049,7 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // shape = A 2D polygon path or region describing the shape to be swept. // path = 2D or 3D path giving the path to sweep over // method = one of "incremental", "natural" or "manual". Default: "incremental" +// --- // normal = normal vector for initializing the incremental method, or for setting normals with method="manual". Default: UP if the path makes an angle lower than 45 degrees to the xy plane, BACK otherwise. // closed = path is a closed loop. Default: false // twist = amount of twist to add in degrees. For closed sweeps must be a multiple of 360/symmetry. Default: 0 @@ -1140,7 +1223,7 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; // path_sweep(ushape, knot_path, closed=true, method="natural"); // Example: knot with twist. Note if you twist it the other direction the center section untwists because of the natural twist there. Also compare to the "incremental" method which has less twist in the center. -// function knot(a,b,t) = // rolling knot +// function knot(a,b,t) = // rolling knot // [ a * cos (3 * t) / (1 - b* sin (2 *t)), // a * sin( 3 * t) / (1 - b* sin (2 *t)), // 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))]; @@ -1153,10 +1236,10 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // [ a * cos (3 * t) / (1 - b* sin (2 *t)), // a * sin( 3 * t) / (1 - b* sin (2 *t)), // 1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))]; -// a = 0.8; b = sqrt (1 - a * a); -// ksteps = 400; -// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)]; -// path_sweep(subdivide_path(pentagon(r=12),30), knot_path, closed=true, twist=-360*8, symmetry=5, method="natural", twist_by_length=false); +// a = 0.8; b = sqrt (1 - a * a); +// ksteps = 400; +// knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)]; +// path_sweep(subdivide_path(pentagon(r=12),30), knot_path, closed=true, twist=-360*8, symmetry=5, method="natural", twist_by_length=false); // Example: This torus knot example comes from list-comprehension-demos. The knot lies on the surface of a torus. When we use the "natural" method the swept figure is angled compared to the surface of the torus because the curve doesn't follow geodesics of the torus. // function knot(phi,R,r,p,q) = // [ (r * cos(q * phi) + R) * cos(p * phi), @@ -1166,7 +1249,7 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // points = 50; // points per loop // R = 400; r = 150; // Torus size // p = 2; q = 5; // Knot parameters -// %torus(r=R,r2=r); +// %torus(r_maj=R,r_min=r); // k = max(p,q) / gcd(p,q) * points; // knot_path = [ for (i=[0:k-1]) knot(360*i/k/gcd(p,q),R,r,p,q) ]; // path_sweep(rot(90,p=ushape),knot_path, method="natural", closed=true); @@ -1183,7 +1266,7 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // points = 50; // points per loop // R = 400; r = 150; // Torus size // p = 2; q = 5; // Knot parameters -// %torus(r=R,r2=r); +// %torus(r_maj=R,r_min=r); // k = max(p,q) / gcd(p,q) * points; // knot_path = [ for (i=[0:k-1]) knot(360*i/k/gcd(p,q),R,r,p,q) ]; // normals = [ for (i=[0:k-1]) knot_normal(360*i/k/gcd(p,q),R,r,p,q) ]; @@ -1195,6 +1278,18 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))]; // inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))]; // sweep(shape, concat(outside,inside),closed=true); +// Example: Using path_sweep on a region +// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)]; +// rgn2 = [square(30,center=false)]; +// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))]; +// mrgn = union(rgn1,rgn2); +// orgn = difference(mrgn,rgn3); +// path_sweep(orgn,arc(r=40,angle=180)); +// Example: A region with a twist +// region = [for(i=pentagon(5)) move(i,p=circle(r=2,$fn=25))]; +// path_sweep(region, +// circle(r=16,$fn=75),closed=true, +// twist=360/5*2,symmetry=5); module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10, anchor="origin",cp,spin=0, orient=UP, extent=false) @@ -1214,7 +1309,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi assert(!closed || twist % (360/symmetry)==0, str("For a closed sweep, twist must be a multiple of 360/symmetry = ",360/symmetry)) assert(closed || symmetry==1, "symmetry must be 1 when closed is false") assert(is_integer(symmetry) && symmetry>0, "symmetry must be a positive integer") - assert(is_path(shape,2) || is_region(shape), "shape must be a 2d path or region.") +// let(shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape")) assert(is_path(path), "input path is not a path") assert(!closed || !approx(path[0],select(path,-1)), "Closed path includes start point at the end") let( @@ -1241,7 +1336,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi let(rotations = [for( i = 0, ynormal = normal - (normal * tangents[0])*tangents[0], - rotation = affine_frame_map(y=ynormal, z=tangents[0]) + rotation = affine3d_frame_map(y=ynormal, z=tangents[0]) ; i < len(tangents) + (closed?1:0) ; rotation = i, ) +// Usage: as module +// path_sweep2d(shape, path, , , , , , , , , ) ; +// Usage: as function +// vnf = path_sweep2d(shape, path, , , ); // Description: // Takes an input 2D polygon (the shape) and a 2d path and constructs a polyhedron by sweeping the shape along the path. // When run as a module returns the polyhedron geometry. When run as a function returns a VNF. @@ -1320,6 +1417,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi // closed = path is a closed loop. Default: false // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. // quality = quality of offset used in calculation. Default: 1 +// --- // convexity = convexity parameter for polyhedron (module only) Default: 10 // anchor = Translate so anchor point is at the origin. (module only) Default: "origin" // spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 @@ -1347,7 +1445,8 @@ function path_sweep2d(shape, path, closed=false, caps, quality=1) = caps = is_def(caps) ? caps : closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), - fullcaps = is_bool(caps) ? [caps,caps] : caps + fullcaps = is_bool(caps) ? [caps,caps] : caps, + shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape") ) assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") diff --git a/tests/test_all.scad b/tests/test_all.scad deleted file mode 100644 index 72a552d..0000000 --- a/tests/test_all.scad +++ /dev/null @@ -1,29 +0,0 @@ - -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include -include - - diff --git a/tests/test_common.scad b/tests/test_common.scad index 2121489..3d150d7 100644 --- a/tests/test_common.scad +++ b/tests/test_common.scad @@ -221,13 +221,10 @@ module test_is_consistent() { assert(is_consistent([])); assert(is_consistent([[],[]])); assert(is_consistent([3,4,5])); - assert(is_consistent([3,4,5], 1)); assert(is_consistent([[3,4],[4,5],[6,7]])); assert(is_consistent([[[3],4],[[4],5]])); assert(!is_consistent(5)); assert(!is_consistent(undef)); - assert(!is_consistent([3,4,undef], 0)); - assert(!is_consistent([[3,4,4],[1,2,3], [0,4,5]], [1,1])); assert(!is_consistent([[3,4,5],[3,4]])); assert(is_consistent([[3,[3,4,[5]]], [5,[2,9,[9]]]])); assert(!is_consistent([[3,[3,4,[5]]], [5,[2,9,9]]])); diff --git a/vectors.scad b/vectors.scad index 9110c47..f8bc2f5 100644 --- a/vectors.scad +++ b/vectors.scad @@ -36,7 +36,7 @@ // is_vector([1,1,1],all_nonzero=false); // Returns true // is_vector([],zero=false); // Returns false function is_vector(v, length, zero, all_nonzero=false, eps=EPSILON) = - is_list(v) && len(v)>0 && 0*v==[for(vi=v) 0] + is_list(v) && len(v)>0 && []==[for(vi=v) if(!is_num(vi)) 0] && (is_undef(length) || len(v)==length) && (is_undef(zero) || ((norm(v) >= eps) == !zero)) && (!all_nonzero || all_nonzero(v)) ; @@ -128,11 +128,8 @@ function vceil(v) = // unit([0,0,0]); // Asserts an error. function unit(v, error=[[["ASSERT"]]]) = assert(is_vector(v), str("Expected a vector. Got: ",v)) - norm(v)=EPSILON,"Tried to normalize a zero vector") - : error - : v/norm(v); + norm(v)=EPSILON,"Tried to normalize a zero vector") : error) : + v/norm(v); // Function: vector_angle() diff --git a/vnf.scad b/vnf.scad index f94b352..707f1c4 100644 --- a/vnf.scad +++ b/vnf.scad @@ -766,7 +766,6 @@ function vnf_validate(vnf, show_warns=true, check_isects=false) = sface2 = list_rotate(face2,min2) ) if (sface1 == sface2) _vnf_validate_err("DUP_FACE", [for (i=sface1) varr[i]]) - ], issues = concat(issues, repeated_faces) ) issues? issues : @@ -896,7 +895,6 @@ function _vnf_validate_err(name, extra) = ) concat(info, [extra]); - function _pts_not_reported(pts, varr, reports) = [ for (i = pts, report = reports, pt = report[3]) @@ -942,12 +940,17 @@ module vnf_validate(vnf, size=1, show_warns=true, check_isects=false) { color([0.5,0.5,0.5,0.67]) vnf_polyhedron(vnf); } - - + // Section: VNF Transformations +// Function: vnf_halfspace() +// Usage: +// vnf_halfspace([a,b,c,d], vnf) +// Description: +// returns the intersection of the VNF with the given half-space. +// Arguments: +// halfspace = half-space to intersect with, given as the four coefficients of the affine inequation a\*x+b\*y+c\*z≥ d. -/// Internal functions function _vnf_halfspace_pts(halfspace, points, faces, inside=undef, coords=[], map=[]) = /* Recursive function to compute the intersection of points (and edges, @@ -988,8 +991,6 @@ function _vnf_halfspace_pts(halfspace, points, faces, (zi*points[j]-zj*pi)/(zi-zj)]), // map: we add the info concat(map, [[for(y=enumerate(adj2)) [y[1], n+y[0]]]])); - - function _vnf_halfspace_face(face, map, inside, i=0, newface=[], newedge=[], exit) = /* Recursive function to intersect a face of the VNF with the half-plane. @@ -1028,8 +1029,6 @@ function _vnf_halfspace_face(face, map, inside, i=0, concat(newface0, [inter]), concat(newedge, [inter]), is_undef(exit) ? inside[p] : exit); - - function _vnf_halfspace_path_search_edge(edge, paths, i=0, ret=[undef,undef]) = /* given an oriented edge [x,y] and a set of oriented paths, * returns the indices [i,j] of paths [before, after] given edge @@ -1039,8 +1038,6 @@ function _vnf_halfspace_path_search_edge(edge, paths, i=0, ret=[undef,undef]) = _vnf_halfspace_path_search_edge(edge, paths, i+1, [last(paths[i]) == edge[0] ? i : ret[0], paths[i][0] == edge[1] ? i : ret[1]]); - - function _vnf_halfspace_paths(edges, i=0, paths=[]) = /* given a set of oriented edges [x,y], returns all paths [x,y,z,..] that may be formed from these edges. @@ -1063,15 +1060,6 @@ function _vnf_halfspace_paths(edges, i=0, paths=[]) = s[0] != s[1] ? [concat(paths[s[0]], paths[s[1]])] : // edge closes a loop [concat(paths[s[0]], [e[1]])])); - - -// Function: vnf_halfspace() -// Usage: -// vnf_halfspace([a,b,c,d], vnf) -// Description: -// returns the intersection of the VNF with the given half-space. -// Arguments: -// halfspace = half-space to intersect with, given as the four coefficients of the affine inequation a\*x+b\*y+c\*z≥ d. function vnf_halfspace(_arg1=_undef, _arg2=_undef, halfspace=_undef, vnf=_undef) = // here is where we wish that OpenSCAD had array lvalues... From 10a37054705caffb237a7c4b4df4e2243dbd0545 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Sun, 28 Mar 2021 17:09:24 +0100 Subject: [PATCH 09/17] Revert "teste edicao em common" This reverts commit 685e727d8dfdf0cb5478a021ce06bf8b5dfea05d. --- common.scad | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/common.scad b/common.scad index dc2333a..ab24380 100644 --- a/common.scad +++ b/common.scad @@ -38,6 +38,7 @@ function typeof(x) = "invalid"; + // Function: is_type() // Usage: // bool = is_type(x, types); @@ -261,8 +262,6 @@ function is_bool_list(list, length) = // See Also: first_defined(), one_defined(), num_defined() // Description: // Returns the value given as `v` if it is not `undef`. Otherwise, returns the value of `dflt`. -// Returns the value given as `v` if it is not `undef`. -// Otherwise, returns the value of `dflt`. // Arguments: // v = Value to pass through if not `undef`. // dflt = Value to return if `v` *is* `undef`. @@ -280,9 +279,6 @@ function default(v,dflt=undef) = is_undef(v)? dflt : v; // Arguments: // v = The list whose items are being checked. // recursive = If true, sublists are checked recursively for defined values. The first sublist that has a defined item is returned. -// Examples: -// list = *** -// val = first_defined(list) function first_defined(v,recursive=false,_i=0) = _i) // Topics: Undef Handling // See Also: default(), first_defined(), num_defined(), any_defined(), all_defined() -// one_defined(vars, names, ) // Description: // Examines the input list `vals` and returns the entry which is not `undef`. // If more than one entry is not `undef` then an error is asserted, specifying @@ -595,13 +590,13 @@ function segs(r) = // Module: no_children() -// Usage: -// no_children($children); // Topics: Error Checking // See Also: no_function(), no_module() +// Usage: +// no_children($children); // Description: -// Assert that the calling module does not support children. Prints an error message to this effect -// and fails if children are present, as indicated by its argument. +// Assert that the calling module does not support children. Prints an error message to this effect and fails if children are present, +// as indicated by its argument. // Arguments: // $children = number of children the module has. // Example: @@ -643,7 +638,6 @@ module no_module() { } - // Section: Testing Helpers @@ -666,7 +660,7 @@ function _valstr(x) = // expected = The value that was expected. // info = Extra info to print out to make the error clearer. // Example: -// assert_approx(1/3, 0.333333333333333, str("number=",1,", demon=",3)); +// assert_approx(1/3, 0.333333333333333, str("numer=",1,", demon=",3)); module assert_approx(got, expected, info) { no_children($children); if (!approx(got, expected)) { @@ -765,8 +759,8 @@ module shape_compare(eps=1/1024) { // The syntax is: `[for (INIT; CONDITION; NEXT) RETVAL]` where: // - INIT is zero or more `let()` style assignments that are evaluated exactly one time, before the first loop. // - CONDITION is an expression evaluated at the start of each loop. If true, continues with the loop. -// - RETVAL is an expression that returns a list item at each loop beginning. -// - NEXT is one or more `let()` style assignments that is evaluated for each loop. +// - RETVAL is an expression that returns a list item for each loop. +// - NEXT is one or more `let()` style assignments that is evaluated at the end of each loop. // . // Since the INIT phase is only run once, and the CONDITION and RETVAL expressions cannot update // variables, that means that only the NEXT phase can be used for iterative calculations. @@ -811,7 +805,6 @@ function looping(state) = state < 2; // Function: loop_while() // Usage: -// state = loop_while(state, continue) // state = loop_while(state, continue); // Topics: Iteration // See Also: looping(), loop_done() @@ -830,7 +823,6 @@ function loop_while(state, continue) = // Function: loop_done() // Usage: -// loop_done(state) // bool = loop_done(state); // Topics: Iteration // See Also: looping(), loop_while() From d92bb933f0137590647984d87dc2bc9366784438 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Mon, 29 Mar 2021 11:33:37 +0100 Subject: [PATCH 10/17] Revert "Revert "teste edicao em common"" This reverts commit 10a37054705caffb237a7c4b4df4e2243dbd0545. --- common.scad | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/common.scad b/common.scad index ab24380..dc2333a 100644 --- a/common.scad +++ b/common.scad @@ -38,7 +38,6 @@ function typeof(x) = "invalid"; - // Function: is_type() // Usage: // bool = is_type(x, types); @@ -262,6 +261,8 @@ function is_bool_list(list, length) = // See Also: first_defined(), one_defined(), num_defined() // Description: // Returns the value given as `v` if it is not `undef`. Otherwise, returns the value of `dflt`. +// Returns the value given as `v` if it is not `undef`. +// Otherwise, returns the value of `dflt`. // Arguments: // v = Value to pass through if not `undef`. // dflt = Value to return if `v` *is* `undef`. @@ -279,6 +280,9 @@ function default(v,dflt=undef) = is_undef(v)? dflt : v; // Arguments: // v = The list whose items are being checked. // recursive = If true, sublists are checked recursively for defined values. The first sublist that has a defined item is returned. +// Examples: +// list = *** +// val = first_defined(list) function first_defined(v,recursive=false,_i=0) = _i) // Topics: Undef Handling // See Also: default(), first_defined(), num_defined(), any_defined(), all_defined() +// one_defined(vars, names, ) // Description: // Examines the input list `vals` and returns the entry which is not `undef`. // If more than one entry is not `undef` then an error is asserted, specifying @@ -590,13 +595,13 @@ function segs(r) = // Module: no_children() -// Topics: Error Checking -// See Also: no_function(), no_module() // Usage: // no_children($children); +// Topics: Error Checking +// See Also: no_function(), no_module() // Description: -// Assert that the calling module does not support children. Prints an error message to this effect and fails if children are present, -// as indicated by its argument. +// Assert that the calling module does not support children. Prints an error message to this effect +// and fails if children are present, as indicated by its argument. // Arguments: // $children = number of children the module has. // Example: @@ -638,6 +643,7 @@ module no_module() { } + // Section: Testing Helpers @@ -660,7 +666,7 @@ function _valstr(x) = // expected = The value that was expected. // info = Extra info to print out to make the error clearer. // Example: -// assert_approx(1/3, 0.333333333333333, str("numer=",1,", demon=",3)); +// assert_approx(1/3, 0.333333333333333, str("number=",1,", demon=",3)); module assert_approx(got, expected, info) { no_children($children); if (!approx(got, expected)) { @@ -759,8 +765,8 @@ module shape_compare(eps=1/1024) { // The syntax is: `[for (INIT; CONDITION; NEXT) RETVAL]` where: // - INIT is zero or more `let()` style assignments that are evaluated exactly one time, before the first loop. // - CONDITION is an expression evaluated at the start of each loop. If true, continues with the loop. -// - RETVAL is an expression that returns a list item for each loop. -// - NEXT is one or more `let()` style assignments that is evaluated at the end of each loop. +// - RETVAL is an expression that returns a list item at each loop beginning. +// - NEXT is one or more `let()` style assignments that is evaluated for each loop. // . // Since the INIT phase is only run once, and the CONDITION and RETVAL expressions cannot update // variables, that means that only the NEXT phase can be used for iterative calculations. @@ -805,6 +811,7 @@ function looping(state) = state < 2; // Function: loop_while() // Usage: +// state = loop_while(state, continue) // state = loop_while(state, continue); // Topics: Iteration // See Also: looping(), loop_done() @@ -823,6 +830,7 @@ function loop_while(state, continue) = // Function: loop_done() // Usage: +// loop_done(state) // bool = loop_done(state); // Topics: Iteration // See Also: looping(), loop_while() From d4c7fab7edf482d646eadb81a81c2e786bbc566e Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Mon, 29 Mar 2021 12:04:45 +0100 Subject: [PATCH 11/17] correction of centroid --- geometry.scad | 45 +++++++++++++++++++--------------------- tests/test_geometry.scad | 4 ++-- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/geometry.scad b/geometry.scad index b145c6f..c8bb2a3 100644 --- a/geometry.scad +++ b/geometry.scad @@ -959,7 +959,7 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = // plane_normal(plane); // Description: // Returns the unit length normal vector for the given plane. -// Argument: +// Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_normal(plane) = assert( _valid_plane(plane), "Invalid input plane." ) @@ -973,7 +973,7 @@ function plane_normal(plane) = // Returns coeficient D of the normalized plane equation `Ax+By+Cz=D`, or the scalar offset of the plane from the origin. // This value may be negative. // The absolute value of this coefficient is the distance of the plane from the origin. -// Argument: +// Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_offset(plane) = assert( _valid_plane(plane), "Invalid input plane." ) @@ -1046,7 +1046,7 @@ function projection_on_plane(plane, points) = // pt = plane_point_nearest_origin(plane); // Description: // Returns the point on the plane that is closest to the origin. -// Argument: +// Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_point_nearest_origin(plane) = let( plane = normalize_plane(plane) ) @@ -1072,6 +1072,7 @@ function distance_from_plane(plane, point) = let( plane = normalize_plane(plane) ) point3d(plane)* point - plane[3]; + // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. @@ -1594,7 +1595,6 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = ]; - // Function: circle_line_intersection() // Usage: // isect = circle_line_intersection(c,r,line,,); @@ -1627,7 +1627,6 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = let( offset = sqrt(r*r-d*d), uvec=unit(line[1]-line[0]) ) [closest-offset*uvec, closest+offset*uvec] - ) [for(p=isect) if ((!bounded[0] || (p-line[0])*(line[1]-line[0])>=0) @@ -1635,7 +1634,6 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = - // Section: Pointlists @@ -1913,24 +1911,23 @@ function align_polygon(reference, poly, angles, cp) = 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; + 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; diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 08af88a..f320390 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -904,11 +904,11 @@ module test_noncollinear_triple() { } *test_noncollinear_triple(); - + module test_centroid() { $fn = 24; assert_approx(centroid(circle(d=100)), [0,0]); - assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); + assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); poly = move([1,2.5,3.1],p=rot([12,49,24], p=path3d(circle(10,$fn=33)))); assert_approx(centroid(poly), [1,2.5,3.1]); From 575db80056e46c71cc94d29d6fc99976ff6e7ef0 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Tue, 30 Mar 2021 00:25:29 +0100 Subject: [PATCH 12/17] correction of centroid --- geometry.scad | 35 ++++++++++++++++++----------------- tests/test_geometry.scad | 2 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/geometry.scad b/geometry.scad index c8bb2a3..a2b10bb 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1911,23 +1911,24 @@ function align_polygon(reference, poly, angles, cp) = 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; + 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; diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index f320390..6133ead 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -908,7 +908,7 @@ module test_noncollinear_triple() { module test_centroid() { $fn = 24; assert_approx(centroid(circle(d=100)), [0,0]); - assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); + assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); poly = move([1,2.5,3.1],p=rot([12,49,24], p=path3d(circle(10,$fn=33)))); assert_approx(centroid(poly), [1,2.5,3.1]); From 407c36a8aeffc9e541997d0d6e882047cdb6b8df Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Mon, 5 Apr 2021 15:59:43 +0100 Subject: [PATCH 13/17] Revert "correction of centroid" This reverts commit 575db80056e46c71cc94d29d6fc99976ff6e7ef0. --- geometry.scad | 35 +++++++++++++++++------------------ tests/test_geometry.scad | 2 +- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/geometry.scad b/geometry.scad index a2b10bb..c8bb2a3 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1911,24 +1911,23 @@ function align_polygon(reference, poly, angles, cp) = 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; + 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; diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 6133ead..f320390 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -908,7 +908,7 @@ module test_noncollinear_triple() { module test_centroid() { $fn = 24; assert_approx(centroid(circle(d=100)), [0,0]); - assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); + assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); poly = move([1,2.5,3.1],p=rot([12,49,24], p=path3d(circle(10,$fn=33)))); assert_approx(centroid(poly), [1,2.5,3.1]); From 2c03b19246f1314953c25d6d80b78e05cdbd3bd0 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Mon, 5 Apr 2021 15:59:54 +0100 Subject: [PATCH 14/17] Revert "Merge remote-tracking branch 'upstream/master'" This reverts commit fd2c073bb226a09af5ef1b8a0b4d588c2849c7ab, reversing changes made to d4c7fab7edf482d646eadb81a81c2e786bbc566e. --- arrays.scad | 57 ++++++------------------------------------ beziers.scad | 4 +-- distributors.scad | 16 ++++++------ math.scad | 5 ++-- paths.scad | 10 ++++---- polyhedra.scad | 3 +-- regions.scad | 2 +- rounding.scad | 2 +- shapes2d.scad | 6 ++--- tests/test_arrays.scad | 51 +++++++++++-------------------------- turtle3d.scad | 10 ++++---- version.scad | 2 +- 12 files changed, 53 insertions(+), 115 deletions(-) diff --git a/arrays.scad b/arrays.scad index e4de121..3fd8bbc 100644 --- a/arrays.scad +++ b/arrays.scad @@ -142,59 +142,18 @@ function last(list) = list[len(list)-1]; -// Function: list_head() +// Function: delete_last() // Usage: -// list = list_head(list,); +// list = delete_last(list); // Topics: List Handling -// See Also: select(), slice(), list_tail(), last() +// See Also: select(), slice(), subindex(), last() // Description: -// Returns the head of the given list, from the first item up until the `to` index, inclusive. -// If the `to` index is negative, then the length of the list is added to it, such that -// `-1` is the last list item. `-2` is the second from last. `-3` is third from last, etc. -// If the list is shorter than the given index, then the full list is returned. -// Arguments: -// list = The list to get the head of. -// to = The last index to include. If negative, adds the list length to it. ie: -1 is the last list item. -// Examples: -// hlist = list_head(["foo", "bar", "baz"]); // Returns: ["foo", "bar"] -// hlist = list_head(["foo", "bar", "baz"], -3); // Returns: ["foo"] -// hlist = list_head(["foo", "bar", "baz"], 2); // Returns: ["foo","bar"] -// hlist = list_head(["foo", "bar", "baz"], -5); // Returns: [] -// hlist = list_head(["foo", "bar", "baz"], 5); // Returns: ["foo","bar","baz"] -function list_head(list, to=-2) = +// Returns a list with all but the last entry from the input list. If input is empty, returns empty list. +// Example: +// nlist = delete_last(["foo", "bar", "baz"]); // Returns: ["foo", "bar"] +function delete_last(list) = assert(is_list(list)) - assert(is_finite(to)) - to<0? [for (i=[0:1:len(list)+to]) list[i]] : - to); -// Topics: List Handling -// See Also: select(), slice(), list_tail(), last() -// Description: -// Returns the tail of the given list, from the `from` index up until the end of the list, inclusive. -// If the `from` index is negative, then the length of the list is added to it, such that -// `-1` is the last list item. `-2` is the second from last. `-3` is third from last, etc. -// If you want it to return the last three items of the list, use `from=-3`. -// Arguments: -// list = The list to get the tail of. -// from = The first index to include. If negative, adds the list length to it. ie: -1 is the last list item. -// Examples: -// tlist = list_tail(["foo", "bar", "baz"]); // Returns: ["bar", "baz"] -// tlist = list_tail(["foo", "bar", "baz"], -1); // Returns: ["baz"] -// tlist = list_tail(["foo", "bar", "baz"], 2); // Returns: ["baz"] -// tlist = list_tail(["foo", "bar", "baz"], -5); // Returns: ["foo","bar","baz"] -// tlist = list_tail(["foo", "bar", "baz"], 5); // Returns: [] -function list_tail(list, from=1) = - assert(is_list(list)) - assert(is_finite(from)) - from>=0? [for (i=[from:1:len(list)-1]) list[i]] : - let(from = from + len(list)) - from>=0? [for (i=[from:1:len(list)-1]) list[i]] : - list; + list==[] ? [] : slice(list,0,-2); // Function: force_list() diff --git a/beziers.scad b/beziers.scad index 79fe650..2a74ac3 100644 --- a/beziers.scad +++ b/beziers.scad @@ -574,7 +574,7 @@ function bezier_line_intersection(curve, line) = // p2 = [30, 30]; // trace_path([p0,p1,p2], showpts=true, size=0.5, color="green"); // fbez = fillet3pts(p0,p1,p2, 10); -// trace_bezier(select(fbez,1,-2), size=1); +// trace_bezier(slice(fbez, 1, -2), size=1); function fillet3pts(p0, p1, p2, r, d, maxerr=0.1, w=0.5, dw=0.25) = let( r = get_radius(r=r,d=d), v0 = unit(p0-p1), @@ -942,7 +942,7 @@ module bezier_polygon(bezier, splinesteps=16, N=3) { assert(is_int(splinesteps)); assert(len(bezier)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1.")); polypoints=bezier_path(bezier, splinesteps, N); - polygon(points=polypoints); + polygon(points=slice(polypoints, 0, -1)); } diff --git a/distributors.scad b/distributors.scad index fde784d..1710f03 100644 --- a/distributors.scad +++ b/distributors.scad @@ -294,9 +294,9 @@ module distribute(spacing=undef, sizes=undef, dir=RIGHT, l=undef) spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); gaps2 = [for (gap = gaps) gap+spc]; spos = dir * -sum(gaps2)/2; - spacings = cumsum([0, each gaps2]); for (i=[0:1:$children-1]) { - $pos = spos + spacings[i] * dir; + totspc = sum(concat([0], slice(gaps2, 0, i))); + $pos = spos + totspc * dir; $idx = i; translate($pos) children(i); } @@ -339,9 +339,9 @@ module xdistribute(spacing=10, sizes=undef, l=undef) spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); gaps2 = [for (gap = gaps) gap+spc]; spos = dir * -sum(gaps2)/2; - spacings = cumsum([0, each gaps2]); for (i=[0:1:$children-1]) { - $pos = spos + spacings[i] * dir; + totspc = sum(concat([0], slice(gaps2, 0, i))); + $pos = spos + totspc * dir; $idx = i; translate($pos) children(i); } @@ -384,9 +384,9 @@ module ydistribute(spacing=10, sizes=undef, l=undef) spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); gaps2 = [for (gap = gaps) gap+spc]; spos = dir * -sum(gaps2)/2; - spacings = cumsum([0, each gaps2]); for (i=[0:1:$children-1]) { - $pos = spos + spacings[i] * dir; + totspc = sum(concat([0], slice(gaps2, 0, i))); + $pos = spos + totspc * dir; $idx = i; translate($pos) children(i); } @@ -429,9 +429,9 @@ module zdistribute(spacing=10, sizes=undef, l=undef) spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); gaps2 = [for (gap = gaps) gap+spc]; spos = dir * -sum(gaps2)/2; - spacings = cumsum([0, each gaps2]); for (i=[0:1:$children-1]) { - $pos = spos + spacings[i] * dir; + totspc = sum(concat([0], slice(gaps2, 0, i))); + $pos = spos + totspc * dir; $idx = i; translate($pos) children(i); } diff --git a/math.scad b/math.scad index c63efb8..bf676e4 100644 --- a/math.scad +++ b/math.scad @@ -544,8 +544,9 @@ function _lcm(a,b) = // Computes lcm for a list of values function _lcmlist(a) = - len(a)==1 ? a[0] : - _lcmlist(concat(lcm(a[0],a[1]),list_tail(a,2))); + len(a)==1 + ? a[0] + : _lcmlist(concat(slice(a,0,len(a)-2),[lcm(a[len(a)-2],a[len(a)-1])])); // Function: lcm() diff --git a/paths.scad b/paths.scad index 28858fa..5d391c3 100644 --- a/paths.scad +++ b/paths.scad @@ -878,14 +878,14 @@ function assemble_a_path_from_fragments(fragments, rightmost=true, startfrag=0, let( // Found fragment intersects with initial path hitidx = select(hits,-1), - newpath = list_head(path,hitidx), + newpath = slice(path,0,hitidx+1), newfrags = concat(len(newpath)>1? [newpath] : [], remainder), outpath = concat(slice(path,hitidx,-2), foundfrag) ) [outpath, newfrags] ) : let( // Path still incomplete. Continue building it. - newpath = concat(path, list_tail(foundfrag)), + newpath = concat(path, slice(foundfrag, 1, -1)), newfrags = concat([newpath], remainder) ) assemble_a_path_from_fragments( @@ -1244,7 +1244,7 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals list_range(s=sp, step=spacing, e=length) ) : is_def(n) && is_undef(spacing)? ( closed? - let(range=list_range(s=0,e=length, n=n+1)) list_head(range) : + let(range=list_range(s=0,e=length, n=n+1)) slice(range,0,-2) : list_range(s=0, e=length, n=n) ) : ( let( @@ -1426,14 +1426,14 @@ function path_cut(path,cutdist,closed) = cuts = len(cutlist) ) [ - [ each list_head(path,cutlist[0][1]-1), + [ each slice(path,0,cutlist[0][1]), if (!approx(cutlist[0][0], path[cutlist[0][1]-1])) cutlist[0][0] ], for(i=[0:1:cuts-2]) cutlist[i][0]==cutlist[i+1][0] ? [] : [ if (!approx(cutlist[i][0], select(path,cutlist[i][1]))) cutlist[i][0], - each slice(path, cutlist[i][1], cutlist[i+1][1]), + each slice(path,cutlist[i][1], cutlist[i+1][1]), if (!approx(cutlist[i+1][0], select(path,cutlist[i+1][1]-1))) cutlist[i+1][0], ], [ diff --git a/polyhedra.scad b/polyhedra.scad index 57cc7eb..ac0d575 100644 --- a/polyhedra.scad +++ b/polyhedra.scad @@ -699,8 +699,7 @@ function regular_polyhedron_info( face_normals, radius_scale*entry[in_radius] ] : - // info == "vnf" ? [move(translation,p=scaled_points), stellate ? faces : face_triangles] : - info == "vnf" ? [move(translation,p=scaled_points), faces] : + info == "vnf" ? [move(translation,p=scaled_points), stellate ? faces : face_triangles] : info == "vertices" ? move(translation,p=scaled_points) : info == "faces" ? faces : info == "face normals" ? face_normals : diff --git a/regions.scad b/regions.scad index 8b5ca6b..b4befba 100644 --- a/regions.scad +++ b/regions.scad @@ -88,7 +88,7 @@ function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = is_list(valid_dim) ? str("one of ",valid_dim) : valid_dim ) ) - closed && approx(path[0], last(path))? list_head(path) : path; + closed && approx(path[0],select(path,-1))? slice(path,0,-2) : path; // Function: cleanup_region() diff --git a/rounding.scad b/rounding.scad index cf95e7b..afebedb 100644 --- a/rounding.scad +++ b/rounding.scad @@ -413,7 +413,7 @@ function _rounding_offsets(edgespec,z_dir=1) = assert(argsOK,str("Invalid specification with type ",edgetype)) let( offsets = - edgetype == "profile"? scale([-1,z_dir], p=list_tail(points)) : + edgetype == "profile"? scale([-1,z_dir], p=slice(points,1,-1)) : edgetype == "chamfer"? chamf_width==0 && chamf_height==0? [] : [[-chamf_width,z_dir*abs(chamf_height)]] : edgetype == "teardrop"? ( radius==0? [] : concat( diff --git a/shapes2d.scad b/shapes2d.scad index 34f92ae..16cd306 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -533,7 +533,7 @@ module dashed_stroke(path, dashpat=[3,3], width=1, closed=false) { function arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false, long=false, cw=false, ccw=false, endpoint=true) = assert(is_bool(endpoint)) !endpoint ? assert(!wedge, "endpoint cannot be false if wedge is true") - list_head(arc(N,r,angle,d,cp,points,width,thickness,start,wedge,long,cw,ccw,true)) : + slice(arc(N,r,angle,d,cp,points,width,thickness,start,wedge,long,cw,ccw,true),0,-2) : assert(is_undef(N) || is_integer(N), "Number of points must be an integer") // First try for 2D arc specified by width and thickness is_def(width) && is_def(thickness)? ( @@ -851,7 +851,7 @@ function _turtle_command(command, parm, parm2, state, index) = ) list_set( state, [path,step], [ - concat(state[path], list_tail(arcpath)), + concat(state[path], slice(arcpath,1,-1)), rot(lrsign * myangle,p=state[step],planar=true) ] ) : @@ -877,7 +877,7 @@ function _turtle_command(command, parm, parm2, state, index) = ) list_set( state, [path,step], [ - concat(state[path], list_tail(arcpath)), + concat(state[path], slice(arcpath,1,-1)), rot(delta_angle,p=state[step],planar=true) ] ) : diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index 5b460e2..c8f86d0 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -27,6 +27,21 @@ module test_select() { } test_select(); +module test_last() { + list = [1,2,3,4]; + assert(last(list)==4); + assert(last([])==undef); +} +test_last(); + +module test_delete_last() { + list = [1,2,3,4]; + assert(delete_last(list) == [1,2,3]); + assert(delete_last([1]) == []); + assert(delete_last([]) == []); +} +test_delete_last(); + module test_slice() { assert(slice([3,4,5,6,7,8,9], 3, 5) == [6,7]); @@ -39,42 +54,6 @@ module test_slice() { test_slice(); -module test_last() { - list = [1,2,3,4]; - assert(last(list)==4); - assert(last([])==undef); -} -test_last(); - - -module test_list_head() { - list = [1,2,3,4]; - assert_equal(list_head(list), [1,2,3]); - assert_equal(list_head([1]), []); - assert_equal(list_head([]), []); - assert_equal(list_head(list,-3), [1,2]); - assert_equal(list_head(list,1), [1,2]); - assert_equal(list_head(list,2), [1,2,3]); - assert_equal(list_head(list,6), [1,2,3,4]); - assert_equal(list_head(list,-6), []); -} -test_list_head(); - - -module test_list_tail() { - list = [1,2,3,4]; - assert_equal(list_tail(list), [2,3,4]); - assert_equal(list_tail([1]), []); - assert_equal(list_tail([]), []); - assert_equal(list_tail(list,-3), [2,3,4]); - assert_equal(list_tail(list,2), [3,4]); - assert_equal(list_tail(list,3), [4]); - assert_equal(list_tail(list,6), []); - assert_equal(list_tail(list,-6), [1,2,3,4]); -} -test_list_tail(); - - module test_in_list() { assert(in_list("bar", ["foo", "bar", "baz"])); assert(!in_list("bee", ["foo", "bar", "baz"])); diff --git a/turtle3d.scad b/turtle3d.scad index c0631d0..8f9176d 100644 --- a/turtle3d.scad +++ b/turtle3d.scad @@ -540,28 +540,28 @@ function _turtle3d_command(command, parm, parm2, state, index) = command=="addlength" ? list_set(state, movestep, state[movestep]+parm) : command=="arcsteps" ? assert(is_int(parm) && parm>0, str("\"",command,"\" requires a postive integer argument at index ",index)) list_set(state, arcsteps, parm) : - command=="roll" ? list_set(state, trlist, concat(list_head(state[trlist]), [lastT*xrot(parm)])): + command=="roll" ? list_set(state, trlist, concat(slice(state[trlist],0,-2), [lastT*xrot(parm)])): in_list(command,["right","left","up","down"]) ? - list_set(state, trlist, concat(list_head(state[trlist]), [lastT*_turtle3d_rotation(command,default(parm,state[angle]))])): + list_set(state, trlist, concat(slice(state[trlist],0,-2), [lastT*_turtle3d_rotation(command,default(parm,state[angle]))])): in_list(command,["xrot","yrot","zrot"]) ? let( Trot = _rotpart(lastT), // Extract rotational part of lastT shift = _transpart(lastT) // Translation part of lastT ) - list_set(state, trlist, concat(list_head(state[trlist]), + list_set(state, trlist, concat(slice(state[trlist],0,-2), [move(shift)*_turtle3d_rotation(command,default(parm,state[angle])) * Trot])): command=="rot" ? let( Trot = _rotpart(lastT), // Extract rotational part of lastT shift = _transpart(lastT) // Translation part of lastT ) - list_set(state, trlist, concat(list_head(state[trlist]),[move(shift) * parm * Trot])): + list_set(state, trlist, concat(slice(state[trlist],0,-2),[move(shift) * parm * Trot])): command=="setdir" ? let( Trot = _rotpart(lastT), shift = _transpart(lastT) ) - list_set(state, trlist, concat(list_head(state[trlist]), + list_set(state, trlist, concat(slice(state[trlist],0,-2), [move(shift)*rot(from=apply(Trot,RIGHT),to=parm) * Trot ])): in_list(command,["arcleft","arcright","arcup","arcdown"]) ? assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index)) diff --git a/version.scad b/version.scad index 14eed9e..f09143e 100644 --- a/version.scad +++ b/version.scad @@ -6,7 +6,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,597]; +BOSL_VERSION = [2,0,595]; // Section: BOSL Library Version Functions From 8c0e389ec4fd255c1483ee28bcbad9b642903179 Mon Sep 17 00:00:00 2001 From: RonaldoCMP Date: Mon, 5 Apr 2021 16:00:07 +0100 Subject: [PATCH 15/17] Revert "correction of centroid" This reverts commit d4c7fab7edf482d646eadb81a81c2e786bbc566e. --- geometry.scad | 45 +++++++++++++++++++++------------------- tests/test_geometry.scad | 4 ++-- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/geometry.scad b/geometry.scad index c8bb2a3..b145c6f 100644 --- a/geometry.scad +++ b/geometry.scad @@ -959,7 +959,7 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = // plane_normal(plane); // Description: // Returns the unit length normal vector for the given plane. -// Arguments: +// Argument: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_normal(plane) = assert( _valid_plane(plane), "Invalid input plane." ) @@ -973,7 +973,7 @@ function plane_normal(plane) = // Returns coeficient D of the normalized plane equation `Ax+By+Cz=D`, or the scalar offset of the plane from the origin. // This value may be negative. // The absolute value of this coefficient is the distance of the plane from the origin. -// Arguments: +// Argument: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_offset(plane) = assert( _valid_plane(plane), "Invalid input plane." ) @@ -1046,7 +1046,7 @@ function projection_on_plane(plane, points) = // pt = plane_point_nearest_origin(plane); // Description: // Returns the point on the plane that is closest to the origin. -// Arguments: +// Argument: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_point_nearest_origin(plane) = let( plane = normalize_plane(plane) ) @@ -1072,7 +1072,6 @@ function distance_from_plane(plane, point) = let( plane = normalize_plane(plane) ) point3d(plane)* point - plane[3]; - // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. // Returns undef if line is parallel to, but not on the given plane. @@ -1595,6 +1594,7 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) = ]; + // Function: circle_line_intersection() // Usage: // isect = circle_line_intersection(c,r,line,,); @@ -1627,6 +1627,7 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = let( offset = sqrt(r*r-d*d), uvec=unit(line[1]-line[0]) ) [closest-offset*uvec, closest+offset*uvec] + ) [for(p=isect) if ((!bounded[0] || (p-line[0])*(line[1]-line[0])>=0) @@ -1634,6 +1635,7 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) = + // Section: Pointlists @@ -1911,23 +1913,24 @@ function align_polygon(reference, poly, angles, cp) = 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; + 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; diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index f320390..08af88a 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -904,11 +904,11 @@ module test_noncollinear_triple() { } *test_noncollinear_triple(); - + module test_centroid() { $fn = 24; assert_approx(centroid(circle(d=100)), [0,0]); - assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); + assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); poly = move([1,2.5,3.1],p=rot([12,49,24], p=path3d(circle(10,$fn=33)))); assert_approx(centroid(poly), [1,2.5,3.1]); From 698268c8c7b35ceda3001ac4f2d77e7694043e9c Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Tue, 6 Apr 2021 19:18:57 -0700 Subject: [PATCH 16/17] Resolved conflicts with master. --- arrays.scad | 57 ++++++++++++++++++++++++++++++++++++------ beziers.scad | 2 +- common.scad | 21 ++++++---------- distributors.scad | 16 ++++++------ geometry.scad | 10 +++++--- math.scad | 5 ++-- paths.scad | 4 +-- regions.scad | 2 +- rounding.scad | 2 +- shapes2d.scad | 4 +-- tests/test_arrays.scad | 35 ++++++++++++++++---------- turtle3d.scad | 10 ++++---- 12 files changed, 107 insertions(+), 61 deletions(-) diff --git a/arrays.scad b/arrays.scad index 6f0b597..c143001 100644 --- a/arrays.scad +++ b/arrays.scad @@ -145,18 +145,59 @@ function last(list) = list[len(list)-1]; -// Function: delete_last() +// Function: list_head() // Usage: -// list = delete_last(list); +// list = list_head(list,); // Topics: List Handling -// See Also: select(), slice(), subindex(), last() +// See Also: select(), slice(), list_tail(), last() // Description: -// Returns a list with all but the last entry from the input list. If input is empty, returns empty list. -// Example: -// nlist = delete_last(["foo", "bar", "baz"]); // Returns: ["foo", "bar"] -function delete_last(list) = +// Returns the head of the given list, from the first item up until the `to` index, inclusive. +// If the `to` index is negative, then the length of the list is added to it, such that +// `-1` is the last list item. `-2` is the second from last. `-3` is third from last, etc. +// If the list is shorter than the given index, then the full list is returned. +// Arguments: +// list = The list to get the head of. +// to = The last index to include. If negative, adds the list length to it. ie: -1 is the last list item. +// Examples: +// hlist = list_head(["foo", "bar", "baz"]); // Returns: ["foo", "bar"] +// hlist = list_head(["foo", "bar", "baz"], -3); // Returns: ["foo"] +// hlist = list_head(["foo", "bar", "baz"], 2); // Returns: ["foo","bar"] +// hlist = list_head(["foo", "bar", "baz"], -5); // Returns: [] +// hlist = list_head(["foo", "bar", "baz"], 5); // Returns: ["foo","bar","baz"] +function list_head(list, to=-2) = assert(is_list(list)) - list==[] ? [] : slice(list,0,-2); + assert(is_finite(to)) + to<0? [for (i=[0:1:len(list)+to]) list[i]] : + to); +// Topics: List Handling +// See Also: select(), slice(), list_tail(), last() +// Description: +// Returns the tail of the given list, from the `from` index up until the end of the list, inclusive. +// If the `from` index is negative, then the length of the list is added to it, such that +// `-1` is the last list item. `-2` is the second from last. `-3` is third from last, etc. +// If you want it to return the last three items of the list, use `from=-3`. +// Arguments: +// list = The list to get the tail of. +// from = The first index to include. If negative, adds the list length to it. ie: -1 is the last list item. +// Examples: +// tlist = list_tail(["foo", "bar", "baz"]); // Returns: ["bar", "baz"] +// tlist = list_tail(["foo", "bar", "baz"], -1); // Returns: ["baz"] +// tlist = list_tail(["foo", "bar", "baz"], 2); // Returns: ["baz"] +// tlist = list_tail(["foo", "bar", "baz"], -5); // Returns: ["foo","bar","baz"] +// tlist = list_tail(["foo", "bar", "baz"], 5); // Returns: [] +function list_tail(list, from=1) = + assert(is_list(list)) + assert(is_finite(from)) + from>=0? [for (i=[from:1:len(list)-1]) list[i]] : + let(from = from + len(list)) + from>=0? [for (i=[from:1:len(list)-1]) list[i]] : + list; // Function: list() diff --git a/beziers.scad b/beziers.scad index bae18c4..d6c2eb7 100644 --- a/beziers.scad +++ b/beziers.scad @@ -945,7 +945,7 @@ module bezier_polygon(bezier, splinesteps=16, N=3) { assert(is_int(splinesteps)); assert(len(bezier)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1.")); polypoints=bezier_path(bezier, splinesteps, N); - polygon(points=slice(polypoints, 0, -1)); + polygon(points=polypoints); } diff --git a/common.scad b/common.scad index 86b7e71..e24c744 100644 --- a/common.scad +++ b/common.scad @@ -273,7 +273,6 @@ function is_bool_list(list, length) = // Topics: Undef Handling // See Also: first_defined(), one_defined(), num_defined() // Description: -// Returns the value given as `v` if it is not `undef`. Otherwise, returns the value of `dflt`. // Returns the value given as `v` if it is not `undef`. // Otherwise, returns the value of `dflt`. // Arguments: @@ -294,8 +293,7 @@ function default(v,dflt=undef) = is_undef(v)? dflt : v; // v = The list whose items are being checked. // recursive = If true, sublists are checked recursively for defined values. The first sublist that has a defined item is returned. // Examples: -// list = *** -// val = first_defined(list) +// val = first_defined([undef,7,undef,true]); // Returns: 1 function first_defined(v,recursive=false,_i=0) = _i) // Topics: Undef Handling // See Also: default(), first_defined(), num_defined(), any_defined(), all_defined() -// one_defined(vars, names, ) // Description: // Examines the input list `vals` and returns the entry which is not `undef`. // If more than one entry is not `undef` then an error is asserted, specifying @@ -608,15 +605,15 @@ function segs(r) = // Module: no_children() +// Topics: Error Checking // Usage: // no_children($children); -// Topics: Error Checking -// See Also: no_function(), no_module() // Description: -// Assert that the calling module does not support children. Prints an error message to this effect -// and fails if children are present, as indicated by its argument. +// Assert that the calling module does not support children. Prints an error message to this effect and fails if children are present, +// as indicated by its argument. // Arguments: // $children = number of children the module has. +// See Also: no_function(), no_module() // Example: // module foo() { // no_children($children); @@ -679,7 +676,7 @@ function _valstr(x) = // expected = The value that was expected. // info = Extra info to print out to make the error clearer. // Example: -// assert_approx(1/3, 0.333333333333333, str("number=",1,", demon=",3)); +// assert_approx(1/3, 0.333333333333333, str("numer=",1,", demon=",3)); module assert_approx(got, expected, info) { no_children($children); if (!approx(got, expected)) { @@ -778,8 +775,8 @@ module shape_compare(eps=1/1024) { // The syntax is: `[for (INIT; CONDITION; NEXT) RETVAL]` where: // - INIT is zero or more `let()` style assignments that are evaluated exactly one time, before the first loop. // - CONDITION is an expression evaluated at the start of each loop. If true, continues with the loop. -// - RETVAL is an expression that returns a list item at each loop beginning. -// - NEXT is one or more `let()` style assignments that is evaluated for each loop. +// - RETVAL is an expression that returns a list item for each loop. +// - NEXT is one or more `let()` style assignments that is evaluated at the end of each loop. // . // Since the INIT phase is only run once, and the CONDITION and RETVAL expressions cannot update // variables, that means that only the NEXT phase can be used for iterative calculations. @@ -824,7 +821,6 @@ function looping(state) = state < 2; // Function: loop_while() // Usage: -// state = loop_while(state, continue) // state = loop_while(state, continue); // Topics: Iteration // See Also: looping(), loop_done() @@ -843,7 +839,6 @@ function loop_while(state, continue) = // Function: loop_done() // Usage: -// loop_done(state) // bool = loop_done(state); // Topics: Iteration // See Also: looping(), loop_while() diff --git a/distributors.scad b/distributors.scad index 06b3712..588acb8 100644 --- a/distributors.scad +++ b/distributors.scad @@ -303,9 +303,9 @@ module distribute(spacing=undef, sizes=undef, dir=RIGHT, l=undef) spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); gaps2 = [for (gap = gaps) gap+spc]; spos = dir * -sum(gaps2)/2; + spacings = cumsum([0, each gaps2]); for (i=[0:1:$children-1]) { - totspc = sum(concat([0], slice(gaps2, 0, i))); - $pos = spos + totspc * dir; + $pos = spos + spacings[i] * dir; $idx = i; translate($pos) children(i); } @@ -348,9 +348,9 @@ module xdistribute(spacing=10, sizes=undef, l=undef) spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); gaps2 = [for (gap = gaps) gap+spc]; spos = dir * -sum(gaps2)/2; + spacings = cumsum([0, each gaps2]); for (i=[0:1:$children-1]) { - totspc = sum(concat([0], slice(gaps2, 0, i))); - $pos = spos + totspc * dir; + $pos = spos + spacings[i] * dir; $idx = i; translate($pos) children(i); } @@ -393,9 +393,9 @@ module ydistribute(spacing=10, sizes=undef, l=undef) spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); gaps2 = [for (gap = gaps) gap+spc]; spos = dir * -sum(gaps2)/2; + spacings = cumsum([0, each gaps2]); for (i=[0:1:$children-1]) { - totspc = sum(concat([0], slice(gaps2, 0, i))); - $pos = spos + totspc * dir; + $pos = spos + spacings[i] * dir; $idx = i; translate($pos) children(i); } @@ -438,9 +438,9 @@ module zdistribute(spacing=10, sizes=undef, l=undef) spc = !is_undef(l)? ((l - sum(gaps)) / ($children-1)) : default(spacing, 10); gaps2 = [for (gap = gaps) gap+spc]; spos = dir * -sum(gaps2)/2; + spacings = cumsum([0, each gaps2]); for (i=[0:1:$children-1]) { - totspc = sum(concat([0], slice(gaps2, 0, i))); - $pos = spos + totspc * dir; + $pos = spos + spacings[i] * dir; $idx = i; translate($pos) children(i); } diff --git a/geometry.scad b/geometry.scad index b145c6f..489be44 100644 --- a/geometry.scad +++ b/geometry.scad @@ -812,7 +812,9 @@ function adj_opp_to_ang(adj,opp) = // Returns the area of a triangle formed between three 2D or 3D vertices. // Result will be negative if the points are 2D and in clockwise order. // Arguments: -// a, b, c = The three vertices of the triangle. +// a = The first vertex of the triangle. +// b = The second vertex of the triangle. +// c = The third vertex of the triangle. // Examples: // triangle_area([0,0], [5,10], [10,0]); // Returns -50 // triangle_area([10,0], [5,10], [0,0]); // Returns 50 @@ -881,7 +883,7 @@ function plane3pt_indexed(points, i1, i2, i3) = // Description: // Returns a plane defined by a normal vector and a point. // Arguments: -// normal = Normal vector to the plane to find.. +// normal = Normal vector to the plane to find. // pt = Point 3D on the plane to find. // Example: // plane_from_normal([0,0,1], [2,2,2]); // Returns the xy plane passing through the point (2,2,2) @@ -1905,8 +1907,8 @@ function align_polygon(reference, poly, angles, cp) = // 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. +// 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) diff --git a/math.scad b/math.scad index ed3ea89..9e4f73a 100644 --- a/math.scad +++ b/math.scad @@ -543,9 +543,8 @@ function _lcm(a,b) = // Computes lcm for a list of values function _lcmlist(a) = - len(a)==1 - ? a[0] - : _lcmlist(concat(slice(a,0,len(a)-2),[lcm(a[len(a)-2],a[len(a)-1])])); + len(a)==1 ? a[0] : + _lcmlist(concat(lcm(a[0],a[1]),list_tail(a,2))); // Function: lcm() diff --git a/paths.scad b/paths.scad index c47bc2f..6db525a 100644 --- a/paths.scad +++ b/paths.scad @@ -885,7 +885,7 @@ function assemble_a_path_from_fragments(fragments, rightmost=true, startfrag=0, [outpath, newfrags] ) : let( // Path still incomplete. Continue building it. - newpath = concat(path, slice(foundfrag, 1, -1)), + newpath = concat(path, list_tail(foundfrag)), newfrags = concat([newpath], remainder) ) assemble_a_path_from_fragments( @@ -1428,7 +1428,7 @@ function path_cut(path,cutdist,closed) = cuts = len(cutlist) ) [ - [ each slice(path,0,cutlist[0][1]), + [ each list_head(path,cutlist[0][1]-1), if (!approx(cutlist[0][0], path[cutlist[0][1]-1])) cutlist[0][0] ], for(i=[0:1:cuts-2]) diff --git a/regions.scad b/regions.scad index 57552ef..b76785c 100644 --- a/regions.scad +++ b/regions.scad @@ -88,7 +88,7 @@ function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = is_list(valid_dim) ? str("one of ",valid_dim) : valid_dim ) ) - closed && approx(path[0],select(path,-1))? slice(path,0,-2) : path; + closed && approx(path[0], last(path))? list_head(path) : path; // Function: cleanup_region() diff --git a/rounding.scad b/rounding.scad index cc4125c..815e220 100644 --- a/rounding.scad +++ b/rounding.scad @@ -413,7 +413,7 @@ function _rounding_offsets(edgespec,z_dir=1) = assert(argsOK,str("Invalid specification with type ",edgetype)) let( offsets = - edgetype == "profile"? scale([-1,z_dir], p=slice(points,1,-1)) : + edgetype == "profile"? scale([-1,z_dir], p=list_tail(points)) : edgetype == "chamfer"? chamf_width==0 && chamf_height==0? [] : [[-chamf_width,z_dir*abs(chamf_height)]] : edgetype == "teardrop"? ( radius==0? [] : concat( diff --git a/shapes2d.scad b/shapes2d.scad index f9f53bb..e14467e 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -851,7 +851,7 @@ function _turtle_command(command, parm, parm2, state, index) = ) list_set( state, [path,step], [ - concat(state[path], slice(arcpath,1,-1)), + concat(state[path], list_tail(arcpath)), rot(lrsign * myangle,p=state[step],planar=true) ] ) : @@ -877,7 +877,7 @@ function _turtle_command(command, parm, parm2, state, index) = ) list_set( state, [path,step], [ - concat(state[path], slice(arcpath,1,-1)), + concat(state[path], list_tail(arcpath)), rot(delta_angle,p=state[step],planar=true) ] ) : diff --git a/tests/test_arrays.scad b/tests/test_arrays.scad index cf2c370..679f34c 100644 --- a/tests/test_arrays.scad +++ b/tests/test_arrays.scad @@ -48,24 +48,33 @@ module test_last() { } test_last(); -module test_delete_last() { + +module test_list_head() { list = [1,2,3,4]; - assert(delete_last(list) == [1,2,3]); - assert(delete_last([1]) == []); - assert(delete_last([]) == []); + assert_equal(list_head(list), [1,2,3]); + assert_equal(list_head([1]), []); + assert_equal(list_head([]), []); + assert_equal(list_head(list,-3), [1,2]); + assert_equal(list_head(list,1), [1,2]); + assert_equal(list_head(list,2), [1,2,3]); + assert_equal(list_head(list,6), [1,2,3,4]); + assert_equal(list_head(list,-6), []); } -test_delete_last(); +test_list_head(); -module test_slice() { - assert(slice([3,4,5,6,7,8,9], 3, 5) == [6,7]); - assert(slice([3,4,5,6,7,8,9], 2, -1) == [5,6,7,8,9]); - assert(slice([3,4,5,6,7,8,9], 1, 1) == []); - assert(slice([3,4,5,6,7,8,9], 6, -1) == [9]); - assert(slice([3,4,5,6,7,8,9], 2, -2) == [5,6,7,8]); - assert(slice([], 2, -2) == []); +module test_list_tail() { + list = [1,2,3,4]; + assert_equal(list_tail(list), [2,3,4]); + assert_equal(list_tail([1]), []); + assert_equal(list_tail([]), []); + assert_equal(list_tail(list,-3), [2,3,4]); + assert_equal(list_tail(list,2), [3,4]); + assert_equal(list_tail(list,3), [4]); + assert_equal(list_tail(list,6), []); + assert_equal(list_tail(list,-6), [1,2,3,4]); } -test_slice(); +test_list_tail(); module test_in_list() { diff --git a/turtle3d.scad b/turtle3d.scad index b9f3035..7f1e4c5 100644 --- a/turtle3d.scad +++ b/turtle3d.scad @@ -540,28 +540,28 @@ function _turtle3d_command(command, parm, parm2, state, index) = command=="addlength" ? list_set(state, movestep, state[movestep]+parm) : command=="arcsteps" ? assert(is_int(parm) && parm>0, str("\"",command,"\" requires a postive integer argument at index ",index)) list_set(state, arcsteps, parm) : - command=="roll" ? list_set(state, trlist, concat(slice(state[trlist],0,-2), [lastT*xrot(parm)])): + command=="roll" ? list_set(state, trlist, concat(list_head(state[trlist]), [lastT*xrot(parm)])): in_list(command,["right","left","up","down"]) ? - list_set(state, trlist, concat(slice(state[trlist],0,-2), [lastT*_turtle3d_rotation(command,default(parm,state[angle]))])): + list_set(state, trlist, concat(list_head(state[trlist]), [lastT*_turtle3d_rotation(command,default(parm,state[angle]))])): in_list(command,["xrot","yrot","zrot"]) ? let( Trot = _rotpart(lastT), // Extract rotational part of lastT shift = _transpart(lastT) // Translation part of lastT ) - list_set(state, trlist, concat(slice(state[trlist],0,-2), + list_set(state, trlist, concat(list_head(state[trlist]), [move(shift)*_turtle3d_rotation(command,default(parm,state[angle])) * Trot])): command=="rot" ? let( Trot = _rotpart(lastT), // Extract rotational part of lastT shift = _transpart(lastT) // Translation part of lastT ) - list_set(state, trlist, concat(slice(state[trlist],0,-2),[move(shift) * parm * Trot])): + list_set(state, trlist, concat(list_head(state[trlist]),[move(shift) * parm * Trot])): command=="setdir" ? let( Trot = _rotpart(lastT), shift = _transpart(lastT) ) - list_set(state, trlist, concat(slice(state[trlist],0,-2), + list_set(state, trlist, concat(list_head(state[trlist]), [move(shift)*rot(from=apply(Trot,RIGHT),to=parm) * Trot ])): in_list(command,["arcleft","arcright","arcup","arcdown"]) ? assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index)) From a8a34e77e0e189bd327d63a4facfc2699b7ebac0 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Tue, 6 Apr 2021 19:25:40 -0700 Subject: [PATCH 17/17] Fixed docs errors and regressions issue. --- geometry.scad | 6 +++--- tests/test_geometry.scad | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/geometry.scad b/geometry.scad index 489be44..73abe90 100644 --- a/geometry.scad +++ b/geometry.scad @@ -961,7 +961,7 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) = // plane_normal(plane); // Description: // Returns the unit length normal vector for the given plane. -// Argument: +// Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_normal(plane) = assert( _valid_plane(plane), "Invalid input plane." ) @@ -975,7 +975,7 @@ function plane_normal(plane) = // Returns coeficient D of the normalized plane equation `Ax+By+Cz=D`, or the scalar offset of the plane from the origin. // This value may be negative. // The absolute value of this coefficient is the distance of the plane from the origin. -// Argument: +// Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_offset(plane) = assert( _valid_plane(plane), "Invalid input plane." ) @@ -1048,7 +1048,7 @@ function projection_on_plane(plane, points) = // pt = plane_point_nearest_origin(plane); // Description: // Returns the point on the plane that is closest to the origin. -// Argument: +// Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. function plane_point_nearest_origin(plane) = let( plane = normalize_plane(plane) ) diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index 3f78e7d..0e70f31 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -908,7 +908,7 @@ module test_noncollinear_triple() { module test_centroid() { $fn = 24; assert_approx(centroid(circle(d=100)), [0,0]); - assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [-20,0]); + assert_approx(centroid(rect([40,60],rounding=10,anchor=LEFT)), [20,0]); assert_approx(centroid(rect([40,60],rounding=10,anchor=FWD)), [0,30]); poly = move([1,2.5,3.1],p=rot([12,49,24], p=path3d(circle(10,$fn=33)))); assert_approx(centroid(poly), [1,2.5,3.1]);