2019-04-16 22:34:54 +00:00
//////////////////////////////////////////////////////////////////////
2019-04-24 10:31:42 +00:00
// LibFile: hull.scad
2019-04-16 22:34:54 +00:00
// Functions to create 2D and 3D convex hulls.
// To use, add the following line to the beginning of your file:
// ```
2019-04-19 07:25:10 +00:00
// include <BOSL2/std.scad>
2019-04-24 10:31:42 +00:00
// include <BOSL2/hull.scad>
2019-04-16 22:34:54 +00:00
// ```
2019-04-24 10:31:42 +00:00
// Derived from Oskar Linde's Hull:
2019-04-16 22:34:54 +00:00
// - https://github.com/openscad/scad-utils
//////////////////////////////////////////////////////////////////////
2019-05-01 06:45:05 +00:00
// Section: Convex Hulls
2019-04-16 22:34:54 +00:00
2019-04-24 10:31:42 +00:00
2019-05-01 06:45:05 +00:00
// Function: hull()
2019-04-24 10:31:42 +00:00
// Usage:
2019-05-01 06:45:05 +00:00
// hull(points);
2019-04-24 10:31:42 +00:00
// Description:
2019-05-06 04:37:36 +00:00
// 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.
2019-05-01 06:45:05 +00:00
// 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 ) ;
2019-04-24 10:31:42 +00:00
2019-05-01 06:45:05 +00:00
// Module: hull_points()
2019-04-16 22:34:54 +00:00
// Usage:
2019-05-01 06:45:05 +00:00
// hull_points(points, [fast]);
2019-04-16 22:34:54 +00:00
// Description:
2019-05-06 04:37:36 +00:00
// 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.
2019-05-01 06:45:05 +00:00
// 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
2019-04-24 10:31:42 +00:00
// Example(2D):
// pts = [[-10,-10], [0,10], [10,10], [12,-10]];
2019-05-01 06:45:05 +00:00
// hull_points(pts);
// Example:
2019-05-01 07:09:04 +00:00
// pts = [for (phi = [30:60:150], theta = [0:60:359]) spherical_to_xyz(10, theta, phi)];
2019-05-01 06:45:05 +00:00
// 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 (
2019-05-27 05:34:46 +00:00
[ [ for ( i = [ 0 : 1 : extra + 2 ] ) i ] ] ,
2019-05-01 06:45:05 +00:00
[ for ( i = [ extra + 3 : 3 : len ( points ) - 3 ] ) [ i , i + 1 , i + 2 ] ]
) ;
hull ( ) polyhedron ( points = points , faces = faces ) ;
}
} else {
perim = hull ( points ) ;
2019-05-01 07:07:55 +00:00
if ( is_num ( perim [ 0 ] ) ) {
2019-05-01 06:45:05 +00:00
polygon ( points = points , paths = [ perim ] ) ;
} else {
polyhedron ( points = points , faces = perim ) ;
}
}
}
2019-04-24 10:31:42 +00:00
}
// Function: hull2d_path()
2019-04-16 22:34:54 +00:00
// Usage:
2019-04-24 10:31:42 +00:00
// hull2d_path(points)
2019-04-16 22:34:54 +00:00
// Description:
2019-05-06 04:37:36 +00:00
// 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`.
2019-04-24 10:31:42 +00:00
// Example(2D):
// pts = [[-10,-10], [0,10], [10,10], [12,-10]];
// path = hull2d_path(pts);
2020-03-25 01:31:22 +00:00
// move_copies(pts) color("red") sphere(1);
2019-04-24 10:31:42 +00:00
// polygon(points=pts, paths=[path]);
function hull2d_path ( points ) =
2019-04-16 22:34:54 +00:00
( len ( points ) < 3 ) ? [ ] : let (
a = 0 , b = 1 ,
2020-03-21 22:16:31 +00:00
c = first_noncollinear ( a , b , points )
2019-04-24 10:31:42 +00:00
) ( c = = len ( points ) ) ? _hull2d_collinear ( points ) : let (
2019-05-27 05:34:46 +00:00
remaining = [ for ( i = [ 2 : 1 : len ( points ) - 1 ] ) if ( i ! = c ) i ] ,
2019-07-10 20:37:23 +00:00
ccw = triangle_area ( points [ a ] , points [ b ] , points [ c ] ) > 0 ,
2019-04-16 22:34:54 +00:00
polygon = ccw ? [ a , b , c ] : [ a , c , b ]
2019-04-24 10:31:42 +00:00
) _hull2d_iterative ( points , polygon , remaining ) ;
2019-04-16 22:34:54 +00:00
// Adds the remaining points one by one to the convex hull
2019-04-24 10:31:42 +00:00
function _hull2d_iterative ( points , polygon , remaining , _i = 0 ) =
2019-04-16 22:34:54 +00:00
( _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
2019-04-24 10:31:42 +00:00
) ( len ( conflicts ) = = 0 ) ? _hull2d_iterative ( points , polygon , remaining , _i + 1 ) : let (
2019-04-16 22:34:54 +00:00
// 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 )
2019-04-24 10:31:42 +00:00
) _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 ] ;
2019-04-16 22:34:54 +00:00
function _find_conflicting_segments ( points , polygon , point ) = [
2019-05-27 05:34:46 +00:00
for ( i = [ 0 : 1 : len ( polygon ) - 1 ] ) let (
2019-04-16 22:34:54 +00:00
j = ( i + 1 ) % len ( polygon ) ,
p1 = points [ polygon [ i ] ] ,
p2 = points [ polygon [ j ] ] ,
2019-07-10 20:37:23 +00:00
area = triangle_area ( p1 , p2 , point )
2019-04-16 22:34:54 +00:00
) if ( area < 0 ) i
] ;
// remove the conflicting segments from the polygon
function _remove_conflicts_and_insert_point ( polygon , conflicts , point ) =
( conflicts [ 0 ] = = 0 ) ? let (
2019-05-27 05:34:46 +00:00
nonconflicting = [ for ( i = [ 0 : 1 : len ( polygon ) - 1 ] ) if ( ! in_list ( i , conflicts ) ) i ] ,
2019-04-16 22:34:54 +00:00
new_indices = concat ( nonconflicting , ( nonconflicting [ len ( nonconflicting ) - 1 ] + 1 ) % len ( polygon ) ) ,
polygon = concat ( [ for ( i = new_indices ) polygon [ i ] ] , point )
) polygon : let (
2019-05-27 05:34:46 +00:00
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 ] ] ,
2019-04-16 22:34:54 +00:00
polygon = concat ( before_conflicts , point , after_conflicts )
) polygon ;
2019-04-24 10:31:42 +00:00
// Function: hull3d_faces()
// Usage:
// hull3d_faces(points)
2019-04-16 22:34:54 +00:00
// Description:
2019-05-06 04:37:36 +00:00
// 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.
2019-04-24 10:31:42 +00:00
// Example(3D):
// pts = [[-20,-20,0], [20,-20,0], [0,20,5], [0,0,20]];
// faces = hull3d_faces(pts);
2020-03-25 01:31:22 +00:00
// move_copies(pts) color("red") sphere(1);
2019-04-24 10:31:42 +00:00
// %polyhedron(points=pts, faces=faces);
function hull3d_faces ( points ) =
2019-04-16 22:34:54 +00:00
( len ( points ) < 3 ) ? list_range ( len ( points ) ) : let (
2019-05-01 06:45:05 +00:00
// start with a single non-collinear triangle
a = 0 ,
b = 1 ,
2019-11-18 02:15:57 +00:00
c = first_noncollinear ( a , b , points )
2019-05-01 06:45:05 +00:00
) ( c = = len ( points ) ) ? _hull2d_collinear ( points ) : let (
2019-04-16 22:34:54 +00:00
plane = plane3pt_indexed ( points , a , b , c ) ,
d = _find_first_noncoplanar ( plane , points , 3 )
) ( d = = len ( points ) ) ? /* all coplanar*/ let (
2019-05-27 19:25:13 +00:00
pts2d = [ for ( p = points ) project_plane ( p , points [ a ] , points [ b ] , points [ c ] ) ] ,
2019-04-24 10:31:42 +00:00
hull2d = hull2d_path ( pts2d )
2019-04-16 22:34:54 +00:00
) hull2d : let (
2019-05-27 05:34:46 +00:00
remaining = [ for ( i = [ 3 : 1 : len ( points ) - 1 ] ) if ( i ! = d ) i ] ,
2019-04-16 22:34:54 +00:00
// 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 ] ) ]
2019-04-24 10:31:42 +00:00
) _hull3d_iterative ( points , triangles , planes , remaining ) ;
2019-04-16 22:34:54 +00:00
// Adds the remaining points one by one to the convex hull
2019-04-24 10:31:42 +00:00
function _hull3d_iterative ( points , triangles , planes , remaining , _i = 0 ) =
2019-04-16 22:34:54 +00:00
_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 ] ) ]
2019-04-24 10:31:42 +00:00
) _hull3d_iterative (
2019-04-16 22:34:54 +00:00
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 ) = [
2019-05-27 05:34:46 +00:00
for ( i = [ 0 : 1 : len ( planes ) - 1 ] )
2019-04-16 22:34:54 +00:00
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: noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap