Implemented solution for issue #159

This commit is contained in:
Revar Desmera 2020-04-29 22:45:41 -07:00
parent 7ea3faee72
commit 182688cf02
6 changed files with 203 additions and 71 deletions

View File

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

View File

@ -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) {

View File

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

View File

@ -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) {

View File

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

View File

@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////////////
BOSL_VERSION = [2,0,284];
BOSL_VERSION = [2,0,285];
// Section: BOSL Library Version Functions