mirror of
https://github.com/BelfrySCAD/BOSL2.git
synced 2025-01-01 09:49:45 +00:00
Merge pull request #219 from adrianVmariano/master
hull fix and regression tests
This commit is contained in:
commit
9ffd959250
5 changed files with 377 additions and 266 deletions
|
@ -129,6 +129,11 @@ function is_list_of(list,pattern) =
|
|||
is_list(list) &&
|
||||
[]==[for(entry=0*list) if (entry != pattern) entry];
|
||||
|
||||
function _list_pattern(list) =
|
||||
is_list(list) ? [for(entry=list) is_list(entry) ? _list_pattern(entry) : 0]
|
||||
: 0;
|
||||
|
||||
|
||||
|
||||
// Function: is_consistent()
|
||||
// Usage:
|
||||
|
|
|
@ -1341,39 +1341,25 @@ function circle_circle_tangents(c1,r1,c2,r2,d1,d2) =
|
|||
|
||||
// Section: Pointlists
|
||||
|
||||
// Function: first_noncollinear()
|
||||
// Usage:
|
||||
// first_noncollinear(i1, i2, points);
|
||||
// Description:
|
||||
// Returns index of the first point in `points` that is not collinear with the points indexed by `i1` and `i2`.
|
||||
// Arguments:
|
||||
// i1 = The first point.
|
||||
// i2 = The second point.
|
||||
// points = The list of points to find a non-collinear point from.
|
||||
function first_noncollinear(i1, i2, points) =
|
||||
[for (j = idx(points)) if (j!=i1 && j!=i2 && !collinear_indexed(points,i1,i2,j)) j][0];
|
||||
|
||||
|
||||
// Function: find_noncollinear_points()
|
||||
// Usage:
|
||||
// find_noncollinear_points(points);
|
||||
// Description:
|
||||
// Finds the indices of three good non-collinear points from the points list `points`.
|
||||
function find_noncollinear_points(points) =
|
||||
function find_noncollinear_points(points,error=true,eps=EPSILON) =
|
||||
let(
|
||||
a = 0,
|
||||
b = furthest_point(points[a], points),
|
||||
pa = points[a],
|
||||
pb = points[b],
|
||||
c = max_index([
|
||||
for (p=points)
|
||||
(approx(p,pa) || approx(p,pb))? 0 :
|
||||
sin(vector_angle(points[a]-p,points[b]-p)) *
|
||||
norm(p-points[a]) * norm(p-points[b])
|
||||
])
|
||||
pa = points[0],
|
||||
b = furthest_point(pa, points),
|
||||
n = unit(points[b]-pa),
|
||||
relpoints = [for(pt=points) pt-pa],
|
||||
proj = relpoints * n,
|
||||
distlist = [for(i=[0:len(points)-1]) norm(relpoints[i]-proj[i]*n)]
|
||||
)
|
||||
assert(c!=a && c!=b, "Cannot find three noncollinear points in pointlist.")
|
||||
[a, b, c];
|
||||
max(distlist)<eps
|
||||
? assert(!error, "Cannot find three noncollinear points in pointlist.")
|
||||
[]
|
||||
: [0,b,max_index(distlist)];
|
||||
|
||||
|
||||
// Function: pointlist_bounds()
|
||||
|
|
498
hull.scad
498
hull.scad
|
@ -1,241 +1,257 @@
|
|||
//////////////////////////////////////////////////////////////////////
|
||||
// LibFile: hull.scad
|
||||
// Functions to create 2D and 3D convex hulls.
|
||||
// To use, add the following line to the beginning of your file:
|
||||
// ```
|
||||
// include <BOSL2/std.scad>
|
||||
// include <BOSL2/hull.scad>
|
||||
// ```
|
||||
// Derived from Oskar Linde's Hull:
|
||||
// - https://github.com/openscad/scad-utils
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// Section: Convex Hulls
|
||||
|
||||
|
||||
// Function: hull()
|
||||
// Usage:
|
||||
// hull(points);
|
||||
// Description:
|
||||
// Takes a list of 2D or 3D points (but not both in the same list) and returns either the list of
|
||||
// indexes into `points` that forms the 2D convex hull perimeter path, or the list of faces that
|
||||
// form the 3d convex hull surface. Each face is a list of indexes into `points`. If the input
|
||||
// points are co-linear, the result will be the indexes of the two extrema points. If the input
|
||||
// points are co-planar, the results will be a simple list of vertex indices that will form a planar
|
||||
// perimeter. Otherwise a list of faces will be returned, where each face is a simple list of
|
||||
// vertex indices for the perimeter of the face.
|
||||
// Arguments:
|
||||
// points = The set of 2D or 3D points to find the hull of.
|
||||
function hull(points) = let(two_d = len(points[0]) == 2) two_d? hull2d_path(points) : hull3d_faces(points);
|
||||
|
||||
|
||||
// Module: hull_points()
|
||||
// Usage:
|
||||
// hull_points(points, [fast]);
|
||||
// Description:
|
||||
// If given a list of 2D points, creates a 2D convex hull polygon that encloses all those points.
|
||||
// If given a list of 3D points, creates a 3D polyhedron that encloses all the points. This should
|
||||
// handle about 4000 points in slow mode. If `fast` is set to true, this should be able to handle
|
||||
// far more.
|
||||
// Arguments:
|
||||
// points = The list of points to form a hull around.
|
||||
// fast = If true, uses a faster cheat that may handle more points, but also may emit warnings that can stop your script if you have "Halt on first warning" enabled. Default: false
|
||||
// Example(2D):
|
||||
// pts = [[-10,-10], [0,10], [10,10], [12,-10]];
|
||||
// hull_points(pts);
|
||||
// Example:
|
||||
// pts = [for (phi = [30:60:150], theta = [0:60:359]) spherical_to_xyz(10, theta, phi)];
|
||||
// hull_points(pts);
|
||||
module hull_points(points, fast=false) {
|
||||
assert(is_list(points));
|
||||
if (points) {
|
||||
assert(is_list(points[0]));
|
||||
if (fast) {
|
||||
if (len(points[0]) == 2) {
|
||||
hull() polygon(points=points);
|
||||
} else {
|
||||
extra = len(points)%3;
|
||||
faces = concat(
|
||||
[[for(i=[0:1:extra+2])i]],
|
||||
[for(i=[extra+3:3:len(points)-3])[i,i+1,i+2]]
|
||||
);
|
||||
hull() polyhedron(points=points, faces=faces);
|
||||
}
|
||||
} else {
|
||||
perim = hull(points);
|
||||
if (is_num(perim[0])) {
|
||||
polygon(points=points, paths=[perim]);
|
||||
} else {
|
||||
polyhedron(points=points, faces=perim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Function: hull2d_path()
|
||||
// Usage:
|
||||
// hull2d_path(points)
|
||||
// Description:
|
||||
// Takes a list of arbitrary 2D points, and finds the minimal convex hull polygon to enclose them.
|
||||
// Returns a path as a list of indices into `points`.
|
||||
// Example(2D):
|
||||
// pts = [[-10,-10], [0,10], [10,10], [12,-10]];
|
||||
// path = hull2d_path(pts);
|
||||
// move_copies(pts) color("red") sphere(1);
|
||||
// polygon(points=pts, paths=[path]);
|
||||
function hull2d_path(points) =
|
||||
(len(points) < 3)? [] : let(
|
||||
a=0, b=1,
|
||||
c = first_noncollinear(a, b, points)
|
||||
) (c == len(points))? _hull2d_collinear(points) : let(
|
||||
remaining = [ for (i = [2:1:len(points)-1]) if (i != c) i ],
|
||||
ccw = triangle_area(points[a], points[b], points[c]) > 0,
|
||||
polygon = ccw? [a,b,c] : [a,c,b]
|
||||
) _hull2d_iterative(points, polygon, remaining);
|
||||
|
||||
|
||||
// Adds the remaining points one by one to the convex hull
|
||||
function _hull2d_iterative(points, polygon, remaining, _i=0) =
|
||||
(_i >= len(remaining))? polygon : let (
|
||||
// pick a point
|
||||
i = remaining[_i],
|
||||
// find the segments that are in conflict with the point (point not inside)
|
||||
conflicts = _find_conflicting_segments(points, polygon, points[i])
|
||||
// no conflicts, skip point and move on
|
||||
) (len(conflicts) == 0)? _hull2d_iterative(points, polygon, remaining, _i+1) : let(
|
||||
// find the first conflicting segment and the first not conflicting
|
||||
// conflict will be sorted, if not wrapping around, do it the easy way
|
||||
polygon = _remove_conflicts_and_insert_point(polygon, conflicts, i)
|
||||
) _hull2d_iterative(points, polygon, remaining, _i+1);
|
||||
|
||||
|
||||
function _hull2d_collinear(points) =
|
||||
let(
|
||||
a = points[0],
|
||||
n = points[1] - a,
|
||||
points1d = [ for(p = points) (p-a)*n ],
|
||||
min_i = min_index(points1d),
|
||||
max_i = max_index(points1d)
|
||||
) [min_i, max_i];
|
||||
|
||||
|
||||
function _find_conflicting_segments(points, polygon, point) = [
|
||||
for (i = [0:1:len(polygon)-1]) let(
|
||||
j = (i+1) % len(polygon),
|
||||
p1 = points[polygon[i]],
|
||||
p2 = points[polygon[j]],
|
||||
area = triangle_area(p1, p2, point)
|
||||
) if (area < 0) i
|
||||
];
|
||||
|
||||
|
||||
// remove the conflicting segments from the polygon
|
||||
function _remove_conflicts_and_insert_point(polygon, conflicts, point) =
|
||||
(conflicts[0] == 0)? let(
|
||||
nonconflicting = [ for(i = [0:1:len(polygon)-1]) if (!in_list(i, conflicts)) i ],
|
||||
new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)),
|
||||
polygon = concat([ for (i = new_indices) polygon[i] ], point)
|
||||
) polygon : let(
|
||||
before_conflicts = [ for(i = [0:1:min(conflicts)]) polygon[i] ],
|
||||
after_conflicts = (max(conflicts) >= (len(polygon)-1))? [] : [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ],
|
||||
polygon = concat(before_conflicts, point, after_conflicts)
|
||||
) polygon;
|
||||
|
||||
|
||||
|
||||
// Function: hull3d_faces()
|
||||
// Usage:
|
||||
// hull3d_faces(points)
|
||||
// Description:
|
||||
// Takes a list of arbitrary 3D points, and finds the minimal convex hull polyhedron to enclose
|
||||
// them. Returns a list of faces, where each face is a list of indexes into the given `points`
|
||||
// list. If all points passed to it are coplanar, then the return is the list of indices of points
|
||||
// forming the minimal convex hull polygon.
|
||||
// Example(3D):
|
||||
// pts = [[-20,-20,0], [20,-20,0], [0,20,5], [0,0,20]];
|
||||
// faces = hull3d_faces(pts);
|
||||
// move_copies(pts) color("red") sphere(1);
|
||||
// %polyhedron(points=pts, faces=faces);
|
||||
function hull3d_faces(points) =
|
||||
(len(points) < 3)? list_range(len(points)) : let (
|
||||
// start with a single non-collinear triangle
|
||||
a = 0,
|
||||
b = 1,
|
||||
c = first_noncollinear(a, b, points)
|
||||
) (c == len(points))? _hull2d_collinear(points) : let(
|
||||
plane = plane3pt_indexed(points, a, b, c),
|
||||
d = _find_first_noncoplanar(plane, points, 3)
|
||||
) (d == len(points))? /* all coplanar*/ let (
|
||||
pts2d = [ for (p = points) project_plane(p, points[a], points[b], points[c]) ],
|
||||
hull2d = hull2d_path(pts2d)
|
||||
) hull2d : let(
|
||||
remaining = [for (i = [3:1:len(points)-1]) if (i != d) i],
|
||||
// Build an initial tetrahedron.
|
||||
// Swap b, c if d is in front of triangle t.
|
||||
ifop = in_front_of_plane(plane, points[d]),
|
||||
bc = ifop? [c,b] : [b,c],
|
||||
b = bc[0],
|
||||
c = bc[1],
|
||||
triangles = [
|
||||
[a,b,c],
|
||||
[d,b,a],
|
||||
[c,d,a],
|
||||
[b,d,c]
|
||||
],
|
||||
// calculate the plane equations
|
||||
planes = [ for (t = triangles) plane3pt_indexed(points, t[0], t[1], t[2]) ]
|
||||
) _hull3d_iterative(points, triangles, planes, remaining);
|
||||
|
||||
|
||||
// Adds the remaining points one by one to the convex hull
|
||||
function _hull3d_iterative(points, triangles, planes, remaining, _i=0) =
|
||||
_i >= len(remaining) ? triangles :
|
||||
let (
|
||||
// pick a point
|
||||
i = remaining[_i],
|
||||
// find the triangles that are in conflict with the point (point not inside)
|
||||
conflicts = _find_conflicts(points[i], planes),
|
||||
// for all triangles that are in conflict, collect their halfedges
|
||||
halfedges = [
|
||||
for(c = conflicts, i = [0:2]) let(
|
||||
j = (i+1)%3
|
||||
) [triangles[c][i], triangles[c][j]]
|
||||
],
|
||||
// find the outer perimeter of the set of conflicting triangles
|
||||
horizon = _remove_internal_edges(halfedges),
|
||||
// generate a new triangle for each horizon halfedge together with the picked point i
|
||||
new_triangles = [ for (h = horizon) concat(h,i) ],
|
||||
// calculate the corresponding plane equations
|
||||
new_planes = [ for (t = new_triangles) plane3pt_indexed(points, t[0], t[1], t[2]) ]
|
||||
) _hull3d_iterative(
|
||||
points,
|
||||
// remove the conflicting triangles and add the new ones
|
||||
concat(list_remove(triangles, conflicts), new_triangles),
|
||||
concat(list_remove(planes, conflicts), new_planes),
|
||||
remaining,
|
||||
_i+1
|
||||
);
|
||||
|
||||
|
||||
function _remove_internal_edges(halfedges) = [
|
||||
for (h = halfedges)
|
||||
if (!in_list(reverse(h), halfedges))
|
||||
h
|
||||
];
|
||||
|
||||
|
||||
function _find_conflicts(point, planes) = [
|
||||
for (i = [0:1:len(planes)-1])
|
||||
if (in_front_of_plane(planes[i], point))
|
||||
i
|
||||
];
|
||||
|
||||
|
||||
function _find_first_noncoplanar(plane, points, i) =
|
||||
(i >= len(points) || !coplanar(plane, points[i]))? i :
|
||||
_find_first_noncoplanar(plane, points, i+1);
|
||||
|
||||
|
||||
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// LibFile: hull.scad
|
||||
// Functions to create 2D and 3D convex hulls.
|
||||
// To use, add the following line to the beginning of your file:
|
||||
// ```
|
||||
// include <BOSL2/std.scad>
|
||||
// include <BOSL2/hull.scad>
|
||||
// ```
|
||||
// Derived from Oskar Linde's Hull:
|
||||
// - https://github.com/openscad/scad-utils
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// Section: Convex Hulls
|
||||
|
||||
|
||||
// Function: hull()
|
||||
// Usage:
|
||||
// hull(points);
|
||||
// Description:
|
||||
// Takes a list of 2D or 3D points (but not both in the same list) and returns either the list of
|
||||
// indexes into `points` that forms the 2D convex hull perimeter path, or the list of faces that
|
||||
// form the 3d convex hull surface. Each face is a list of indexes into `points`. If the input
|
||||
// points are co-linear, the result will be the indexes of the two extrema points. If the input
|
||||
// points are co-planar, the results will be a simple list of vertex indices that will form a planar
|
||||
// perimeter. Otherwise a list of faces will be returned, where each face is a simple list of
|
||||
// vertex indices for the perimeter of the face.
|
||||
// Arguments:
|
||||
// points = The set of 2D or 3D points to find the hull of.
|
||||
function hull(points) =
|
||||
assert(is_path(points),"Invalid input to hull")
|
||||
len(points[0]) == 2
|
||||
? hull2d_path(points)
|
||||
: hull3d_faces(points);
|
||||
|
||||
|
||||
// Module: hull_points()
|
||||
// Usage:
|
||||
// hull_points(points, [fast]);
|
||||
// Description:
|
||||
// If given a list of 2D points, creates a 2D convex hull polygon that encloses all those points.
|
||||
// If given a list of 3D points, creates a 3D polyhedron that encloses all the points. This should
|
||||
// handle about 4000 points in slow mode. If `fast` is set to true, this should be able to handle
|
||||
// far more.
|
||||
// Arguments:
|
||||
// points = The list of points to form a hull around.
|
||||
// fast = If true, uses a faster cheat that may handle more points, but also may emit warnings that can stop your script if you have "Halt on first warning" enabled. Default: false
|
||||
// Example(2D):
|
||||
// pts = [[-10,-10], [0,10], [10,10], [12,-10]];
|
||||
// hull_points(pts);
|
||||
// Example:
|
||||
// pts = [for (phi = [30:60:150], theta = [0:60:359]) spherical_to_xyz(10, theta, phi)];
|
||||
// hull_points(pts);
|
||||
module hull_points(points, fast=false) {
|
||||
if (points) {
|
||||
assert(is_list(points[0]));
|
||||
if (fast) {
|
||||
if (len(points[0]) == 2) {
|
||||
hull() polygon(points=points);
|
||||
} else {
|
||||
extra = len(points)%3;
|
||||
faces = concat(
|
||||
[[for(i=[0:1:extra+2])i]],
|
||||
[for(i=[extra+3:3:len(points)-3])[i,i+1,i+2]]
|
||||
);
|
||||
hull() polyhedron(points=points, faces=faces);
|
||||
}
|
||||
} else {
|
||||
perim = hull(points);
|
||||
if (is_num(perim[0])) {
|
||||
polygon(points=points, paths=[perim]);
|
||||
} else {
|
||||
polyhedron(points=points, faces=perim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Function: hull2d_path()
|
||||
// Usage:
|
||||
// hull2d_path(points)
|
||||
// Description:
|
||||
// Takes a list of arbitrary 2D points, and finds the convex hull polygon to enclose them.
|
||||
// Returns a path as a list of indices into `points`. May return extra points, that are on edges of the hull.
|
||||
// Example(2D):
|
||||
// pts = [[-10,-10], [0,10], [10,10], [12,-10]];
|
||||
// path = hull2d_path(pts);
|
||||
// move_copies(pts) color("red") sphere(1);
|
||||
// polygon(points=pts, paths=[path]);
|
||||
function hull2d_path(points) =
|
||||
assert(is_path(points,2),"Invalid input to hull2d_path")
|
||||
len(points) < 2 ? []
|
||||
: len(points) == 2 ? [0,1]
|
||||
: let(tri=find_noncollinear_points(points, error=false))
|
||||
tri == [] ? _hull_collinear(points)
|
||||
: let(
|
||||
remaining = [ for (i = [0:1:len(points)-1]) if (i != tri[0] && i!=tri[1] && i!=tri[2]) i ],
|
||||
ccw = triangle_area(points[tri[0]], points[tri[1]], points[tri[2]]) > 0,
|
||||
polygon = ccw ? [tri[0],tri[1],tri[2]] : [tri[0],tri[2],tri[1]]
|
||||
) _hull2d_iterative(points, polygon, remaining);
|
||||
|
||||
|
||||
|
||||
// Adds the remaining points one by one to the convex hull
|
||||
function _hull2d_iterative(points, polygon, remaining, _i=0) =
|
||||
(_i >= len(remaining))? polygon : let (
|
||||
// pick a point
|
||||
i = remaining[_i],
|
||||
// find the segments that are in conflict with the point (point not inside)
|
||||
conflicts = _find_conflicting_segments(points, polygon, points[i])
|
||||
// no conflicts, skip point and move on
|
||||
) (len(conflicts) == 0)? _hull2d_iterative(points, polygon, remaining, _i+1) : let(
|
||||
// find the first conflicting segment and the first not conflicting
|
||||
// conflict will be sorted, if not wrapping around, do it the easy way
|
||||
polygon = _remove_conflicts_and_insert_point(polygon, conflicts, i)
|
||||
) _hull2d_iterative(points, polygon, remaining, _i+1);
|
||||
|
||||
|
||||
function _hull_collinear(points) =
|
||||
let(
|
||||
a = points[0],
|
||||
n = points[1] - a,
|
||||
points1d = [ for(p = points) (p-a)*n ],
|
||||
min_i = min_index(points1d),
|
||||
max_i = max_index(points1d)
|
||||
) [min_i, max_i];
|
||||
|
||||
|
||||
function _find_conflicting_segments(points, polygon, point) = [
|
||||
for (i = [0:1:len(polygon)-1]) let(
|
||||
j = (i+1) % len(polygon),
|
||||
p1 = points[polygon[i]],
|
||||
p2 = points[polygon[j]],
|
||||
area = triangle_area(p1, p2, point)
|
||||
) if (area < 0) i
|
||||
];
|
||||
|
||||
|
||||
// remove the conflicting segments from the polygon
|
||||
function _remove_conflicts_and_insert_point(polygon, conflicts, point) =
|
||||
(conflicts[0] == 0)? let(
|
||||
nonconflicting = [ for(i = [0:1:len(polygon)-1]) if (!in_list(i, conflicts)) i ],
|
||||
new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)),
|
||||
polygon = concat([ for (i = new_indices) polygon[i] ], point)
|
||||
) polygon : let(
|
||||
before_conflicts = [ for(i = [0:1:min(conflicts)]) polygon[i] ],
|
||||
after_conflicts = (max(conflicts) >= (len(polygon)-1))? [] : [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ],
|
||||
polygon = concat(before_conflicts, point, after_conflicts)
|
||||
) polygon;
|
||||
|
||||
|
||||
|
||||
// Function: hull3d_faces()
|
||||
// Usage:
|
||||
// hull3d_faces(points)
|
||||
// Description:
|
||||
// Takes a list of arbitrary 3D points, and finds the convex hull polyhedron to enclose
|
||||
// them. Returns a list of triangular faces, where each face is a list of indexes into the given `points`
|
||||
// list. The output will be valid for use with the polyhedron command, but may include vertices that are in the interior of a face of the hull, so it is not
|
||||
// necessarily the minimal representation of the hull.
|
||||
// If all points passed to it are coplanar, then the return is the list of indices of points
|
||||
// forming the convex hull polygon.
|
||||
// Example(3D):
|
||||
// pts = [[-20,-20,0], [20,-20,0], [0,20,5], [0,0,20]];
|
||||
// faces = hull3d_faces(pts);
|
||||
// move_copies(pts) color("red") sphere(1);
|
||||
// %polyhedron(points=pts, faces=faces);
|
||||
function hull3d_faces(points) =
|
||||
assert(is_path(points,3),"Invalid input to hull3d_faces")
|
||||
len(points) < 3 ? list_range(len(points))
|
||||
: let ( // start with a single non-collinear triangle
|
||||
tri = find_noncollinear_points(points, error=false)
|
||||
)
|
||||
tri==[] ? _hull_collinear(points)
|
||||
: let(
|
||||
a = tri[0],
|
||||
b = tri[1],
|
||||
c = tri[2],
|
||||
plane = plane3pt_indexed(points, a, b, c),
|
||||
d = _find_first_noncoplanar(plane, points, 3)
|
||||
)
|
||||
d == len(points)
|
||||
? /* all coplanar*/
|
||||
let (
|
||||
pts2d = [ for (p = points) project_plane(p, points[a], points[b], points[c]) ],
|
||||
hull2d = hull2d_path(pts2d)
|
||||
) hull2d
|
||||
: let(
|
||||
remaining = [for (i = [0:1:len(points)-1]) if (i!=a && i!=b && i!=c && i!=d) i],
|
||||
// Build an initial tetrahedron.
|
||||
// Swap b, c if d is in front of triangle t.
|
||||
ifop = in_front_of_plane(plane, points[d]),
|
||||
bc = ifop? [c,b] : [b,c],
|
||||
b = bc[0],
|
||||
c = bc[1],
|
||||
triangles = [
|
||||
[a,b,c],
|
||||
[d,b,a],
|
||||
[c,d,a],
|
||||
[b,d,c]
|
||||
],
|
||||
// calculate the plane equations
|
||||
planes = [ for (t = triangles) plane3pt_indexed(points, t[0], t[1], t[2]) ]
|
||||
) _hull3d_iterative(points, triangles, planes, remaining);
|
||||
|
||||
|
||||
// Adds the remaining points one by one to the convex hull
|
||||
function _hull3d_iterative(points, triangles, planes, remaining, _i=0) =
|
||||
_i >= len(remaining) ? triangles :
|
||||
let (
|
||||
// pick a point
|
||||
i = remaining[_i],
|
||||
// find the triangles that are in conflict with the point (point not inside)
|
||||
conflicts = _find_conflicts(points[i], planes),
|
||||
// for all triangles that are in conflict, collect their halfedges
|
||||
halfedges = [
|
||||
for(c = conflicts, i = [0:2]) let(
|
||||
j = (i+1)%3
|
||||
) [triangles[c][i], triangles[c][j]]
|
||||
],
|
||||
// find the outer perimeter of the set of conflicting triangles
|
||||
horizon = _remove_internal_edges(halfedges),
|
||||
// generate a new triangle for each horizon halfedge together with the picked point i
|
||||
new_triangles = [ for (h = horizon) concat(h,i) ],
|
||||
// calculate the corresponding plane equations
|
||||
new_planes = [ for (t = new_triangles) plane3pt_indexed(points, t[0], t[1], t[2]) ]
|
||||
) _hull3d_iterative(
|
||||
points,
|
||||
// remove the conflicting triangles and add the new ones
|
||||
concat(list_remove(triangles, conflicts), new_triangles),
|
||||
concat(list_remove(planes, conflicts), new_planes),
|
||||
remaining,
|
||||
_i+1
|
||||
);
|
||||
|
||||
|
||||
function _remove_internal_edges(halfedges) = [
|
||||
for (h = halfedges)
|
||||
if (!in_list(reverse(h), halfedges))
|
||||
h
|
||||
];
|
||||
|
||||
|
||||
function _find_conflicts(point, planes) = [
|
||||
for (i = [0:1:len(planes)-1])
|
||||
if (in_front_of_plane(planes[i], point))
|
||||
i
|
||||
];
|
||||
|
||||
|
||||
function _find_first_noncoplanar(plane, points, i) =
|
||||
(i >= len(points) || !coplanar(plane, points[i]))? i :
|
||||
_find_first_noncoplanar(plane, points, i+1);
|
||||
|
||||
|
||||
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap
|
||||
|
|
|
@ -519,6 +519,7 @@ module test_polygon_shift_to_closest_point() {
|
|||
test_polygon_shift_to_closest_point();
|
||||
|
||||
|
||||
/*
|
||||
module test_first_noncollinear(){
|
||||
pts = [
|
||||
[1,1], [2,2], [3,3], [4,4], [4,5], [5,6]
|
||||
|
@ -555,11 +556,14 @@ module test_first_noncollinear(){
|
|||
assert(first_noncollinear(5,4,pts) == 0);
|
||||
}
|
||||
test_first_noncollinear();
|
||||
*/
|
||||
|
||||
|
||||
module test_find_noncollinear_points() {
|
||||
assert(find_noncollinear_points([[1,1],[2,2],[3,3],[4,4],[4,5],[5,6]]) == [0,5,3]);
|
||||
assert(find_noncollinear_points([[1,1],[2,2],[8,3],[4,4],[4,5],[5,6]]) == [0,2,5]);
|
||||
u = unit([5,3]);
|
||||
assert_equal(find_noncollinear_points([for(i = [2,3,4,5,7,12,15]) i * u], error=false),[]);
|
||||
}
|
||||
test_find_noncollinear_points();
|
||||
|
||||
|
|
100
tests/test_hull.scad
Normal file
100
tests/test_hull.scad
Normal file
|
@ -0,0 +1,100 @@
|
|||
include <../std.scad>
|
||||
include <../hull.scad>
|
||||
|
||||
module test_hull() {
|
||||
assert_equal(hull([[3,4],[5,5]]), [0,1]);
|
||||
assert_equal(hull([[3,4,1],[5,5,3]]), [0,1]);
|
||||
|
||||
test_collinear_2d = let(u = unit([5,3])) [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
|
||||
assert_equal(hull(test_collinear_2d), [7,1]);
|
||||
test_collinear_3d = let(u = unit([5,3,2])) [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
|
||||
assert_equal(hull(test_collinear_3d), [7,1]);
|
||||
|
||||
/* // produces some extra points along edges
|
||||
test_square_2d = [for(x=[1:5], y=[2:6]) [x,y]];
|
||||
echo(test_square_2d);
|
||||
move_copies(test_square_2d) circle(r=.1,$fn=16);
|
||||
color("red")move_copies(select(test_square_2d,hull(test_square_2d))) circle(r=.1,$fn=16);
|
||||
*/
|
||||
|
||||
/* // also produces extra points along edges
|
||||
test_square_2d = rot(22,p=[for(x=[1:5], y=[2:6]) [x,y]]);
|
||||
echo(test_square_2d);
|
||||
move_copies(test_square_2d) circle(r=.1,$fn=16);
|
||||
color("red")move_copies(select(test_square_2d,hull(test_square_2d))) circle(r=.1,$fn=16);
|
||||
*/
|
||||
|
||||
rand10_2d = [[1.55356, -1.98965], [4.23157, -0.947788], [-4.06193, -1.55463],
|
||||
[1.23889, -3.73133], [-1.02637, -4.0155], [4.26806, -4.61909],
|
||||
[3.59556, -3.1574], [-2.77776, -4.21857], [-3.66253,-4.34458], [1.82324, 0.102025]];
|
||||
assert_equal(sort(hull(rand10_2d)), [1,2,5,8,9]);
|
||||
|
||||
rand75_2d = [[-3.14743, -3.28139], [0.15343, -0.370249], [0.082565, 3.95939], [-2.56925, -3.16262], [-1.59463, 4.20893],
|
||||
[-4.90744, -1.21374], [-1.0819, -1.93703], [-3.72723, -3.0744], [-3.34339, 1.53535], [3.15803, -0.307388], [4.23289,
|
||||
4.46259], [1.73624, 1.38918], [3.72087, -1.55028], [1.2604, 2.30502], [-0.966431, 1.673], [-3.26866, -0.531443], [1.52605,
|
||||
0.991804], [-1.26305, 1.0737], [-4.31943, 4.11932], [0.488101, 0.0425981], [1.0233, -0.723037], [-4.73406, 2.14568],
|
||||
[-4.75915, 3.83262], [4.90999, -2.76668], [1.91971, -3.8604], [4.38594, -0.761767], [-0.352984, 1.55291], [2.02714,
|
||||
-0.340099], [1.76052, 2.09196], [-1.27485, -4.39477], [4.36364, 3.84964], [0.593612, -4.00028], [3.06833, -3.67117],
|
||||
[4.26834, -4.21213], [4.60226, -0.120432], [-2.45646, 2.60327], [-4.79461, 3.83724], [-3.29755, 0.760159], [0.218423,
|
||||
4.1687], [-0.115829, -2.06242], [-3.96188, 3.21568], [4.3018, -2.5299], [-4.41694, 4.75173], [-3.8393, 2.82212], [-1.14268,
|
||||
1.80751], [2.05805, 1.68593], [-3.0159, -2.91139], [-1.44828, -1.93564], [-0.265887, 0.519893], [-0.457361, -0.610096],
|
||||
[-0.426359, -2.37315], [-3.1018, 2.31141], [0.179141, -3.56242], [-0.491786, 0.813055], [-3.28502, -1.18933], [0.0914813,
|
||||
2.16122], [4.5777, 4.83972], [-1.07096, 2.74992], [-0.698689, 3.9032], [-1.21809, -1.54434], [3.14457, 4.92302], [-4.63176,
|
||||
2.81952], [4.84414, 4.63699], [2.4259, -0.747268], [-1.52088, -4.58305], [1.6961, -3.73678], [-0.483003, -3.67283],
|
||||
[-3.72746, -0.284265], [2.07629, 1.99902], [-3.12698, -0.96353], [4.02254, 3.41521], [-0.963391, -3.2143], [0.315255,
|
||||
0.593049], [1.57006, 1.80436], [4.60957, -2.86325]];
|
||||
assert_equal(sort(hull(rand75_2d)),[5,7,23,33,36,42,56,60,62,64]);
|
||||
|
||||
rand10_2d_rot = rot([22,44,12], p=path3d(rand10_2d));
|
||||
assert_equal(sort(hull(rand10_2d_rot)), [1,2,5,8,9]);
|
||||
|
||||
rand75_2d_rot = rot([122,-44,32], p=path3d(rand75_2d));
|
||||
assert_equal(sort(hull(rand75_2d_rot)), [5,7,23,33,36,42,56,60,62,64]);
|
||||
|
||||
testpoints_on_sphere = [ for(p =
|
||||
[
|
||||
[1,PHI,0], [-1,PHI,0], [1,-PHI,0], [-1,-PHI,0],
|
||||
[0,1,PHI], [0,-1,PHI], [0,1,-PHI], [0,-1,-PHI],
|
||||
[PHI,0,1], [-PHI,0,1], [PHI,0,-1], [-PHI,0,-1]
|
||||
])
|
||||
unit(p)
|
||||
];
|
||||
assert_equal(hull(testpoints_on_sphere), [[8, 4, 0], [0, 4, 1], [4, 8, 5], [8, 2, 5], [2, 3, 5], [0, 1, 6], [3, 2, 7], [1, 4, 9], [4, 5, 9],
|
||||
[5, 3, 9], [8, 0, 10], [2, 8, 10], [0, 6, 10], [6, 7, 10], [7, 2, 10], [6, 1, 11], [3, 7, 11], [7, 6, 11], [1, 9, 11], [9, 3, 11]]);
|
||||
|
||||
rand10_3d = [[14.0893, -15.2751, 21.0843], [-14.1564, 17.5751, 3.32094], [17.4966, 12.1717, 18.0607], [24.5489, 9.64591, 10.4738], [-12.0233, -24.4368, 13.1614],
|
||||
[6.24019, -18.4135, 24.9554], [11.9438, -15.9724, -22.6454], [11.6147, 7.56059, 7.5667], [-19.7491, 9.42769, 15.3419], [-10.3726, 16.3559, 3.38503]];
|
||||
assert_equal(hull(rand10_3d),[[3, 6, 0], [1, 3, 2], [3, 0, 2], [6, 1, 4], [0, 6, 5], [6, 4, 5], [2, 0, 5], [1, 2, 8], [2, 5, 8], [4, 1, 8], [5, 4, 8], [6, 3, 9], [3, 1, 9], [1, 6, 9]]);
|
||||
|
||||
rand25_3d = [[-20.5261, 14.5058, -11.6349], [16.4625, 20.1316, 12.9816], [-14.0268, 5.58802, 17.686], [-5.47944, 16.2501,
|
||||
5.3086], [20.2168, -11.8466, 12.4598], [14.4633, -15.1479, 4.82151], [12.7897, 5.25704, 19.6205], [11.2456,
|
||||
18.2794, -3.47074], [-1.87665, 22.9852, 1.99367], [-15.6052, -2.11009, 14.0096], [-10.7389, -14.569,
|
||||
5.6121], [24.5965, 17.9039, 20.8313], [-13.7054, 13.3362, 1.50374], [10.1111, -23.1494, 19.9305], [14.154,
|
||||
19.6682, -0.170182], [-22.6438, 22.7429, -0.776773], [-9.75056, 17.8896, -8.04152], [23.1746, 20.5475,
|
||||
22.6957], [-10.5356, -4.32407, -7.0911], [2.20779, -8.30749, 6.87185], [23.2643, 2.64462, -19.0087],
|
||||
[24.4055, 24.4504, 23.4777], [-3.84086, -6.98473, -10.2889], [0.178043, -16.07, 16.8081], [-8.86482,
|
||||
-12.8256, 14.7418], [11.1759, -11.5614, -11.643], [7.16751, 13.9344, -19.1675], [2.26602, -10.5374,
|
||||
0.125718], [-13.9053, 11.1143, -21.9289], [24.9018, -23.5307, -21.4684], [-13.6609, -19.6495, -8.91583],
|
||||
[-16.5393, -22.4105, -6.91617], [-4.11378, -3.14362, -5.6881], [7.50883, -17.5284, -0.0615319], [-7.41739,
|
||||
0.0721313, -7.47111], [22.6975, -7.99655, 14.0555], [-13.3644, 9.26993, 20.858], [-13.6889, 16.7462,
|
||||
-14.5836], [16.5137, 3.90703, -5.49396], [-6.75614, -11.1444, -24.5309], [22.9868, 10.0028, 12.2866],
|
||||
[-4.81079, -0.967785, -10.4726], [-0.949023, 23.1441, -2.08208], [16.1256, -8.2295, -24.0113], [6.45274,
|
||||
-7.21416, 23.1409], [22.8274, 1.07038, 19.1756], [-10.6256, -10.0112, -6.12274], [6.29254, -7.81875,
|
||||
-24.4037], [22.8538, 8.78163, -6.82567], [-1.96142, 19.1728, -1.726]];
|
||||
assert_equal(hull(rand25_3d),[[21, 29, 11], [29, 21, 20], [21, 14, 20], [20, 14, 26], [15, 0, 28], [13, 29, 31], [0, 15,
|
||||
31], [15, 9, 31], [9, 24, 31], [24, 13, 31], [28, 0, 31], [11, 29, 35], [29, 13, 35], [15,
|
||||
21, 36], [9, 15, 36], [24, 9, 36], [13, 24, 36], [15, 28, 37], [28, 26, 37], [28, 31, 39],
|
||||
[31, 29, 39], [14, 21, 42], [21, 15, 42], [26, 14, 42], [15, 37, 42], [37, 26, 42], [29, 20,
|
||||
43], [39, 29, 43], [20, 26, 43], [26, 28, 43], [21, 13, 44], [13, 36, 44], [36, 21, 44],
|
||||
[21, 11, 45], [11, 35, 45], [13, 21, 45], [35, 13, 45], [28, 39, 47], [39, 43, 47], [43, 28, 47]]);
|
||||
|
||||
/* // Inconsistently treats coplanar faces: sometimes face center vertex is included in output, sometimes not
|
||||
test_cube_3d = [for(x=[1:3], y=[1:3], z=[1:3]) [x,y,z]];
|
||||
assert_equal(hull(test_cube_3d), [[3, 2, 0], [2, 3, 4], [26, 2, 5], [2, 4, 5], [4, 3, 6], [5, 4, 6], [5, 6, 7], [6, 26, 7], [26, 5, 8],
|
||||
[5, 7, 8], [7, 26, 8], [0, 2, 9], [3, 0, 9], [6, 3, 9], [9, 2, 10], [2, 26, 11], [10, 2, 11], [6, 9, 12],
|
||||
[26, 6, 15], [6, 12, 15], [9, 10, 18], [10, 11, 18], [12, 9, 18], [15, 12, 18], [26, 18, 19], [18, 11, 19],
|
||||
[11, 26, 20], [26, 19, 20], [19, 11, 20], [15, 18, 21], [18, 26, 21], [26, 15, 24], [15, 21, 24], [21, 26, 24]]);
|
||||
echo(len=len(hull(test_cube_3d)));
|
||||
*/
|
||||
}
|
||||
test_hull();
|
Loading…
Reference in a new issue