Merge pull request #491 from revarbat/pr/483

PR483 resubmit
This commit is contained in:
Revar Desmera 2021-04-06 19:30:12 -07:00 committed by GitHub
commit 64b4176202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 92 deletions

View file

@ -577,7 +577,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),

View file

@ -38,7 +38,6 @@ function typeof(x) =
"invalid";
// Function: is_type()
// Usage:
// bool = is_type(x, types);
@ -274,7 +273,8 @@ 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:
// v = Value to pass through if not `undef`.
// dflt = Value to return if `v` *is* `undef`.
@ -292,6 +292,8 @@ 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:
// val = first_defined([undef,7,undef,true]); // Returns: 1
function first_defined(v,recursive=false,_i=0) =
_i<len(v) && (
is_undef(v[_i]) || (
@ -604,7 +606,6 @@ function segs(r) =
// Module: no_children()
// Topics: Error Checking
// See Also: no_function(), no_module()
// Usage:
// no_children($children);
// Description:
@ -612,6 +613,7 @@ function segs(r) =
// 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);
@ -651,6 +653,7 @@ module no_module() {
}
// Section: Testing Helpers

View file

@ -17,10 +17,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],
@ -74,12 +74,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)==[];
@ -151,10 +151,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];
@ -168,9 +169,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)
@ -188,9 +189,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)
@ -209,9 +210,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)
@ -229,10 +230,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)
)
@ -250,10 +251,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)
)
@ -461,7 +462,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;
@ -810,6 +811,10 @@ 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 = 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
@ -877,6 +882,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]) =
@ -896,7 +904,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);
@ -905,9 +913,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 :
@ -931,7 +938,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);
@ -940,7 +947,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),
@ -954,6 +961,8 @@ function plane_from_polygon(poly, fast=false, eps=EPSILON) =
// plane_normal(plane);
// Description:
// Returns the unit length normal vector for the given plane.
// 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." )
unit([plane.x, plane.y, plane.z]);
@ -966,6 +975,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.
// 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." )
plane[3]/norm([plane.x, plane.y, plane.z]);
@ -1037,6 +1048,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.
// 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) )
point3d(plane) * plane[3];
@ -1053,7 +1066,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,8 +1074,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.
@ -1119,7 +1130,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.")
@ -1148,7 +1159,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." )
@ -1203,6 +1214,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." )
@ -1229,10 +1244,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 :
@ -1249,7 +1264,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
@ -1268,7 +1283,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;
@ -1513,6 +1528,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;
@ -1623,11 +1645,15 @@ function circle_line_intersection(c,r,line,d,bounded=false,eps=EPSILON) =
// 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),
@ -1701,29 +1727,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);
@ -1734,6 +1760,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:
@ -1773,6 +1801,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." )
@ -1876,32 +1907,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, <eps>)
@ -1910,7 +1944,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/Evenodd_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.
@ -1920,17 +1954,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
@ -1954,12 +1988,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()
@ -1980,6 +2014,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);
@ -1990,6 +2026,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;
@ -2000,6 +2038,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]];
@ -2010,7 +2050,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(
@ -2020,7 +2062,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) =

View file

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

View file

@ -835,7 +835,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();
@ -909,8 +910,7 @@ module test_centroid() {
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=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();
@ -936,19 +936,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();