@jscad/modeling 2.7.2 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/jscad-modeling.min.js +443 -398
  3. package/package.json +2 -2
  4. package/src/curves/bezier/tangentAt.test.js +1 -1
  5. package/src/curves/bezier/valueAt.test.js +1 -1
  6. package/src/geometries/geom2/index.d.ts +1 -0
  7. package/src/geometries/geom2/index.js +12 -1
  8. package/src/geometries/geom2/isA.js +2 -2
  9. package/src/geometries/geom2/toCompactBinary.js +4 -4
  10. package/src/geometries/geom2/toString.js +1 -1
  11. package/src/geometries/geom2/transform.test.js +1 -1
  12. package/src/geometries/geom2/validate.d.ts +3 -0
  13. package/src/geometries/geom2/validate.js +36 -0
  14. package/src/geometries/geom3/fromCompactBinary.js +1 -1
  15. package/src/geometries/geom3/index.d.ts +1 -0
  16. package/src/geometries/geom3/index.js +19 -1
  17. package/src/geometries/geom3/isA.js +2 -2
  18. package/src/geometries/geom3/toCompactBinary.js +4 -4
  19. package/src/geometries/geom3/toString.js +1 -1
  20. package/src/geometries/geom3/transform.test.js +1 -1
  21. package/src/geometries/geom3/validate.d.ts +3 -0
  22. package/src/geometries/geom3/validate.js +62 -0
  23. package/src/geometries/index.js +8 -1
  24. package/src/geometries/path2/eachPoint.js +3 -3
  25. package/src/geometries/path2/index.d.ts +1 -0
  26. package/src/geometries/path2/index.js +13 -1
  27. package/src/geometries/path2/isA.js +2 -2
  28. package/src/geometries/path2/reverse.js +4 -4
  29. package/src/geometries/path2/toCompactBinary.js +6 -6
  30. package/src/geometries/path2/toString.js +1 -1
  31. package/src/geometries/path2/transform.test.js +1 -1
  32. package/src/geometries/path2/validate.d.ts +3 -0
  33. package/src/geometries/path2/validate.js +41 -0
  34. package/src/geometries/poly2/arePointsInside.js +0 -35
  35. package/src/geometries/poly2/arePointsInside.test.js +1 -1
  36. package/src/geometries/poly2/index.js +6 -0
  37. package/src/geometries/poly3/index.d.ts +1 -0
  38. package/src/geometries/poly3/index.js +9 -2
  39. package/src/geometries/poly3/invert.js +7 -1
  40. package/src/geometries/poly3/isA.js +2 -2
  41. package/src/geometries/poly3/isConvex.js +2 -2
  42. package/src/geometries/poly3/measureArea.js +4 -4
  43. package/src/geometries/poly3/measureArea.test.js +16 -16
  44. package/src/geometries/poly3/measureBoundingBox.js +2 -2
  45. package/src/geometries/poly3/measureBoundingSphere.js +2 -2
  46. package/src/geometries/poly3/measureBoundingSphere.test.js +8 -8
  47. package/src/geometries/poly3/measureSignedVolume.js +4 -4
  48. package/src/geometries/poly3/toPoints.js +2 -2
  49. package/src/geometries/poly3/toString.js +2 -2
  50. package/src/geometries/poly3/transform.js +2 -2
  51. package/src/geometries/poly3/validate.d.ts +4 -0
  52. package/src/geometries/poly3/validate.js +50 -0
  53. package/src/maths/index.js +1 -1
  54. package/src/maths/line2/equals.js +2 -2
  55. package/src/maths/line2/fromValues.js +2 -2
  56. package/src/maths/line2/intersectPointOfLines.js +1 -1
  57. package/src/maths/line2/intersectPointOfLines.test.js +1 -1
  58. package/src/maths/line2/reverse.test.js +1 -1
  59. package/src/maths/line2/transform.test.js +1 -1
  60. package/src/maths/line3/equals.js +2 -2
  61. package/src/maths/line3/reverse.test.js +1 -1
  62. package/src/maths/line3/transform.test.js +1 -1
  63. package/src/maths/mat4/fromVectorRotation.js +1 -1
  64. package/src/maths/mat4/fromVectorRotation.test.js +1 -1
  65. package/src/maths/mat4/identity.test.js +1 -1
  66. package/src/maths/mat4/invert.js +18 -18
  67. package/src/maths/mat4/isIdentity.js +1 -1
  68. package/src/maths/mat4/isMirroring.js +4 -4
  69. package/src/maths/mat4/isMirroring.test.js +1 -1
  70. package/src/maths/mat4/leftMultiplyVec3.js +2 -2
  71. package/src/maths/mat4/toString.js +2 -2
  72. package/src/maths/mat4/translate.test.js +1 -1
  73. package/src/maths/plane/flip.test.js +1 -1
  74. package/src/maths/plane/fromPoints.d.ts +1 -1
  75. package/src/maths/plane/fromPoints.js +1 -3
  76. package/src/maths/plane/signedDistanceToPoint.js +1 -1
  77. package/src/maths/plane/transform.test.js +1 -1
  78. package/src/maths/utils/aboutEqualNormals.js +2 -2
  79. package/src/maths/vec2/abs.d.ts +1 -1
  80. package/src/maths/vec2/add.test.js +1 -1
  81. package/src/maths/vec2/angleDegrees.d.ts +1 -1
  82. package/src/maths/vec2/angleRadians.d.ts +1 -1
  83. package/src/maths/vec2/create.js +1 -1
  84. package/src/maths/vec2/cross.test.js +1 -1
  85. package/src/maths/vec2/divide.test.js +1 -1
  86. package/src/maths/vec2/fromAngleDegrees.js +1 -1
  87. package/src/maths/vec2/fromScalar.js +1 -1
  88. package/src/maths/vec2/length.d.ts +1 -1
  89. package/src/maths/vec2/length.js +1 -1
  90. package/src/maths/vec2/lerp.test.js +1 -1
  91. package/src/maths/vec2/multiply.test.js +1 -1
  92. package/src/maths/vec2/negate.test.js +1 -1
  93. package/src/maths/vec2/normal.js +1 -1
  94. package/src/maths/vec2/normalize.d.ts +1 -1
  95. package/src/maths/vec2/normalize.test.js +1 -1
  96. package/src/maths/vec2/rotate.test.js +1 -1
  97. package/src/maths/vec2/squaredLength.d.ts +1 -1
  98. package/src/maths/vec2/squaredLength.js +3 -3
  99. package/src/maths/vec2/subtract.test.js +1 -1
  100. package/src/maths/vec2/toString.js +1 -1
  101. package/src/maths/vec2/transform.test.js +1 -1
  102. package/src/maths/vec3/abs.d.ts +1 -1
  103. package/src/maths/vec3/add.test.js +1 -1
  104. package/src/maths/vec3/cross.test.js +1 -1
  105. package/src/maths/vec3/divide.test.js +1 -1
  106. package/src/maths/vec3/fromScalar.js +1 -1
  107. package/src/maths/vec3/fromVec2.d.ts +1 -1
  108. package/src/maths/vec3/fromVec2.js +3 -3
  109. package/src/maths/vec3/length.d.ts +1 -1
  110. package/src/maths/vec3/length.js +4 -4
  111. package/src/maths/vec3/lerp.test.js +1 -1
  112. package/src/maths/vec3/multiply.test.js +1 -1
  113. package/src/maths/vec3/negate.d.ts +1 -1
  114. package/src/maths/vec3/negate.test.js +1 -1
  115. package/src/maths/vec3/normalize.d.ts +1 -1
  116. package/src/maths/vec3/normalize.test.js +1 -1
  117. package/src/maths/vec3/rotateX.test.js +1 -1
  118. package/src/maths/vec3/rotateY.test.js +1 -1
  119. package/src/maths/vec3/rotateZ.test.js +1 -1
  120. package/src/maths/vec3/scale.test.js +1 -1
  121. package/src/maths/vec3/squaredLength.d.ts +1 -1
  122. package/src/maths/vec3/squaredLength.js +4 -4
  123. package/src/maths/vec3/subtract.test.js +1 -1
  124. package/src/maths/vec3/toString.js +1 -1
  125. package/src/maths/vec3/transform.test.js +1 -1
  126. package/src/maths/vec4/toString.js +1 -1
  127. package/src/maths/vec4/transform.test.js +1 -1
  128. package/src/measurements/measureBoundingSphere.js +4 -4
  129. package/src/measurements/measureCenterOfMass.js +1 -1
  130. package/src/measurements/measureCenterOfMass.test.js +2 -2
  131. package/src/operations/booleans/intersect.test.js +8 -0
  132. package/src/operations/booleans/mayOverlap.js +3 -3
  133. package/src/operations/booleans/retessellate.js +2 -2
  134. package/src/operations/booleans/scission.js +1 -1
  135. package/src/operations/booleans/scission.test.js +4 -4
  136. package/src/operations/booleans/subtract.js +1 -1
  137. package/src/operations/booleans/subtract.test.js +8 -0
  138. package/src/operations/booleans/trees/Node.js +10 -16
  139. package/src/operations/booleans/trees/PolygonTreeNode.js +13 -14
  140. package/src/operations/booleans/trees/Tree.js +1 -2
  141. package/src/operations/booleans/trees/splitPolygonByPlane.js +2 -3
  142. package/src/operations/booleans/union.test.js +28 -1
  143. package/src/operations/booleans/unionGeom3Sub.js +1 -1
  144. package/src/operations/expansions/expand.js +2 -2
  145. package/src/operations/expansions/expand.test.js +32 -55
  146. package/src/operations/expansions/expandShell.js +24 -18
  147. package/src/operations/expansions/offset.js +1 -1
  148. package/src/operations/expansions/offset.test.js +50 -89
  149. package/src/operations/expansions/offsetFromPoints.js +11 -6
  150. package/src/operations/extrusions/earcut/assignHoles.js +91 -0
  151. package/src/operations/extrusions/earcut/assignHoles.test.js +74 -0
  152. package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
  153. package/src/operations/extrusions/earcut/index.js +252 -0
  154. package/src/operations/extrusions/earcut/linkedList.js +58 -0
  155. package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
  156. package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
  157. package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
  158. package/src/operations/extrusions/earcut/triangle.js +16 -0
  159. package/src/operations/extrusions/extrudeFromSlices.js +10 -3
  160. package/src/operations/extrusions/extrudeFromSlices.test.js +47 -31
  161. package/src/operations/extrusions/extrudeLinear.js +10 -5
  162. package/src/operations/extrusions/extrudeLinear.test.js +91 -35
  163. package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
  164. package/src/operations/extrusions/extrudeLinearPath2.js +24 -0
  165. package/src/operations/extrusions/extrudeRectangular.js +1 -1
  166. package/src/operations/extrusions/extrudeRectangular.test.js +22 -15
  167. package/src/operations/extrusions/extrudeRotate.test.js +31 -27
  168. package/src/operations/extrusions/project.js +1 -1
  169. package/src/operations/extrusions/project.test.js +5 -5
  170. package/src/operations/extrusions/slice/calculatePlane.js +7 -4
  171. package/src/operations/extrusions/slice/isA.js +2 -2
  172. package/src/operations/extrusions/slice/repairSlice.js +47 -0
  173. package/src/operations/extrusions/slice/toPolygons.js +24 -60
  174. package/src/operations/hulls/hull.test.js +25 -2
  175. package/src/operations/hulls/hullChain.js +1 -1
  176. package/src/operations/hulls/hullChain.test.js +6 -4
  177. package/src/operations/hulls/hullGeom2.js +1 -1
  178. package/src/operations/hulls/hullPath2.js +6 -4
  179. package/src/operations/hulls/hullPath2.test.js +16 -0
  180. package/src/operations/hulls/hullPoints2.test.js +1 -1
  181. package/src/operations/modifiers/edges.js +1 -1
  182. package/src/operations/modifiers/generalize.js +1 -1
  183. package/src/operations/modifiers/generalize.test.js +6 -0
  184. package/src/operations/modifiers/snap.test.js +3 -3
  185. package/src/operations/transforms/align.d.ts +1 -1
  186. package/src/operations/transforms/align.test.js +12 -0
  187. package/src/operations/transforms/center.js +17 -17
  188. package/src/operations/transforms/center.test.js +12 -0
  189. package/src/operations/transforms/mirror.js +12 -12
  190. package/src/operations/transforms/mirror.test.js +16 -0
  191. package/src/operations/transforms/rotate.js +12 -12
  192. package/src/operations/transforms/rotate.test.js +10 -0
  193. package/src/operations/transforms/scale.js +19 -19
  194. package/src/operations/transforms/scale.test.js +15 -0
  195. package/src/operations/transforms/transform.js +3 -3
  196. package/src/operations/transforms/transform.test.js +5 -0
  197. package/src/operations/transforms/translate.js +14 -14
  198. package/src/operations/transforms/translate.test.js +16 -0
  199. package/src/primitives/arc.js +1 -1
  200. package/src/primitives/arc.test.js +11 -0
  201. package/src/primitives/circle.test.js +15 -9
  202. package/src/primitives/cube.test.js +3 -0
  203. package/src/primitives/cuboid.test.js +9 -24
  204. package/src/primitives/cylinder.test.js +7 -4
  205. package/src/primitives/cylinderElliptic.js +13 -6
  206. package/src/primitives/cylinderElliptic.test.js +72 -52
  207. package/src/primitives/ellipse.js +3 -1
  208. package/src/primitives/ellipse.test.js +14 -8
  209. package/src/primitives/ellipsoid.js +7 -5
  210. package/src/primitives/ellipsoid.test.js +84 -82
  211. package/src/primitives/geodesicSphere.d.ts +0 -1
  212. package/src/primitives/geodesicSphere.test.js +3 -0
  213. package/src/primitives/line.test.js +1 -0
  214. package/src/primitives/polygon.test.js +15 -10
  215. package/src/primitives/polyhedron.js +1 -1
  216. package/src/primitives/polyhedron.test.js +14 -42
  217. package/src/primitives/rectangle.test.js +3 -0
  218. package/src/primitives/roundedCuboid.test.js +5 -0
  219. package/src/primitives/roundedCylinder.js +6 -4
  220. package/src/primitives/roundedCylinder.test.js +40 -36
  221. package/src/primitives/roundedRectangle.test.js +5 -0
  222. package/src/primitives/sphere.test.js +52 -73
  223. package/src/primitives/square.test.js +3 -0
  224. package/src/primitives/star.test.js +6 -0
  225. package/src/primitives/torus.d.ts +0 -1
  226. package/src/primitives/torus.test.js +8 -1
  227. package/src/primitives/triangle.js +1 -1
  228. package/src/primitives/triangle.test.js +7 -0
  229. package/src/text/vectorText.js +2 -2
  230. package/src/utils/areAllShapesTheSameType.js +2 -2
  231. package/src/utils/areAllShapesTheSameType.test.js +17 -0
  232. package/src/utils/index.d.ts +1 -0
  233. package/src/utils/index.js +3 -1
  234. package/src/utils/padArrayToLength.js +1 -1
  235. package/src/utils/trigonometry.d.ts +2 -0
  236. package/src/utils/trigonometry.js +35 -0
  237. package/src/utils/trigonometry.test.js +25 -0
  238. package/test/helpers/nearlyEqual.js +4 -1
