From 182688cf025cddd70457535ab582073b6228897a Mon Sep 17 00:00:00 2001 From: Revar Desmera Date: Wed, 29 Apr 2020 22:45:41 -0700 Subject: [PATCH] Implemented solution for issue #159 --- coords.scad | 58 ++++++++++----- distributors.scad | 8 +- geometry.scad | 183 +++++++++++++++++++++++++++++++++++----------- mutators.scad | 8 +- skin.scad | 15 ++-- version.scad | 2 +- 6 files changed, 203 insertions(+), 71 deletions(-) diff --git a/coords.scad b/coords.scad index 2b7035d..19f3406 100644 --- a/coords.scad +++ b/coords.scad @@ -143,15 +143,21 @@ function xy_to_polar(x,y=undef) = let( // Function: project_plane() -// Usage: -// xy = project_plane(point, a, b, c); -// xy = project_plane(point, [A,B,C]]; +// Usage: With 3 Points +// xyz = project_plane(point, a, b, c); +// Usage: With Pointlist +// xyz = project_plane(point, POINTLIST); +// Usage: With Plane Definition [A,B,C,D] Where Ax+By+Cz=D +// xyz = project_plane(point, PLANE); // Description: -// Given three points defining a plane, returns the projected planar [X,Y] coordinates of the -// closest point to a 3D `point`. The origin of the planar coordinate system [0,0] will be at point -// `a`, and the Y+ axis direction will be towards point `b`. This coordinate system can be useful -// in taking a set of nearly coplanar points, and converting them to a pure XY set of coordinates -// for manipulation, before convering them back to the original 3D plane. +// Converts the given 3D point from global coordinates to the 2D planar coordinates of the closest +// point on the plane. This coordinate system can be useful in taking a set of nearly coplanar +// points, and converting them to a pure XY set of coordinates for manipulation, before converting +// them back to the original 3D plane. +// Can be called one of three ways: +// - Given three points, `a`, `b`, and `c`, the planar coordinate system will have `[0,0]` at point `a`, and the Y+ axis will be towards point `b`. +// - Given a list of points, finds three reasonably spaced non-collinear points in the list and uses them as points `a`, `b`, and `c` as above. +// - Given a plane definition `[A,B,C,D]` where `Ax+By+Cz=D`, the closest point on that plane to the global origin at `[0,0,0]` will be the planar coordinate origin `[0,0]`. // Arguments: // point = The 3D point, or list of 3D points to project into the plane's 2D coordinate system. // a = A 3D point that the plane passes through. Used to define the plane. @@ -164,8 +170,14 @@ function xy_to_polar(x,y=undef) = let( // xy2 = project_plane(pt, [a,b,c]); function project_plane(point, a, b, c) = is_undef(b) && is_undef(c) && is_list(a)? let( - indices = find_noncollinear_points(a) - ) project_plane(point, a[indices[0]], a[indices[1]], a[indices[2]]) : + mat = is_vector(a,4)? plane_transform(a) : + assert(is_path(a) && len(a)>=3) + plane_transform(plane_from_points(a)), + pts = is_vector(point)? point2d(apply(mat,point)) : + is_path(point)? path2d(apply(mat,point)) : + is_region(point)? [for (x=point) path2d(apply(mat,x))] : + assert(false, "point must be a 3D point, path, or region.") + ) pts : assert(is_vector(a)) assert(is_vector(b)) assert(is_vector(c)) @@ -180,13 +192,18 @@ function project_plane(point, a, b, c) = // Function: lift_plane() -// Usage: +// Usage: With 3 Points // xyz = lift_plane(point, a, b, c); -// xyz = lift_plane(point, [A,B,C]); +// Usage: With Pointlist +// xyz = lift_plane(point, POINTLIST); +// Usage: With Plane Definition [A,B,C,D] Where Ax+By+Cz=D +// xyz = lift_plane(point, PLANE); // Description: -// Given three points defining a plane, converts a planar [X,Y] coordinate to the actual -// corresponding 3D point on the plane. The origin of the planar coordinate system [0,0] -// will be at point `a`, and the Y+ axis direction will be towards point `b`. +// Converts the given 2D point from planar coordinates to the global 3D coordinates of the point on the plane. +// Can be called one of three ways: +// - Given three points, `a`, `b`, and `c`, the planar coordinate system will have `[0,0]` at point `a`, and the Y+ axis will be towards point `b`. +// - Given a list of points, finds three non-collinear points in the list and uses them as points `a`, `b`, and `c` as above. +// - Given a plane definition `[A,B,C,D]` where `Ax+By+Cz=D`, the closest point on that plane to the global origin at `[0,0,0]` will be the planar coordinate origin `[0,0]`. // Arguments: // point = The 2D point, or list of 2D points in the plane's coordinate system to get the 3D position of. // a = A 3D point that the plane passes through. Used to define the plane. @@ -194,8 +211,15 @@ function project_plane(point, a, b, c) = // c = A 3D point that the plane passes through. Used to define the plane. function lift_plane(point, a, b, c) = is_undef(b) && is_undef(c) && is_list(a)? let( - indices = find_noncollinear_points(a) - ) lift_plane(point, a[indices[0]], a[indices[1]], a[indices[2]]) : + mat = is_vector(a,4)? plane_transform(a) : + assert(is_path(a) && len(a)>=3) + plane_transform(plane_from_points(a)), + imat = matrix_inverse(mat), + pts = is_vector(point)? apply(imat,point3d(point)) : + is_path(point)? apply(imat,path3d(point)) : + is_region(point)? [for (x=point) apply(imat,path3d(x))] : + assert(false, "point must be a 2D point, path, or region.") + ) pts : assert(is_vector(a)) assert(is_vector(b)) assert(is_vector(c)) diff --git a/distributors.scad b/distributors.scad index 5111df5..b008113 100644 --- a/distributors.scad +++ b/distributors.scad @@ -998,9 +998,13 @@ module ovoid_spread(r=undef, d=undef, n=100, cone_ang=90, scale=[1,1,1], perp=tr // Example: // mirror_copy(UP+BACK, cp=[0,-5,-5]) rot(from=UP, to=BACK+UP) cylinder(d1=10, d2=0, h=20); // color("blue",0.25) translate([0,-5,-5]) rot(from=UP, to=BACK+UP) cube([15,15,0.01], center=true); -module mirror_copy(v=[0,0,1], offset=0, cp=[0,0,0]) +module mirror_copy(v=[0,0,1], offset=0, cp) { - nv = v/norm(v); + cp = is_vector(v,4)? plane_normal(v) * v[3] : + is_vector(cp)? cp : + is_num(cp)? cp*unit(v) : + [0,0,0]; + nv = is_vector(v,4)? plane_normal(v) : unit(v); off = nv*offset; if (cp == [0,0,0]) { translate(off) { diff --git a/geometry.scad b/geometry.scad index a5fe783..e5c4a15 100644 --- a/geometry.scad +++ b/geometry.scad @@ -84,6 +84,23 @@ function collinear_indexed(points, a, b, c, eps=EPSILON) = ) collinear(p1, p2, p3, eps); +// Function: points_are_collinear() +// Usage: +// points_are_collinear(points); +// Description: +// Given a list of points, returns true if all points in the list are collinear. +// Arguments: +// points = The list of points to test. +// eps = How much variance is allowed in testing that each point is on the same line. Default: `EPSILON` (1e-9) +function points_are_collinear(points, eps=EPSILON) = + let( + a = furthest_point(points[0], points), + b = furthest_point(points[a], points), + pa = points[a], + pb = points[b] + ) all([for (pt = points) collinear(pa, pb, pt, eps=eps)]); + + // Function: distance_from_line() // Usage: // distance_from_line(line, pt); @@ -584,38 +601,14 @@ function plane3pt_indexed(points, i1, i2, i3) = ) plane3pt(p1,p2,p3); -// Function: plane_intersection() -// Usage: -// plane_intersection(plane1, plane2, [plane3]) -// Description: -// Compute the point which is the intersection of the three planes, or the line intersection of two planes. -// 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 of the input planes are parallel -// then returns undef. -function plane_intersection(plane1,plane2,plane3) = - is_def(plane3)? let( - matrix = [for(p=[plane1,plane2,plane3]) select(p,0,2)], - rhs = [for(p=[plane1,plane2,plane3]) p[3]] - ) linear_solve(matrix,rhs) : - let( - normal = cross(plane_normal(plane1), plane_normal(plane2)) - ) approx(norm(normal),0) ? undef : - let( - matrix = [for(p=[plane1,plane2]) select(p,0,2)], - rhs = [for(p=[plane1,plane2]) p[3]], - point = linear_solve(matrix,rhs) - ) is_undef(point)? undef : - [point, point+normal]; - - // Function: plane_from_normal() // Usage: -// plane_from_normal(normal, pt) +// plane_from_normal(normal, [pt]) // Description: // Returns a plane defined by a normal vector and a point. // 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) = +function plane_from_normal(normal, pt=[0,0,0]) = concat(normal, [normal*pt]); @@ -632,6 +625,12 @@ function plane_from_normal(normal, pt) = // 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) +// Example: +// xyzpath = rot(45, v=[-0.3,1,0], p=path3d(star(n=6,id=70,d=100), 70)); +// plane = plane_from_points(xyzpath); +// #stroke(xyzpath,closed=true); +// cp = centroid(xyzpath); +// move(cp) rot(from=UP,to=plane_normal(plane)) anchor_arrow(); function plane_from_points(points, fast=false, eps=EPSILON) = let( points = deduplicate(points), @@ -646,6 +645,39 @@ function plane_from_points(points, fast=false, eps=EPSILON) = ) all_coplanar? plane : undef; +// Function: plane_from_polygon() +// Usage: +// plane_from_polygon(points, [fast], [eps]); +// Description: +// Given a 3D planar polygon, returns the cartesian equation of a plane. +// Returns [A,B,C,D] where Ax+By+Cz=D is the equation of the plane. +// If not all the points in the polygon are coplanar, then `undef` is returned. +// If `fast` is true, then a polygon where not all points are coplanar will +// result in an invalid plane value, as all coplanar checks are skipped. +// Arguments: +// poly = The planar 3D polygon to find the plane of. +// fast = If true, don'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) +// Example: +// xyzpath = rot(45, v=[0,1,0], p=path3d(star(n=5,step=2,d=100), 70)); +// plane = plane_from_polygon(xyzpath); +// #stroke(xyzpath,closed=true); +// cp = centroid(xyzpath); +// move(cp) rot(from=UP,to=plane_normal(plane)) anchor_arrow(); +function plane_from_polygon(poly, fast=false, eps=EPSILON) = + let( + poly = deduplicate(poly), + n = polygon_normal(poly), + plane = [n.x, n.y, n.z, n*poly[0]] + ) fast? plane : let( + all_coplanar = [ + for (pt = poly) + if (!coplanar(plane,pt,eps=eps)) 1 + ] == [] + ) all_coplanar? plane : + undef; + + // Function: plane_normal() // Usage: // plane_normal(plane); @@ -654,6 +686,48 @@ function plane_from_points(points, fast=false, eps=EPSILON) = function plane_normal(plane) = unit([for (i=[0:2]) plane[i]]); +// Function: plane_offset() +// Usage: +// d = plane_offset(plane); +// Description: +// Returns D, or the scalar offset of the plane from the origin. This can be a negative value. +// The absolute value of this is the distance of the plane from the origin at its closest approach. +function plane_offset(plane) = plane[3]; + + +// Function: plane_transform() +// Usage: +// mat = plane_transform(plane); +// Description: +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, returns a 3D affine +// transformation matrix that will rotate and translate from points on that plane +// to points on the XY plane. You can generally then use `path2d()` to drop the +// Z coordinates, so you can work with the points in 2D. +// Arguments: +// plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. +// Example: +// xyzpath = move([10,20,30], p=yrot(25, p=path3d(circle(d=100)))); +// plane = plane_from_points(xyzpath); +// mat = plane_transform(plane); +// xypath = path2d(apply(mat, xyzpath)); +// #stroke(xyzpath,closed=true); +// stroke(xypath,closed=true); +function plane_transform(plane) = + let( + n = plane_normal(plane), + cp = n * plane[3] + ) rot(from=n, to=UP) * move(-cp); + + +// Function: plane_point_nearest_origin() +// Usage: +// pt = plane_point_nearest_origin(plane); +// Description: +// Returns the point on the plane that is closest to the origin. +function plane_point_nearest_origin(plane) = + plane_normal(plane) * plane[3]; + + // Function: distance_from_plane() // Usage: // distance_from_plane(plane, point) @@ -808,21 +882,28 @@ function polygon_line_intersection(poly, line, bounded=false, eps=EPSILON) = res[0]; -// Function: points_are_collinear() +// Function: plane_intersection() // Usage: -// points_are_collinear(points); +// plane_intersection(plane1, plane2, [plane3]) // Description: -// Given a list of points, returns true if all points in the list are collinear. -// Arguments: -// points = The list of points to test. -// eps = How much variance is allowed in testing that each point is on the same line. Default: `EPSILON` (1e-9) -function points_are_collinear(points, eps=EPSILON) = +// Compute the point which is the intersection of the three planes, or the line intersection of two planes. +// 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 of the input planes are parallel +// then returns undef. +function plane_intersection(plane1,plane2,plane3) = + is_def(plane3)? let( + matrix = [for(p=[plane1,plane2,plane3]) select(p,0,2)], + rhs = [for(p=[plane1,plane2,plane3]) p[3]] + ) linear_solve(matrix,rhs) : let( - a = furthest_point(points[0], points), - b = furthest_point(points[a], points), - pa = points[a], - pb = points[b] - ) all([for (pt = points) collinear(pa, pb, pt, eps=eps)]); + normal = cross(plane_normal(plane1), plane_normal(plane2)) + ) approx(norm(normal),0) ? undef : + let( + matrix = [for(p=[plane1,plane2]) select(p,0,2)], + rhs = [for(p=[plane1,plane2]) p[3]], + point = linear_solve(matrix,rhs) + ) is_undef(point)? undef : + [point, point+normal]; // Function: coplanar() @@ -1289,7 +1370,7 @@ function centroid(poly) = // Usage: // point_in_polygon(point, path, [eps]) // Description: -// This function tests whether the given point is inside, outside or on the boundary of +// This function tests whether the given 2D point is inside, outside or on the boundary of // the specified 2D polygon using the Winding Number method. // 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. @@ -1299,7 +1380,7 @@ function centroid(poly) = // But the polygon cannot have holes (it must be simply connected). // Rounding error may give mixed results for points on or near the boundary. // Arguments: -// point = The point to check position of. +// point = The 2D point to check position of. // path = The list of 2D path points forming the perimeter of the polygon. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function point_in_polygon(point, path, eps=EPSILON) = @@ -1334,7 +1415,7 @@ function polygon_is_clockwise(path) = // Usage: // clockwise_polygon(path); // Description: -// Given a polygon path, returns the clockwise winding version of that path. +// Given a 2D polygon path, returns the clockwise winding version of that path. function clockwise_polygon(path) = polygon_is_clockwise(path)? path : reverse_polygon(path); @@ -1343,7 +1424,7 @@ function clockwise_polygon(path) = // Usage: // ccw_polygon(path); // Description: -// Given a polygon path, returns the counter-clockwise winding version of that path. +// Given a 2D polygon path, returns the counter-clockwise winding version of that path. function ccw_polygon(path) = polygon_is_clockwise(path)? reverse_polygon(path) : path; @@ -1357,5 +1438,23 @@ function reverse_polygon(poly) = let(lp=len(poly)) [for (i=idx(poly)) poly[(lp-i)%lp]]; +// Function: polygon_normal() +// Usage: +// n = polygon_normal(poly); +// Description: +// Given a 3D planar polygon, returns a unit-length normal vector for the +// clockwise orientation of the polygon. +// Example: +function polygon_normal(poly) = + let( + poly = path3d(cleanup_path(poly)), + p0 = poly[0], + n = sum([ + for (i=[1:1:len(poly)-2]) + cross(poly[i+1]-p0, poly[i]-p0) + ]) + ) unit(n); + + // vim: noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/mutators.scad b/mutators.scad index 0868994..56d5d8a 100644 --- a/mutators.scad +++ b/mutators.scad @@ -31,9 +31,13 @@ // half_of(DOWN+LEFT, s=200) sphere(d=150); // Example(2D): // half_of([1,1], planar=true) circle(d=50); -module half_of(v=UP, cp=[0,0,0], s=1000, planar=false) +module half_of(v=UP, cp, s=1000, planar=false) { - cp = is_num(cp)? cp*unit(v) : cp; + cp = is_vector(v,4)? assert(cp==undef, "Don't use cp with plane definition.") plane_normal(v) * v[3] : + is_vector(cp)? cp : + is_num(cp)? cp*unit(v) : + [0,0,0]; + v = is_vector(v,4)? plane_normal(v) : v; if (cp != [0,0,0]) { translate(cp) half_of(v=v, s=s, planar=planar) translate(-cp) children(); } else if (planar) { diff --git a/skin.scad b/skin.scad index 84d0d96..269d840 100644 --- a/skin.scad +++ b/skin.scad @@ -474,13 +474,14 @@ function _skin_core(profiles, caps) = // Function: subdivide_and_slice() -// Usage: 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 largest profile. -// You can set `numpoints="lcm"` to sample to the least common multiple of -// all curves, which will avoid sampling artifacts but may produce a huge output. -// After subdivision, profiles are sliced. +// Usage: +// 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 +// largest profile. You can set `numpoints="lcm"` to sample to the least common multiple of all +// curves, which will avoid sampling artifacts but may produce a huge output. After subdivision, +// profiles are sliced. // Arguments: // profiles = profiles to operate on // slices = number of slices to insert between each pair of profiles. May be a vector diff --git a/version.scad b/version.scad index 1b10651..19cd8d5 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,284]; +BOSL_VERSION = [2,0,285]; // Section: BOSL Library Version Functions