Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
2d56448eaThis file is pinned to the selected release. Download the original to open it in your design tool.
// https://github.com/OskarLinde/scad-utils/blob/master/hull.scad
// NOTE: this code uses
// * experimental let() syntax
// * experimental list comprehension syntax
// * search() bugfix and feature addition
// * vector min()/max()
// Calculates the convex hull of a set of points.
// The result is expressed in point indices.
// If the points are collinear (or 2d), the result is a convex
// polygon [i1,i2,i3,...], otherwise a triangular
// polyhedron [[i1,i2,i3],[i2,i3,i4],...]
function hull(points) =
!(len(points) > 0) ? [] :
len(points[0]) == 2 ? convexhull2d(points) :
len(points[0]) == 3 ? convexhull3d(points) : [];
epsilon = 1e-9;
// 2d version
function convexhull2d(points) =
len(points) < 3 ? [] : let(
a=0, b=1,
c = find_first_noncollinear([a,b], points, 2)
) c == len(points) ? convexhull_collinear(points) : let(
remaining = [ for (i = [2:len(points)-1]) if (i != c) i ],
polygon = area_2d(points[a], points[b], points[c]) > 0 ? [a,b,c] : [b,a,c]
) convex_hull_iterative_2d(points, polygon, remaining);
// Adds the remaining points one by one to the convex hull
function convex_hull_iterative_2d(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 ? convex_hull_iterative_2d(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)
) convex_hull_iterative_2d(
points,
polygon,
remaining,
i_+1
);
function find_conflicting_segments(points, polygon, point) = [
for (i = [0:len(polygon)-1]) let(j = (i+1) % len(polygon))
if (area_2d(points[polygon[i]], points[polygon[j]], point) < 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:len(polygon)-1]) if (!contains(conflicts, i)) i ],
new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)),
polygon = concat([ for (i = new_indices) polygon[i] ], point)
) polygon : let(
prior_to_first_conflict = [ for(i = [0:1:min(conflicts)]) polygon[i] ],
after_last_conflict = [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ],
polygon = concat(prior_to_first_conflict, point, after_last_conflict)
) polygon;
// 3d version
function convexhull3d(points) =
len(points) < 3 ? [ for(i = [0:1:len(points)-1]) i ] : let (
// start with a single triangle
a=0, b=1, c=2,
plane = plane(points,a,b,c),
d = find_first_noncoplanar(plane, points, 3)
) d == len(points) ? /* all coplanar*/ let (
pts2d = [ for (p = points) plane_project(p, points[a], points[b], points[c]) ],
hull2d = convexhull2d(pts2d)
) hull2d : let(
remaining = [for (i = [3:len(points)-1]) if (i != d) i],
// Build an initial tetrahedron
// swap b,c if d is in front of triangle t
bc = in_front(plane, points[d]) ? [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) plane(points, t[0], t[1], t[2]) ]
) convex_hull_iterative(points, triangles, planes, remaining);
// A plane equation (normal, offset)
function plane(points, a, b, c) = let(
normal = unit(cross(points[c]-points[a], points[b]-points[a]))
) [
normal,
normal * points[a]
];
// Adds the remaining points one by one to the convex hull
function convex_hull_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)
for(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) plane(points, t[0], t[1], t[2]) ]
) convex_hull_iterative(
points,
// remove the conflicting triangles and add the new ones
concat(remove_elements(triangles, conflicts), new_triangles),
concat(remove_elements(planes, conflicts), new_planes),
remaining,
i_+1
);
function convexhull_collinear(points) = let(
n = points[1] - points[0],
a = points[0],
points1d = [ for(p = points) (p-a)*n ],
min_i = min_index(points1d),
max_i = max_index(points1d)
) [ min_i, max_i ];
function min_index(values,min_,min_i_,i_) =
i_ == undef ? min_index(values,values[0],0,1) :
i_ >= len(values) ? min_i_ :
values[i_] < min_ ? min_index(values,values[i_],i_,i_+1)
: min_index(values,min_,min_i_,i_+1);
function max_index(values,max_,max_i_,i_) =
i_ == undef ? max_index(values,values[0],0,1) :
i_ >= len(values) ? max_i_ :
values[i_] > max_ ? max_index(values,values[i_],i_,i_+1)
: max_index(values,max_,max_i_,i_+1);
function remove_elements(array, elements) = [
for (i = [0:len(array)-1])
if (!search(i, elements))
array[i]
];
function remove_internal_edges(halfedges) = [
for (h = halfedges)
if (!contains(halfedges, reverse(h)))
h
];
function plane_project(point, a, b, c) = let(
u = b-a,
v = c-a,
n = cross(u,v),
w = cross(n,u),
relpoint = point-a
) [relpoint * u, relpoint * w];
function plane_unproject(point, a, b, c) = let(
u = b-a,
v = c-a,
n = cross(u,v),
w = cross(n,u)
) a + point[0] * u + point[1] * w;
function reverse(arr) = [ for (i = [len(arr)-1:-1:0]) arr[i] ];
function contains(arr, element) = search([element],arr)[0] != [] ? true : false;
function find_conflicts(point, planes) = [
for (i = [0:len(planes)-1])
if (in_front(planes[i], point))
i
];
function find_first_noncollinear(line, points, i) =
i >= len(points) ? len(points) :
collinear(points[line[0]],
points[line[1]],
points[i]) ? find_first_noncollinear(line, points, i+1)
: i;
function find_first_noncoplanar(plane, points, i) =
i >= len(points) ? len(points) :
coplanar(plane, points[i]) ? find_first_noncoplanar(plane, points, i+1)
: i;
function distance(plane, point) = plane[0] * point - plane[1];
function in_front(plane, point) = distance(plane, point) > epsilon;
function coplanar(plane, point) = abs(distance(plane,point)) <= epsilon;
function unit(v) = v/norm(v);
function area_2d(a,b,c) = (
a[0] * (b[1] - c[1]) +
b[0] * (c[1] - a[1]) +
c[0] * (a[1] - b[1])) / 2;
function collinear(a,b,c) = abs(area_2d(a,b,c)) < epsilon;
function spherical(cartesian) = [
atan2(cartesian[1], cartesian[0]),
asin(cartesian[2])
];
function cartesian(spherical) = [
cos(spherical[1]) * cos(spherical[0]),
cos(spherical[1]) * sin(spherical[0]),
sin(spherical[1])
];
/// TESTCODE
phi = 1.618033988749895;
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)
];
testpoints_spherical = [ for(p = testpoints_on_sphere) spherical(p) ];
testpoints_circular = [ for(a = [0:15:360-epsilon]) [cos(a),sin(a)] ];
testpoints_coplanar = let(u = unit([1,3,7]), v = unit([-2,1,-2])) [ for(i = [1:10]) rands(-1,1,1)[0] * u + rands(-1,1,1)[0] * v ];
testpoints_collinear_2d = let(u = unit([5,3])) [ for(i = [1:20]) rands(-1,1,1)[0] * u ];
testpoints_collinear_3d = let(u = unit([5,3,-5])) [ for(i = [1:20]) rands(-1,1,1)[0] * u ];
testpoints2d = 20 * [for (i = [1:10]) concat(rands(-1,1,2))];
testpoints3d = 20 * [for (i = [1:50]) concat(rands(-1,1,3))];
// All points are on the sphere, no point should be red
translate([-50,0]) visualize_hull(20*testpoints_on_sphere);
// 2D points
translate([50,0]) visualize_hull(testpoints2d);
// All points on a circle, no point should be red
translate([0,50]) visualize_hull(20*testpoints_circular);
// All points 3d but collinear
translate([0,-50]) visualize_hull(20*testpoints_coplanar);
// Collinear
translate([50,50]) visualize_hull(20*testpoints_collinear_2d);
// Collinear
translate([-50,50]) visualize_hull(20*testpoints_collinear_3d);
// 3D points
visualize_hull(testpoints3d);
module visualize_hull(points) {
hull = hull(points);
%if (len(hull) > 0 && len(hull[0]) > 0)
polyhedron(points=points, faces = hull);
else
polyhedron(points=points, faces = [hull]);
for (i = [0:len(points)-1]) assign(p = points[i], $fn = 16) {
translate(p) {
if (hull_contains_index(hull,i)) {
color("blue") sphere(1);
} else {
color("red") sphere(1);
}
}
}
function hull_contains_index(hull, index) =
search(index,hull,1,0) ||
search(index,hull,1,1) ||
search(index,hull,1,2);
}