@@ -0,0 +1,252 @@
1
+ const eliminateHoles = require('./eliminateHoles')
2
+ const { removeNode, sortLinked } = require('./linkedList')
3
+ const { cureLocalIntersections, filterPoints, isValidDiagonal, linkedPolygon, splitPolygon } = require('./linkedPolygon')
4
+ const { area, pointInTriangle } = require('./triangle')
5
+
6
+ /*
7
+ * An implementation of the earcut polygon triangulation algorithm.
8
+ *
9
+ * Original source from https://github.com/mapbox/earcut
10
+ * Copyright (c) 2016 Mapbox
11
+ *
12
+ * @param {data} A flat array of vertex coordinates.
13
+ * @param {holeIndices} An array of hole indices if any.
14
+ * @param {dim} The number of coordinates per vertex in the input array.
15
+ */
16
+ const triangulate = (data, holeIndices, dim = 2) => {
17
+ const hasHoles = holeIndices && holeIndices.length
18
+ const outerLen = hasHoles ? holeIndices[0] * dim : data.length
19
+ let outerNode = linkedPolygon(data, 0, outerLen, dim, true)
20
+ const triangles = []
21
+
22
+ if (!outerNode || outerNode.next === outerNode.prev) return triangles
23
+
24
+ let minX, minY, maxX, maxY, invSize
25
+
26
+ if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim)
27
+
28
+ // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
29
+ if (data.length > 80 * dim) {
30
+ minX = maxX = data[0]
31
+ minY = maxY = data[1]
32
+
33
+ for (let i = dim; i < outerLen; i += dim) {
34
+ const x = data[i]
35
+ const y = data[i + 1]
36
+ if (x < minX) minX = x
37
+ if (y < minY) minY = y
38
+ if (x > maxX) maxX = x
39
+ if (y > maxY) maxY = y
40
+ }
41
+
42
+ // minX, minY and invSize are later used to transform coords into integers for z-order calculation
43
+ invSize = Math.max(maxX - minX, maxY - minY)
44
+ invSize = invSize !== 0 ? 1 / invSize : 0
45
+ }
46
+
47
+ earcutLinked(outerNode, triangles, dim, minX, minY, invSize)
48
+
49
+ return triangles
50
+ }
51
+
52
+ /*
53
+ * main ear slicing loop which triangulates a polygon (given as a linked list)
54
+ */
55
+ const earcutLinked = (ear, triangles, dim, minX, minY, invSize, pass) => {
56
+ if (!ear) return
57
+
58
+ // interlink polygon nodes in z-order
59
+ if (!pass && invSize) indexCurve(ear, minX, minY, invSize)
60
+
61
+ let stop = ear
62
+ let prev
63
+ let next
64
+
65
+ // iterate through ears, slicing them one by one
66
+ while (ear.prev !== ear.next) {
67
+ prev = ear.prev
68
+ next = ear.next
69
+
70
+ if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
71
+ // cut off the triangle
72
+ triangles.push(prev.i / dim)
73
+ triangles.push(ear.i / dim)
74
+ triangles.push(next.i / dim)
75
+
76
+ removeNode(ear)
77
+
78
+ // skipping the next vertex leads to less sliver triangles
79
+ ear = next.next
80
+ stop = next.next
81
+
82
+ continue
83
+ }
84
+
85
+ ear = next
86
+
87
+ // if we looped through the whole remaining polygon and can't find any more ears
88
+ if (ear === stop) {
89
+ // try filtering points and slicing again
90
+ if (!pass) {
91
+ earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1)
92
+
93
+ // if this didn't work, try curing all small self-intersections locally
94
+ } else if (pass === 1) {
95
+ ear = cureLocalIntersections(filterPoints(ear), triangles, dim)
96
+ earcutLinked(ear, triangles, dim, minX, minY, invSize, 2)
97
+
98
+ // as a last resort, try splitting the remaining polygon into two
99
+ } else if (pass === 2) {
100
+ splitEarcut(ear, triangles, dim, minX, minY, invSize)
101
+ }
102
+
103
+ break
104
+ }
105
+ }
106
+ }
107
+
108
+ /*
109
+ * check whether a polygon node forms a valid ear with adjacent nodes
110
+ */
111
+ const isEar = (ear) => {
112
+ const a = ear.prev
113
+ const b = ear
114
+ const c = ear.next
115
+
116
+ if (area(a, b, c) >= 0) return false // reflex, can't be an ear
117
+
118
+ // now make sure we don't have other points inside the potential ear
119
+ let p = ear.next.next
120
+
121
+ while (p !== ear.prev) {
122
+ if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next) >= 0) {
123
+ return false
124
+ }
125
+ p = p.next
126
+ }
127
+
128
+ return true
129
+ }
130
+
131
+ const isEarHashed = (ear, minX, minY, invSize) => {
132
+ const a = ear.prev
133
+ const b = ear
134
+ const c = ear.next
135
+
136
+ if (area(a, b, c) >= 0) return false // reflex, can't be an ear
137
+
138
+ // triangle bbox; min & max are calculated like this for speed
139
+ const minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x)
140
+ const minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y)
141
+ const maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x)
142
+ const maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y)
143
+
144
+ // z-order range for the current triangle bbox
145
+ const minZ = zOrder(minTX, minTY, minX, minY, invSize)
146
+ const maxZ = zOrder(maxTX, maxTY, minX, minY, invSize)
147
+
148
+ let p = ear.prevZ
149
+ let n = ear.nextZ
150
+
151
+ // look for points inside the triangle in both directions
152
+ while (p && p.z >= minZ && n && n.z <= maxZ) {
153
+ if (p !== ear.prev && p !== ear.next &&
154
+ pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
155
+ area(p.prev, p, p.next) >= 0) return false
156
+ p = p.prevZ
157
+
158
+ if (n !== ear.prev && n !== ear.next &&
159
+ pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
160
+ area(n.prev, n, n.next) >= 0) return false
161
+ n = n.nextZ
162
+ }
163
+
164
+ // look for remaining points in decreasing z-order
165
+ while (p && p.z >= minZ) {
166
+ if (p !== ear.prev && p !== ear.next &&
167
+ pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
168
+ area(p.prev, p, p.next) >= 0) return false
169
+ p = p.prevZ
170
+ }
171
+
172
+ // look for remaining points in increasing z-order
173
+ while (n && n.z <= maxZ) {
174
+ if (n !== ear.prev && n !== ear.next &&
175
+ pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
176
+ area(n.prev, n, n.next) >= 0) return false
177
+ n = n.nextZ
178
+ }
179
+
180
+ return true
181
+ }
182
+
183
+ /*
184
+ * try splitting polygon into two and triangulate them independently
185
+ */
186
+ const splitEarcut = (start, triangles, dim, minX, minY, invSize) => {
187
+ // look for a valid diagonal that divides the polygon into two
188
+ let a = start
189
+ do {
190
+ let b = a.next.next
191
+ while (b !== a.prev) {
192
+ if (a.i !== b.i && isValidDiagonal(a, b)) {
193
+ // split the polygon in two by the diagonal
194
+ let c = splitPolygon(a, b)
195
+
196
+ // filter colinear points around the cuts
197
+ a = filterPoints(a, a.next)
198
+ c = filterPoints(c, c.next)
199
+
200
+ // run earcut on each half
201
+ earcutLinked(a, triangles, dim, minX, minY, invSize)
202
+ earcutLinked(c, triangles, dim, minX, minY, invSize)
203
+ return
204
+ }
205
+
206
+ b = b.next
207
+ }
208
+
209
+ a = a.next
210
+ } while (a !== start)
211
+ }
212
+
213
+ /*
214
+ * interlink polygon nodes in z-order
215
+ */
216
+ const indexCurve = (start, minX, minY, invSize) => {
217
+ let p = start
218
+ do {
219
+ if (p.z === null) p.z = zOrder(p.x, p.y, minX, minY, invSize)
220
+ p.prevZ = p.prev
221
+ p.nextZ = p.next
222
+ p = p.next
223
+ } while (p !== start)
224
+
225
+ p.prevZ.nextZ = null
226
+ p.prevZ = null
227
+
228
+ sortLinked(p, (p) => p.z)
229
+ }
230
+
231
+ /*
232
+ * z-order of a point given coords and inverse of the longer side of data bbox
233
+ */
234
+ const zOrder = (x, y, minX, minY, invSize) => {
235
+ // coords are transformed into non-negative 15-bit integer range
236
+ x = 32767 * (x - minX) * invSize
237
+ y = 32767 * (y - minY) * invSize
238
+
239
+ x = (x | (x << 8)) & 0x00FF00FF
240
+ x = (x | (x << 4)) & 0x0F0F0F0F
241
+ x = (x | (x << 2)) & 0x33333333
242
+ x = (x | (x << 1)) & 0x55555555
243
+
244
+ y = (y | (y << 8)) & 0x00FF00FF
245
+ y = (y | (y << 4)) & 0x0F0F0F0F
246
+ y = (y | (y << 2)) & 0x33333333
247
+ y = (y | (y << 1)) & 0x55555555
248
+
249
+ return x | (y << 1)
250
+ }
251
+
252
+ module.exports = triangulate
@@ -0,0 +1,58 @@
1
+ const sortLinked = require('./linkedListSort')
2
+
3
+ class Node {
4
+ constructor (i, x, y) {
5
+ // vertex index in coordinates array
6
+ this.i = i
7
+
8
+ // vertex coordinates
9
+ this.x = x
10
+ this.y = y
11
+
12
+ // previous and next vertex nodes in a polygon ring
13
+ this.prev = null
14
+ this.next = null
15
+
16
+ // z-order curve value
17
+ this.z = null
18
+
19
+ // previous and next nodes in z-order
20
+ this.prevZ = null
21
+ this.nextZ = null
22
+
23
+ // indicates whether this is a steiner point
24
+ this.steiner = false
25
+ }
26
+ }
27
+
28
+ /*
29
+ * create a node and optionally link it with previous one (in a circular doubly linked list)
30
+ */
31
+ const insertNode = (i, x, y, last) => {
32
+ const p = new Node(i, x, y)
33
+
34
+ if (!last) {
35
+ p.prev = p
36
+ p.next = p
37
+ } else {
38
+ p.next = last.next
39
+ p.prev = last
40
+ last.next.prev = p
41
+ last.next = p
42
+ }
43
+
44
+ return p
45
+ }
46
+
47
+ /*
48
+ * remove a node and join prev with next nodes
49
+ */
50
+ const removeNode = (p) => {
51
+ p.next.prev = p.prev
52
+ p.prev.next = p.next
53
+
54
+ if (p.prevZ) p.prevZ.nextZ = p.nextZ
55
+ if (p.nextZ) p.nextZ.prevZ = p.prevZ
56
+ }
57
+
58
+ module.exports = { Node, insertNode, removeNode, sortLinked }
@@ -0,0 +1,54 @@
1
+
2
+ // Simon Tatham's linked list merge sort algorithm
3
+ // https://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
4
+ const sortLinked = (list, fn) => {
5
+ let i, p, q, e, numMerges
6
+ let inSize = 1
7
+
8
+ do {
9
+ p = list
10
+ list = null
11
+ let tail = null
12
+ numMerges = 0
13
+
14
+ while (p) {
15
+ numMerges++
16
+ q = p
17
+ let pSize = 0
18
+ for (i = 0; i < inSize; i++) {
19
+ pSize++
20
+ q = q.nextZ
21
+ if (!q) break
22
+ }
23
+
24
+ let qSize = inSize
25
+
26
+ while (pSize > 0 || (qSize > 0 && q)) {
27
+ if (pSize !== 0 && (qSize === 0 || !q || fn(p) <= fn(q))) {
28
+ e = p
29
+ p = p.nextZ
30
+ pSize--
31
+ } else {
32
+ e = q
33
+ q = q.nextZ
34
+ qSize--
35
+ }
36
+
37
+ if (tail) tail.nextZ = e
38
+ else list = e
39
+
40
+ e.prevZ = tail
41
+ tail = e
42
+ }
43
+
44
+ p = q
45
+ }
46
+
47
+ tail.nextZ = null
48
+ inSize *= 2
49
+ } while (numMerges > 1)
50
+
51
+ return list
52
+ }
53
+
54
+ module.exports = sortLinked
@@ -0,0 +1,197 @@
1
+ const { Node, insertNode, removeNode } = require('./linkedList')
2
+ const { area } = require('./triangle')
3
+
4
+ /*
5
+ * create a circular doubly linked list from polygon points in the specified winding order
6
+ */
7
+ const linkedPolygon = (data, start, end, dim, clockwise) => {
8
+ let last
9
+
10
+ if (clockwise === (signedArea(data, start, end, dim) > 0)) {
11
+ for (let i = start; i < end; i += dim) {
12
+ last = insertNode(i, data[i], data[i + 1], last)
13
+ }
14
+ } else {
15
+ for (let i = end - dim; i >= start; i -= dim) {
16
+ last = insertNode(i, data[i], data[i + 1], last)
17
+ }
18
+ }
19
+
20
+ if (last && equals(last, last.next)) {
21
+ removeNode(last)
22
+ last = last.next
23
+ }
24
+
25
+ return last
26
+ }
27
+
28
+ /*
29
+ * eliminate colinear or duplicate points
30
+ */
31
+ const filterPoints = (start, end) => {
32
+ if (!start) return start
33
+ if (!end) end = start
34
+
35
+ let p = start
36
+ let again
37
+ do {
38
+ again = false
39
+
40
+ if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
41
+ removeNode(p)
42
+ p = end = p.prev
43
+ if (p === p.next) break
44
+ again = true
45
+ } else {
46
+ p = p.next
47
+ }
48
+ } while (again || p !== end)
49
+
50
+ return end
51
+ }
52
+
53
+ /*
54
+ * go through all polygon nodes and cure small local self-intersections
55
+ */
56
+ const cureLocalIntersections = (start, triangles, dim) => {
57
+ let p = start
58
+ do {
59
+ const a = p.prev
60
+ const b = p.next.next
61
+
62
+ if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
63
+ triangles.push(a.i / dim)
64
+ triangles.push(p.i / dim)
65
+ triangles.push(b.i / dim)
66
+
67
+ // remove two nodes involved
68
+ removeNode(p)
69
+ removeNode(p.next)
70
+
71
+ p = start = b
72
+ }
73
+
74
+ p = p.next
75
+ } while (p !== start)
76
+
77
+ return filterPoints(p)
78
+ }
79
+
80
+ /*
81
+ * check if a polygon diagonal intersects any polygon segments
82
+ */
83
+ const intersectsPolygon = (a, b) => {
84
+ let p = a
85
+ do {
86
+ if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
87
+ intersects(p, p.next, a, b)) return true
88
+ p = p.next
89
+ } while (p !== a)
90
+
91
+ return false
92
+ }
93
+
94
+ /*
95
+ * check if a polygon diagonal is locally inside the polygon
96
+ */
97
+ const locallyInside = (a, b) => area(a.prev, a, a.next) < 0
98
+ ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0
99
+ : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0
100
+
101
+ /*
102
+ * check if the middle point of a polygon diagonal is inside the polygon
103
+ */
104
+ const middleInside = (a, b) => {
105
+ let p = a
106
+ let inside = false
107
+ const px = (a.x + b.x) / 2
108
+ const py = (a.y + b.y) / 2
109
+ do {
110
+ if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
111
+ (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)) { inside = !inside }
112
+ p = p.next
113
+ } while (p !== a)
114
+
115
+ return inside
116
+ }
117
+
118
+ /*
119
+ * link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two
120
+ * if one belongs to the outer ring and another to a hole, it merges it into a single ring
121
+ */
122
+ const splitPolygon = (a, b) => {
123
+ const a2 = new Node(a.i, a.x, a.y)
124
+ const b2 = new Node(b.i, b.x, b.y)
125
+ const an = a.next
126
+ const bp = b.prev
127
+
128
+ a.next = b
129
+ b.prev = a
130
+
131
+ a2.next = an
132
+ an.prev = a2
133
+
134
+ b2.next = a2
135
+ a2.prev = b2
136
+
137
+ bp.next = b2
138
+ b2.prev = bp
139
+
140
+ return b2
141
+ }
142
+
143
+ /*
144
+ * check if a diagonal between two polygon nodes is valid (lies in polygon interior)
145
+ */
146
+ const isValidDiagonal = (a, b) => a.next.i !== b.i &&
147
+ a.prev.i !== b.i &&
148
+ !intersectsPolygon(a, b) && // doesn't intersect other edges
149
+ (
150
+ locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
151
+ (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
152
+ equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0
153
+ )
154
+
155
+ /*
156
+ * check if two segments intersect
157
+ */
158
+ const intersects = (p1, q1, p2, q2) => {
159
+ const o1 = Math.sign(area(p1, q1, p2))
160
+ const o2 = Math.sign(area(p1, q1, q2))
161
+ const o3 = Math.sign(area(p2, q2, p1))
162
+ const o4 = Math.sign(area(p2, q2, q1))
163
+
164
+ if (o1 !== o2 && o3 !== o4) return true // general case
165
+
166
+ if (o1 === 0 && onSegment(p1, p2, q1)) return true // p1, q1 and p2 are colinear and p2 lies on p1q1
167
+ if (o2 === 0 && onSegment(p1, q2, q1)) return true // p1, q1 and q2 are colinear and q2 lies on p1q1
168
+ if (o3 === 0 && onSegment(p2, p1, q2)) return true // p2, q2 and p1 are colinear and p1 lies on p2q2
169
+ if (o4 === 0 && onSegment(p2, q1, q2)) return true // p2, q2 and q1 are colinear and q1 lies on p2q2
170
+
171
+ return false
172
+ }
173
+
174
+ /*
175
+ * for colinear points p, q, r, check if point q lies on segment pr
176
+ */
177
+ const onSegment = (p, q, r) => q.x <= Math.max(p.x, r.x) &&
178
+ q.x >= Math.min(p.x, r.x) &&
179
+ q.y <= Math.max(p.y, r.y) &&
180
+ q.y >= Math.min(p.y, r.y)
181
+
182
+ const signedArea = (data, start, end, dim) => {
183
+ let sum = 0
184
+ for (let i = start, j = end - dim; i < end; i += dim) {
185
+ sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1])
186
+ j = i
187
+ }
188
+
189
+ return sum
190
+ }
191
+
192
+ /*
193
+ * check if two points are equal
194
+ */
195
+ const equals = (p1, p2) => p1.x === p2.x && p1.y === p2.y
196
+
197
+ module.exports = { cureLocalIntersections, filterPoints, isValidDiagonal, linkedPolygon, locallyInside, splitPolygon }
@@ -0,0 +1,64 @@
1
+ const geom2 = require('../../../geometries/geom2')
2
+ const plane = require('../../../maths/plane')
3
+ const vec2 = require('../../../maths/vec2')
4
+ const vec3 = require('../../../maths/vec3')
5
+ const calculatePlane = require('../slice/calculatePlane')
6
+ const assignHoles = require('./assignHoles')
7
+
8
+ /*
9
+ * Constructs a polygon hierarchy which associates holes with their outer solids.
10
+ * This class maps a 3D polygon onto a 2D space using an orthonormal basis.
11
+ * It tracks the mapping so that points can be reversed back to 3D losslessly.
12
+ */
13
+ class PolygonHierarchy {
14
+ constructor (slice) {
15
+ this.plane = calculatePlane(slice)
16
+
17
+ // create an orthonormal basis
18
+ // choose an arbitrary right hand vector, making sure it is somewhat orthogonal to the plane normal
19
+ const rightvector = vec3.orthogonal(vec3.create(), this.plane)
20
+ const perp = vec3.cross(vec3.create(), this.plane, rightvector)
21
+ this.v = vec3.normalize(perp, perp)
22
+ this.u = vec3.cross(vec3.create(), this.v, this.plane)
23
+
24
+ // map from 2D to original 3D points
25
+ this.basisMap = new Map()
26
+
27
+ // project slice onto 2D plane
28
+ const projected = slice.edges.map((e) => e.map((v) => this.to2D(v)))
29
+
30
+ // compute polygon hierarchies, assign holes to solids
31
+ const geometry = geom2.create(projected)
32
+ this.roots = assignHoles(geometry)
33
+ }
34
+
35
+ /*
36
+ * project a 3D point onto the 2D plane
37
+ */
38
+ to2D (vector3) {
39
+ const vector2 = vec2.fromValues(vec3.dot(vector3, this.u), vec3.dot(vector3, this.v))
40
+ this.basisMap.set(vector2, vector3)
41
+ return vector2
42
+ }
43
+
44
+ /*
45
+ * un-project a 2D point back into 3D
46
+ */
47
+ to3D (vector2) {
48
+ // use a map to get the original 3D, no floating point error
49
+ const original = this.basisMap.get(vector2)
50
+ if (original) {
51
+ return original
52
+ } else {
53
+ console.log('Warning: point not in original slice')
54
+ const v1 = vec3.scale(vec3.create(), this.u, vector2[0])
55
+ const v2 = vec3.scale(vec3.create(), this.v, vector2[1])
56
+
57
+ const planeOrigin = vec3.scale(vec3.create(), plane, plane[3])
58
+ const v3 = vec3.add(v1, v1, planeOrigin)
59
+ return vec3.add(v2, v2, v3)
60
+ }
61
+ }
62
+ }
63
+
64
+ module.exports = PolygonHierarchy
@@ -0,0 +1,16 @@
1
+
2
+ /*
3
+ * check if a point lies within a convex triangle
4
+ */
5
+ const pointInTriangle = (ax, ay, bx, by, cx, cy, px, py) => (
6
+ (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
7
+ (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
8
+ (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0
9
+ )
10
+
11
+ /*
12
+ * signed area of a triangle
13
+ */
14
+ const area = (p, q, r) => (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y)
15
+
16
+ module.exports = { area, pointInTriangle }
@@ -5,6 +5,7 @@ const geom3 = require('../../geometries/geom3')
5
5
  const poly3 = require('../../geometries/poly3')
6
6
 
7
7
  const slice = require('./slice')
8
+ const repairSlice = require('./slice/repairSlice')
8
9
 
9
10
  const extrudeWalls = require('./extrudeWalls')
10
11
 
@@ -25,6 +26,7 @@ const defaultCallback = (progress, index, base) => {
25
26
  * @param {Boolean} [options.capStart=true] the solid should have a cap at the start
26
27
  * @param {Boolean} [options.capEnd=true] the solid should have a cap at the end
27
28
  * @param {Boolean} [options.close=false] the solid should have a closing section between start and end
29
+ * @param {Boolean} [options.repair=true] - repair gaps in the geometry
28
30
  * @param {Function} [options.callback] the callback function that generates each slice
29
31
  * @param {Object} base - the base object which is used to create slices (see the example for callback information)
30
32
  * @return {geom3} the extruded shape
@@ -48,12 +50,18 @@ const extrudeFromSlices = (options, base) => {
48
50
  capStart: true,
49
51
  capEnd: true,
50
52
  close: false,
53
+ repair: true,
51
54
  callback: defaultCallback
52
55
  }
53
- const { numberOfSlices, capStart, capEnd, close, callback: generate } = Object.assign({ }, defaults, options)
56
+ const { numberOfSlices, capStart, capEnd, close, repair, callback: generate } = Object.assign({ }, defaults, options)
54
57
 
55
58
  if (numberOfSlices < 2) throw new Error('numberOfSlices must be 2 or more')
56
59
 
60
+ // Repair gaps in the base slice
61
+ if (repair) {
62
+ repairSlice(base)
63
+ }
64
+
57
65
  const sMax = numberOfSlices - 1
58
66
 
59
67
  let startSlice = null
@@ -90,8 +98,7 @@ const extrudeFromSlices = (options, base) => {
90
98
  }
91
99
  if (capStart) {
92
100
  // create a cap at the start
93
- slice.reverse(startSlice, startSlice)
94
- const startPolygons = slice.toPolygons(startSlice)
101
+ const startPolygons = slice.toPolygons(startSlice).map(poly3.invert)
95
102
  polygons = polygons.concat(startPolygons)
96
103
  }
97
104
  if (!capStart && !capEnd) {