hull.scad 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. // NOTE: this code uses
  2. // * experimental let() syntax
  3. // * experimental list comprehension syntax
  4. // * search() bugfix and feature addition
  5. // * vector min()/max()
  6. // Calculates the convex hull of a set of points.
  7. // The result is expressed in point indices.
  8. // If the points are collinear (or 2d), the result is a convex
  9. // polygon [i1,i2,i3,...], otherwise a triangular
  10. // polyhedron [[i1,i2,i3],[i2,i3,i4],...]
  11. function hull(points) =
  12. !(len(points) > 0) ? [] :
  13. len(points[0]) == 2 ? convexhull2d(points) :
  14. len(points[0]) == 3 ? convexhull3d(points) : [];
  15. epsilon = 1e-9;
  16. // 2d version
  17. function convexhull2d(points) =
  18. len(points) < 3 ? [] : let(
  19. a=0, b=1,
  20. c = find_first_noncollinear([a,b], points, 2)
  21. ) c == len(points) ? convexhull_collinear(points) : let(
  22. remaining = [ for (i = [2:len(points)-1]) if (i != c) i ],
  23. polygon = area_2d(points[a], points[b], points[c]) > 0 ? [a,b,c] : [b,a,c]
  24. ) convex_hull_iterative_2d(points, polygon, remaining);
  25. // Adds the remaining points one by one to the convex hull
  26. function convex_hull_iterative_2d(points, polygon, remaining, i_=0) = i_ >= len(remaining) ? polygon :
  27. let (
  28. // pick a point
  29. i = remaining[i_],
  30. // find the segments that are in conflict with the point (point not inside)
  31. conflicts = find_conflicting_segments(points, polygon, points[i])
  32. // no conflicts, skip point and move on
  33. ) len(conflicts) == 0 ? convex_hull_iterative_2d(points, polygon, remaining, i_+1) : let(
  34. // find the first conflicting segment and the first not conflicting
  35. // conflict will be sorted, if not wrapping around, do it the easy way
  36. polygon = remove_conflicts_and_insert_point(polygon, conflicts, i)
  37. ) convex_hull_iterative_2d(
  38. points,
  39. polygon,
  40. remaining,
  41. i_+1
  42. );
  43. function find_conflicting_segments(points, polygon, point) = [
  44. for (i = [0:len(polygon)-1]) let(j = (i+1) % len(polygon))
  45. if (area_2d(points[polygon[i]], points[polygon[j]], point) < 0)
  46. i
  47. ];
  48. // remove the conflicting segments from the polygon
  49. function remove_conflicts_and_insert_point(polygon, conflicts, point) =
  50. conflicts[0] == 0 ? let(
  51. nonconflicting = [ for(i = [0:len(polygon)-1]) if (!contains(conflicts, i)) i ],
  52. new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)),
  53. polygon = concat([ for (i = new_indices) polygon[i] ], point)
  54. ) polygon : let(
  55. prior_to_first_conflict = [ for(i = [0:1:min(conflicts)]) polygon[i] ],
  56. after_last_conflict = [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ],
  57. polygon = concat(prior_to_first_conflict, point, after_last_conflict)
  58. ) polygon;
  59. // 3d version
  60. function convexhull3d(points) =
  61. len(points) < 3 ? [ for(i = [0:1:len(points)-1]) i ] : let (
  62. // start with a single triangle
  63. a=0, b=1, c=2,
  64. plane = plane(points,a,b,c),
  65. d = find_first_noncoplanar(plane, points, 3)
  66. ) d == len(points) ? /* all coplanar*/ let (
  67. pts2d = [ for (p = points) plane_project(p, points[a], points[b], points[c]) ],
  68. hull2d = convexhull2d(pts2d)
  69. ) hull2d : let(
  70. remaining = [for (i = [3:len(points)-1]) if (i != d) i],
  71. // Build an initial tetrahedron
  72. // swap b,c if d is in front of triangle t
  73. bc = in_front(plane, points[d]) ? [c,b] : [b,c],
  74. b = bc[0], c = bc[1],
  75. triangles = [
  76. [a,b,c],
  77. [d,b,a],
  78. [c,d,a],
  79. [b,d,c],
  80. ],
  81. // calculate the plane equations
  82. planes = [ for (t = triangles) plane(points, t[0], t[1], t[2]) ]
  83. ) convex_hull_iterative(points, triangles, planes, remaining);
  84. // A plane equation (normal, offset)
  85. function plane(points, a, b, c) = let(
  86. normal = unit(cross(points[c]-points[a], points[b]-points[a]))
  87. ) [
  88. normal,
  89. normal * points[a]
  90. ];
  91. // Adds the remaining points one by one to the convex hull
  92. function convex_hull_iterative(points, triangles, planes, remaining, i_=0) = i_ >= len(remaining) ? triangles :
  93. let (
  94. // pick a point
  95. i = remaining[i_],
  96. // find the triangles that are in conflict with the point (point not inside)
  97. conflicts = find_conflicts(points[i], planes),
  98. // for all triangles that are in conflict, collect their halfedges
  99. halfedges = [
  100. for(c = conflicts)
  101. for(i = [0:2]) let(j = (i+1)%3)
  102. [triangles[c][i], triangles[c][j]]
  103. ],
  104. // find the outer perimeter of the set of conflicting triangles
  105. horizon = remove_internal_edges(halfedges),
  106. // generate a new triangle for each horizon halfedge together with the picked point i
  107. new_triangles = [ for (h = horizon) concat(h,i) ],
  108. // calculate the corresponding plane equations
  109. new_planes = [ for (t = new_triangles) plane(points, t[0], t[1], t[2]) ]
  110. ) convex_hull_iterative(
  111. points,
  112. // remove the conflicting triangles and add the new ones
  113. concat(remove_elements(triangles, conflicts), new_triangles),
  114. concat(remove_elements(planes, conflicts), new_planes),
  115. remaining,
  116. i_+1
  117. );
  118. function convexhull_collinear(points) = let(
  119. n = points[1] - points[0],
  120. a = points[0],
  121. points1d = [ for(p = points) (p-a)*n ],
  122. min_i = min_index(points1d),
  123. max_i = max_index(points1d)
  124. ) [ min_i, max_i ];
  125. function min_index(values,min_,min_i_,i_) =
  126. i_ == undef ? min_index(values,values[0],0,1) :
  127. i_ >= len(values) ? min_i_ :
  128. values[i_] < min_ ? min_index(values,values[i_],i_,i_+1)
  129. : min_index(values,min_,min_i_,i_+1);
  130. function max_index(values,max_,max_i_,i_) =
  131. i_ == undef ? max_index(values,values[0],0,1) :
  132. i_ >= len(values) ? max_i_ :
  133. values[i_] > max_ ? max_index(values,values[i_],i_,i_+1)
  134. : max_index(values,max_,max_i_,i_+1);
  135. function remove_elements(array, elements) = [
  136. for (i = [0:len(array)-1])
  137. if (!search(i, elements))
  138. array[i]
  139. ];
  140. function remove_internal_edges(halfedges) = [
  141. for (h = halfedges)
  142. if (!contains(halfedges, reverse(h)))
  143. h
  144. ];
  145. function plane_project(point, a, b, c) = let(
  146. u = b-a,
  147. v = c-a,
  148. n = cross(u,v),
  149. w = cross(n,u),
  150. relpoint = point-a
  151. ) [relpoint * u, relpoint * w];
  152. function plane_unproject(point, a, b, c) = let(
  153. u = b-a,
  154. v = c-a,
  155. n = cross(u,v),
  156. w = cross(n,u)
  157. ) a + point[0] * u + point[1] * w;
  158. function reverse(arr) = [ for (i = [len(arr)-1:-1:0]) arr[i] ];
  159. function contains(arr, element) = search([element],arr)[0] != [] ? true : false;
  160. function find_conflicts(point, planes) = [
  161. for (i = [0:len(planes)-1])
  162. if (in_front(planes[i], point))
  163. i
  164. ];
  165. function find_first_noncollinear(line, points, i) =
  166. i >= len(points) ? len(points) :
  167. collinear(points[line[0]],
  168. points[line[1]],
  169. points[i]) ? find_first_noncollinear(line, points, i+1)
  170. : i;
  171. function find_first_noncoplanar(plane, points, i) =
  172. i >= len(points) ? len(points) :
  173. coplanar(plane, points[i]) ? find_first_noncoplanar(plane, points, i+1)
  174. : i;
  175. function distance(plane, point) = plane[0] * point - plane[1];
  176. function in_front(plane, point) = distance(plane, point) > epsilon;
  177. function coplanar(plane, point) = abs(distance(plane,point)) <= epsilon;
  178. function unit(v) = v/norm(v);
  179. function area_2d(a,b,c) = (
  180. a[0] * (b[1] - c[1]) +
  181. b[0] * (c[1] - a[1]) +
  182. c[0] * (a[1] - b[1])) / 2;
  183. function collinear(a,b,c) = abs(area_2d(a,b,c)) < epsilon;
  184. function spherical(cartesian) = [
  185. atan2(cartesian[1], cartesian[0]),
  186. asin(cartesian[2])
  187. ];
  188. function cartesian(spherical) = [
  189. cos(spherical[1]) * cos(spherical[0]),
  190. cos(spherical[1]) * sin(spherical[0]),
  191. sin(spherical[1])
  192. ];
  193. /// TESTCODE
  194. phi = 1.618033988749895;
  195. testpoints_on_sphere = [ for(p =
  196. [
  197. [1,phi,0], [-1,phi,0], [1,-phi,0], [-1,-phi,0],
  198. [0,1,phi], [0,-1,phi], [0,1,-phi], [0,-1,-phi],
  199. [phi,0,1], [-phi,0,1], [phi,0,-1], [-phi,0,-1]
  200. ])
  201. unit(p)
  202. ];
  203. testpoints_spherical = [ for(p = testpoints_on_sphere) spherical(p) ];
  204. testpoints_circular = [ for(a = [0:15:360-epsilon]) [cos(a),sin(a)] ];
  205. 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 ];
  206. testpoints_collinear_2d = let(u = unit([5,3])) [ for(i = [1:20]) rands(-1,1,1)[0] * u ];
  207. testpoints_collinear_3d = let(u = unit([5,3,-5])) [ for(i = [1:20]) rands(-1,1,1)[0] * u ];
  208. testpoints2d = 20 * [for (i = [1:10]) concat(rands(-1,1,2))];
  209. testpoints3d = 20 * [for (i = [1:50]) concat(rands(-1,1,3))];
  210. // All points are on the sphere, no point should be red
  211. translate([-50,0]) visualize_hull(20*testpoints_on_sphere);
  212. // 2D points
  213. translate([50,0]) visualize_hull(testpoints2d);
  214. // All points on a circle, no point should be red
  215. translate([0,50]) visualize_hull(20*testpoints_circular);
  216. // All points 3d but collinear
  217. translate([0,-50]) visualize_hull(20*testpoints_coplanar);
  218. // Collinear
  219. translate([50,50]) visualize_hull(20*testpoints_collinear_2d);
  220. // Collinear
  221. translate([-50,50]) visualize_hull(20*testpoints_collinear_3d);
  222. // 3D points
  223. visualize_hull(testpoints3d);
  224. module visualize_hull(points) {
  225. hull = hull(points);
  226. %if (len(hull) > 0 && len(hull[0]) > 0)
  227. polyhedron(points=points, faces = hull);
  228. else
  229. polyhedron(points=points, faces = [hull]);
  230. for (i = [0:len(points)-1]) assign(p = points[i], $fn = 16) {
  231. translate(p) {
  232. if (hull_contains_index(hull,i)) {
  233. color("blue") sphere(1);
  234. } else {
  235. color("red") sphere(1);
  236. }
  237. }
  238. }
  239. function hull_contains_index(hull, index) =
  240. search(index,hull,1,0) ||
  241. search(index,hull,1,1) ||
  242. search(index,hull,1,2);
  243. }