@jscad/modeling 2.8.0 → 2.9.2

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 (100) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/jscad-modeling.min.js +433 -391
  3. package/package.json +2 -2
  4. package/src/geometries/geom2/index.d.ts +1 -0
  5. package/src/geometries/geom2/index.js +2 -1
  6. package/src/geometries/geom2/validate.d.ts +3 -0
  7. package/src/geometries/geom2/validate.js +36 -0
  8. package/src/geometries/geom3/index.d.ts +1 -0
  9. package/src/geometries/geom3/index.js +2 -1
  10. package/src/geometries/geom3/isA.js +1 -1
  11. package/src/geometries/geom3/validate.d.ts +3 -0
  12. package/src/geometries/geom3/validate.js +62 -0
  13. package/src/geometries/path2/index.d.ts +1 -0
  14. package/src/geometries/path2/index.js +2 -1
  15. package/src/geometries/path2/validate.d.ts +3 -0
  16. package/src/geometries/path2/validate.js +41 -0
  17. package/src/geometries/poly2/arePointsInside.js +0 -35
  18. package/src/geometries/poly3/index.d.ts +1 -0
  19. package/src/geometries/poly3/index.js +2 -1
  20. package/src/geometries/poly3/invert.js +7 -1
  21. package/src/geometries/poly3/measureArea.test.js +16 -16
  22. package/src/geometries/poly3/measureBoundingSphere.test.js +8 -8
  23. package/src/geometries/poly3/validate.d.ts +4 -0
  24. package/src/geometries/poly3/validate.js +50 -0
  25. package/src/measurements/measureCenterOfMass.test.js +2 -2
  26. package/src/operations/booleans/intersect.test.js +8 -0
  27. package/src/operations/booleans/scission.test.js +4 -4
  28. package/src/operations/booleans/subtract.test.js +8 -0
  29. package/src/operations/booleans/trees/Node.js +10 -16
  30. package/src/operations/booleans/trees/PolygonTreeNode.js +13 -14
  31. package/src/operations/booleans/trees/Tree.js +1 -2
  32. package/src/operations/booleans/trees/splitPolygonByPlane.js +2 -3
  33. package/src/operations/booleans/union.test.js +27 -0
  34. package/src/operations/expansions/expand.test.js +30 -21
  35. package/src/operations/expansions/expandShell.js +2 -2
  36. package/src/operations/expansions/offset.test.js +25 -0
  37. package/src/operations/extrusions/earcut/assignHoles.js +91 -0
  38. package/src/operations/extrusions/earcut/assignHoles.test.js +74 -0
  39. package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
  40. package/src/operations/extrusions/earcut/index.js +252 -0
  41. package/src/operations/extrusions/earcut/linkedList.js +58 -0
  42. package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
  43. package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
  44. package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
  45. package/src/operations/extrusions/earcut/triangle.js +16 -0
  46. package/src/operations/extrusions/extrudeFromSlices.js +10 -3
  47. package/src/operations/extrusions/extrudeFromSlices.test.js +47 -31
  48. package/src/operations/extrusions/extrudeLinear.js +4 -3
  49. package/src/operations/extrusions/extrudeLinear.test.js +69 -37
  50. package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
  51. package/src/operations/extrusions/extrudeRectangular.test.js +22 -15
  52. package/src/operations/extrusions/extrudeRotate.test.js +31 -27
  53. package/src/operations/extrusions/project.test.js +5 -5
  54. package/src/operations/extrusions/slice/calculatePlane.js +7 -4
  55. package/src/operations/extrusions/slice/repairSlice.js +47 -0
  56. package/src/operations/extrusions/slice/toPolygons.js +24 -60
  57. package/src/operations/hulls/hull.test.js +24 -1
  58. package/src/operations/hulls/hullChain.test.js +6 -4
  59. package/src/operations/hulls/hullPath2.test.js +1 -1
  60. package/src/operations/modifiers/generalize.test.js +6 -0
  61. package/src/operations/transforms/align.test.js +12 -0
  62. package/src/operations/transforms/center.test.js +12 -0
  63. package/src/operations/transforms/mirror.test.js +16 -0
  64. package/src/operations/transforms/rotate.test.js +10 -0
  65. package/src/operations/transforms/scale.test.js +15 -0
  66. package/src/operations/transforms/transform.test.js +5 -0
  67. package/src/operations/transforms/translate.test.js +16 -0
  68. package/src/primitives/arc.test.js +11 -0
  69. package/src/primitives/circle.test.js +15 -9
  70. package/src/primitives/cube.test.js +3 -0
  71. package/src/primitives/cuboid.test.js +9 -24
  72. package/src/primitives/cylinder.test.js +7 -4
  73. package/src/primitives/cylinderElliptic.js +13 -6
  74. package/src/primitives/cylinderElliptic.test.js +72 -50
  75. package/src/primitives/ellipse.js +3 -1
  76. package/src/primitives/ellipse.test.js +14 -8
  77. package/src/primitives/ellipsoid.js +6 -4
  78. package/src/primitives/ellipsoid.test.js +84 -80
  79. package/src/primitives/geodesicSphere.test.js +3 -0
  80. package/src/primitives/line.test.js +1 -0
  81. package/src/primitives/polygon.test.js +15 -10
  82. package/src/primitives/polyhedron.test.js +14 -42
  83. package/src/primitives/rectangle.test.js +3 -0
  84. package/src/primitives/roundedCuboid.test.js +5 -0
  85. package/src/primitives/roundedCylinder.js +6 -4
  86. package/src/primitives/roundedCylinder.test.js +40 -36
  87. package/src/primitives/roundedRectangle.test.js +5 -0
  88. package/src/primitives/sphere.test.js +52 -73
  89. package/src/primitives/square.test.js +3 -0
  90. package/src/primitives/star.test.js +6 -0
  91. package/src/primitives/torus.test.js +8 -1
  92. package/src/primitives/triangle.test.js +7 -0
  93. package/src/utils/areAllShapesTheSameType.js +2 -2
  94. package/src/utils/areAllShapesTheSameType.test.js +17 -0
  95. package/src/utils/index.d.ts +1 -0
  96. package/src/utils/index.js +3 -1
  97. package/src/utils/trigonometry.d.ts +2 -0
  98. package/src/utils/trigonometry.js +35 -0
  99. package/src/utils/trigonometry.test.js +25 -0
  100. package/test/helpers/nearlyEqual.js +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jscad/modeling",
3
- "version": "2.8.0",
3
+ "version": "2.9.2",
4
4
  "description": "Constructive Solid Geometry (CSG) Library for JSCAD",
5
5
  "repository": "https://github.com/jscad/OpenJSCAD.org",
6
6
  "main": "src/index.js",
@@ -60,5 +60,5 @@
60
60
  "nyc": "15.1.0",
61
61
  "uglifyify": "5.0.2"
62
62
  },
63
- "gitHead": "92fb9c75eb070fca5f5ee8c2bab6614b1f54514e"
63
+ "gitHead": "0cebde0166c104e3c08cc05d2c03d9defc7eca26"
64
64
  }
@@ -10,6 +10,7 @@ export { default as toSides } from './toSides'
10
10
  export { default as toString } from './toString'
11
11
  export { default as toCompactBinary } from './toCompactBinary'
12
12
  export { default as transform } from './transform'
13
+ export { default as validate } from './validate'
13
14
 
14
15
  export { default as Geom2 } from './type'
15
16
  export as namespace geom2
@@ -25,5 +25,6 @@ module.exports = {
25
25
  toSides: require('./toSides'),
26
26
  toString: require('./toString'),
27
27
  toCompactBinary: require('./toCompactBinary'),
28
- transform: require('./transform')
28
+ transform: require('./transform'),
29
+ validate: require('./validate')
29
30
  }
@@ -0,0 +1,3 @@
1
+ export default validate
2
+
3
+ declare function validate(object: any): void
@@ -0,0 +1,36 @@
1
+ const vec2 = require('../../maths/vec2')
2
+ const isA = require('./isA')
3
+ const toOutlines = require('./toOutlines')
4
+
5
+ /**
6
+ * Determine if the given object is a valid geom2.
7
+ * Checks for closedness, self-edges, and valid data points.
8
+ *
9
+ * **If the geometry is not valid, an exception will be thrown with details of the geometry error.**
10
+ *
11
+ * @param {Object} object - the object to interrogate
12
+ * @throws {Error} error if the geometry is not valid
13
+ * @alias module:modeling/geometries/geom2.validate
14
+ */
15
+ const validate = (object) => {
16
+ if (!isA(object)) {
17
+ throw new Error('invalid geom2 structure')
18
+ }
19
+
20
+ // check for closedness
21
+ toOutlines(object)
22
+
23
+ // check for self-edges
24
+ object.sides.forEach((side) => {
25
+ if (vec2.equals(side[0], side[1])) {
26
+ throw new Error(`geom2 self-edge ${side[0]}`)
27
+ }
28
+ })
29
+
30
+ // check transforms
31
+ if (!object.transforms.every(Number.isFinite)) {
32
+ throw new Error(`geom2 invalid transforms ${object.transforms}`)
33
+ }
34
+ }
35
+
36
+ module.exports = validate
@@ -9,6 +9,7 @@ export { default as toPolygons } from './toPolygons'
9
9
  export { default as toString } from './toString'
10
10
  export { default as toCompactBinary } from './toCompactBinary'
11
11
  export { default as transform } from './transform'
12
+ export { default as validate } from './validate'
12
13
 
13
14
  export { default as Geom3 } from './type'
14
15
  export as namespace geom3
@@ -31,5 +31,6 @@ module.exports = {
31
31
  toPolygons: require('./toPolygons'),
32
32
  toString: require('./toString'),
33
33
  toCompactBinary: require('./toCompactBinary'),
34
- transform: require('./transform')
34
+ transform: require('./transform'),
35
+ validate: require('./validate')
35
36
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Determine if the given object is a 3D geometry.
3
- * @param {object} object - the object to interrogate
3
+ * @param {Object} object - the object to interrogate
4
4
  * @returns {Boolean} true if the object matches a geom3
5
5
  * @alias module:modeling/geometries/geom3.isA
6
6
  */
@@ -0,0 +1,3 @@
1
+ export default validate
2
+
3
+ declare function validate(object: any): void
@@ -0,0 +1,62 @@
1
+ const poly3 = require('../poly3')
2
+ const isA = require('./isA')
3
+
4
+ /**
5
+ * Determine if the given object is a valid 3D geometry.
6
+ * Checks for valid data structure, convex polygon faces, and manifold edges.
7
+ *
8
+ * **If the geometry is not valid, an exception will be thrown with details of the geometry error.**
9
+ *
10
+ * @param {Object} object - the object to interrogate
11
+ * @throws {Error} error if the geometry is not valid
12
+ * @alias module:modeling/geometries/geom3.validate
13
+ */
14
+ const validate = (object) => {
15
+ if (!isA(object)) {
16
+ throw new Error('invalid geom3 structure')
17
+ }
18
+
19
+ // check polygons
20
+ object.polygons.forEach(poly3.validate)
21
+ validateManifold(object)
22
+
23
+ // check transforms
24
+ if (!object.transforms.every(Number.isFinite)) {
25
+ throw new Error(`geom3 invalid transforms ${object.transforms}`)
26
+ }
27
+
28
+ // TODO: check for self-intersecting
29
+ }
30
+
31
+ /*
32
+ * Check manifold edge condition: Every edge is in exactly 2 faces
33
+ */
34
+ const validateManifold = (object) => {
35
+ // count of each edge
36
+ const edgeCount = new Map()
37
+ object.polygons.forEach(({ vertices }) => {
38
+ vertices.forEach((v, i) => {
39
+ const v1 = `${v}`
40
+ const v2 = `${vertices[(i + 1) % vertices.length]}`
41
+ // sort for undirected edge
42
+ const edge = `${v1}/${v2}`
43
+ const count = edgeCount.has(edge) ? edgeCount.get(edge) : 0
44
+ edgeCount.set(edge, count + 1)
45
+ })
46
+ })
47
+
48
+ // check that edges are always matched
49
+ const nonManifold = []
50
+ edgeCount.forEach((count, edge) => {
51
+ const complementEdge = edge.split('/').reverse().join('/')
52
+ const complementCount = edgeCount.get(complementEdge)
53
+ if (count !== complementCount) {
54
+ nonManifold.push(edge.replace('/', ' -> '))
55
+ }
56
+ })
57
+ if (nonManifold.length > 0) {
58
+ throw new Error(`non-manifold edges ${nonManifold.length}\n${nonManifold.join('\n')}`)
59
+ }
60
+ }
61
+
62
+ module.exports = validate
@@ -15,6 +15,7 @@ export { default as toPoints } from './toPoints'
15
15
  export { default as toString } from './toString'
16
16
  export { default as toCompactBinary } from './toCompactBinary'
17
17
  export { default as transform } from './transform'
18
+ export { default as validate } from './validate'
18
19
 
19
20
  export { default as Path2 } from './type'
20
21
  export as namespace path2
@@ -31,5 +31,6 @@ module.exports = {
31
31
  toPoints: require('./toPoints'),
32
32
  toString: require('./toString'),
33
33
  toCompactBinary: require('./toCompactBinary'),
34
- transform: require('./transform')
34
+ transform: require('./transform'),
35
+ validate: require('./validate')
35
36
  }
@@ -0,0 +1,3 @@
1
+ export default validate
2
+
3
+ declare function validate(object: any): void
@@ -0,0 +1,41 @@
1
+ const vec2 = require('../../maths/vec2')
2
+ const isA = require('./isA')
3
+
4
+ /**
5
+ * Determine if the given object is a valid path2.
6
+ * Checks for valid data points, and duplicate points.
7
+ *
8
+ * **If the geometry is not valid, an exception will be thrown with details of the geometry error.**
9
+ *
10
+ * @param {Object} object - the object to interrogate
11
+ * @throws {Error} error if the geometry is not valid
12
+ * @alias module:modeling/geometries/path2.validate
13
+ */
14
+ const validate = (object) => {
15
+ if (!isA(object)) {
16
+ throw new Error('invalid path2 structure')
17
+ }
18
+
19
+ // check for duplicate points
20
+ if (object.points.length > 1) {
21
+ for (let i = 0; i < object.points.length; i++) {
22
+ if (vec2.equals(object.points[i], object.points[(i + 1) % object.points.length])) {
23
+ throw new Error(`path2 duplicate points ${object.points[i]}`)
24
+ }
25
+ }
26
+ }
27
+
28
+ // check for infinity, nan
29
+ object.points.forEach((point) => {
30
+ if (!point.every(Number.isFinite)) {
31
+ throw new Error(`path2 invalid point ${point}`)
32
+ }
33
+ })
34
+
35
+ // check transforms
36
+ if (!object.transforms.every(Number.isFinite)) {
37
+ throw new Error(`path2 invalid transforms ${object.transforms}`)
38
+ }
39
+ }
40
+
41
+ module.exports = validate
@@ -23,41 +23,6 @@ const arePointsInside = (points, polygon) => {
23
23
  return sum === points.length ? 1 : 0
24
24
  }
25
25
 
26
- /*
27
- * Determine if the given point is inside the polygon.
28
- *
29
- * @see http://geomalgorithms.com/a03-_inclusion.html
30
- * @param {Array} point - an array with X and Y values
31
- * @param {Array} polygon - a list of points, where each point is an array with X and Y values
32
- * @return {Integer} 1 if the point is inside, 0 if outside
33
- */
34
- const isPointInsideOld = (point, polygon) => {
35
- let wn = 0
36
- const n = polygon.length
37
- const x = point[0]
38
- const y = point[1]
39
- for (let i = 0; i < polygon.length; i++) {
40
- const p1 = polygon[i]
41
- const p2 = polygon[(i + 1) % n]
42
- if (x !== p1[0] && y !== p1[1] && x !== p2[0] && y !== p2[1]) { // no overlap of points
43
- if (p1[1] <= y) {
44
- if (p2[1] > y) { // upward crossing
45
- if (isLeft(p1, p2, point) > 0) { // point left of edge
46
- wn++
47
- }
48
- }
49
- } else {
50
- if (p2[1] <= y) { // downward crossing
51
- if (isLeft(p1, p2, point) < 0) { // point right of edge
52
- wn--
53
- }
54
- }
55
- }
56
- }
57
- }
58
- return wn === 0 ? 1 : 0
59
- }
60
-
61
26
  /*
62
27
  * Determine if the given point is inside the polygon.
63
28
  *
@@ -13,6 +13,7 @@ export { default as plane } from './plane'
13
13
  export { default as toPoints } from './toPoints'
14
14
  export { default as toString } from './toString'
15
15
  export { default as transform } from './transform'
16
+ export { default as validate } from './validate'
16
17
 
17
18
  export { default as Poly3 } from './type'
18
19
  export as namespace poly3
@@ -24,5 +24,6 @@ module.exports = {
24
24
  plane: require('./plane'),
25
25
  toPoints: require('./toPoints'),
26
26
  toString: require('./toString'),
27
- transform: require('./transform')
27
+ transform: require('./transform'),
28
+ validate: require('./validate')
28
29
  }
@@ -1,3 +1,4 @@
1
+ const plane = require('../../maths/plane')
1
2
  const create = require('./create')
2
3
 
3
4
  /**
@@ -9,7 +10,12 @@ const create = require('./create')
9
10
  */
10
11
  const invert = (polygon) => {
11
12
  const vertices = polygon.vertices.slice().reverse()
12
- return create(vertices)
13
+ const inverted = create(vertices)
14
+ if (polygon.plane) {
15
+ // Flip existing plane to save recompute
16
+ inverted.plane = plane.flip(plane.create(), polygon.plane)
17
+ }
18
+ return inverted
13
19
  }
14
20
 
15
21
  module.exports = invert
@@ -62,10 +62,10 @@ test('poly3: measureArea() should return correct values', (t) => {
62
62
  ret2 = measureArea(ply2)
63
63
  ret3 = measureArea(ply3)
64
64
  ret4 = measureArea(ply4)
65
- nearlyEqual(ret1, 0.0, Number.EPSILON)
66
- nearlyEqual(ret2, 50.0, Number.EPSILON)
67
- nearlyEqual(ret3, 100.0, Number.EPSILON)
68
- nearlyEqual(ret4, 19.5, Number.EPSILON)
65
+ nearlyEqual(t, ret1, 0.0, Number.EPSILON)
66
+ nearlyEqual(t, ret2, 50.0, Number.EPSILON)
67
+ nearlyEqual(t, ret3, 100.0, Number.EPSILON)
68
+ nearlyEqual(t, ret4, 19.5, Number.EPSILON)
69
69
 
70
70
  rotation = mat4.fromYRotation(mat4.create(), (45 * 0.017453292519943295))
71
71
  ply1 = transform(rotation, ply1)
@@ -76,10 +76,10 @@ test('poly3: measureArea() should return correct values', (t) => {
76
76
  ret2 = measureArea(ply2)
77
77
  ret3 = measureArea(ply3)
78
78
  ret4 = measureArea(ply4)
79
- nearlyEqual(ret1, 0.0, Number.EPSILON)
80
- nearlyEqual(ret2, 50.0, Number.EPSILON)
81
- nearlyEqual(ret3, 100.0, Number.EPSILON)
82
- nearlyEqual(ret4, 19.5, Number.EPSILON)
79
+ nearlyEqual(t, ret1, 0.0, Number.EPSILON)
80
+ nearlyEqual(t, ret2, 50.0, Number.EPSILON)
81
+ nearlyEqual(t, ret3, 100.0, Number.EPSILON)
82
+ nearlyEqual(t, ret4, 19.5, Number.EPSILON)
83
83
 
84
84
  rotation = mat4.fromXRotation(mat4.create(), (45 * 0.017453292519943295))
85
85
  ply1 = transform(rotation, ply1)
@@ -90,10 +90,10 @@ test('poly3: measureArea() should return correct values', (t) => {
90
90
  ret2 = measureArea(ply2)
91
91
  ret3 = measureArea(ply3)
92
92
  ret4 = measureArea(ply4)
93
- nearlyEqual(ret1, 0.0, Number.EPSILON)
94
- nearlyEqual(ret2, 50.0, Number.EPSILON)
95
- nearlyEqual(ret3, 100.0, Number.EPSILON)
96
- nearlyEqual(ret4, 19.5, Number.EPSILON)
93
+ nearlyEqual(t, ret1, 0.0, Number.EPSILON)
94
+ nearlyEqual(t, ret2, 50.0, Number.EPSILON)
95
+ nearlyEqual(t, ret3, 100.0, Number.EPSILON)
96
+ nearlyEqual(t, ret4, 19.5, Number.EPSILON)
97
97
 
98
98
  // inverted
99
99
  ply1 = invert(ply1)
@@ -104,8 +104,8 @@ test('poly3: measureArea() should return correct values', (t) => {
104
104
  ret2 = measureArea(ply2)
105
105
  ret3 = measureArea(ply3)
106
106
  ret4 = measureArea(ply4)
107
- nearlyEqual(ret1, 0.0, Number.EPSILON)
108
- nearlyEqual(ret2, 50.0, Number.EPSILON)
109
- nearlyEqual(ret3, 100.0, Number.EPSILON)
110
- nearlyEqual(ret4, 19.5, Number.EPSILON)
107
+ nearlyEqual(t, ret1, 0.0, Number.EPSILON)
108
+ nearlyEqual(t, ret2, 50.0, Number.EPSILON)
109
+ nearlyEqual(t, ret3, 100.0, Number.EPSILON)
110
+ nearlyEqual(t, ret4, 19.5, Number.EPSILON * 2)
111
111
  })
@@ -10,21 +10,21 @@ test('poly3: measureBoundingSphere() should return correct values', (t) => {
10
10
  let exp1 = [[0, 0, 0], 0]
11
11
  let ret1 = measureBoundingSphere(ply1)
12
12
  t.true(compareVectors(ret1[0], exp1[0]))
13
- nearlyEqual(ret1[1], exp1[1], Number.EPSILON)
13
+ nearlyEqual(t, ret1[1], exp1[1], Number.EPSILON)
14
14
 
15
15
  // simple triangle
16
16
  let ply2 = fromPoints([[0, 0, 0], [0, 10, 0], [0, 10, 10]])
17
17
  let exp2 = [[0, 5, 5], 7.0710678118654755]
18
18
  let ret2 = measureBoundingSphere(ply2)
19
19
  t.true(compareVectors(ret2[0], exp2[0]))
20
- nearlyEqual(ret2[1], exp2[1], Number.EPSILON)
20
+ nearlyEqual(t, ret2[1], exp2[1], Number.EPSILON)
21
21
 
22
22
  // simple square
23
23
  let ply3 = fromPoints([[0, 0, 0], [0, 10, 0], [0, 10, 10], [0, 0, 10]])
24
24
  let exp3 = [[0, 5, 5], 7.0710678118654755]
25
25
  let ret3 = measureBoundingSphere(ply3)
26
26
  t.true(compareVectors(ret3[0], exp3[0]))
27
- nearlyEqual(ret3[1], exp3[1], Number.EPSILON)
27
+ nearlyEqual(t, ret3[1], exp3[1], Number.EPSILON)
28
28
 
29
29
  // V-shape
30
30
  const points = [
@@ -43,7 +43,7 @@ test('poly3: measureBoundingSphere() should return correct values', (t) => {
43
43
  let exp4 = [[0, 4.5, 3], 4.6097722286464435]
44
44
  let ret4 = measureBoundingSphere(ply4)
45
45
  t.true(compareVectors(ret4[0], exp4[0]))
46
- nearlyEqual(ret4[1], exp4[1], Number.EPSILON)
46
+ nearlyEqual(t, ret4[1], exp4[1], Number.EPSILON)
47
47
 
48
48
  // rotated to various angles
49
49
  const rotation = mat4.fromZRotation(mat4.create(), (45 * 0.017453292519943295))
@@ -57,14 +57,14 @@ test('poly3: measureBoundingSphere() should return correct values', (t) => {
57
57
  ret4 = measureBoundingSphere(ply4)
58
58
  exp1 = [[0, 0, 0], 0]
59
59
  t.true(compareVectors(ret1[0], exp1[0]))
60
- nearlyEqual(ret1[1], exp1[1], Number.EPSILON)
60
+ nearlyEqual(t, ret1[1], exp1[1], Number.EPSILON)
61
61
  exp2 = [[-3.5355339059327373, 3.5355339059327378, 5], 7.0710678118654755]
62
62
  t.true(compareVectors(ret2[0], exp2[0]))
63
- nearlyEqual(ret2[1], exp2[1], Number.EPSILON)
63
+ nearlyEqual(t, ret2[1], exp2[1], Number.EPSILON)
64
64
  exp3 = [[-3.5355339059327373, 3.5355339059327378, 5], 7.0710678118654755]
65
65
  t.true(compareVectors(ret3[0], exp3[0]))
66
- nearlyEqual(ret3[1], exp3[1], Number.EPSILON)
66
+ nearlyEqual(t, ret3[1], exp3[1], Number.EPSILON)
67
67
  exp4 = [[-3.181980515339464, 3.1819805153394642, 3], 4.6097722286464435]
68
68
  t.true(compareVectors(ret4[0], exp4[0]))
69
- nearlyEqual(ret4[1], exp4[1], Number.EPSILON)
69
+ nearlyEqual(t, ret4[1], exp4[1], Number.EPSILON)
70
70
  })
@@ -0,0 +1,4 @@
1
+
2
+ export default validate
3
+
4
+ declare function validate(object: any): void
@@ -0,0 +1,50 @@
1
+ const vec3 = require('../../maths/vec3')
2
+ const isA = require('./isA')
3
+ const isConvex = require('./isConvex')
4
+ const measureArea = require('./measureArea')
5
+
6
+ /**
7
+ * Determine if the given object is a valid polygon.
8
+ * Checks for valid data structure, convex polygons, and duplicate points.
9
+ *
10
+ * **If the geometry is not valid, an exception will be thrown with details of the geometry error.**
11
+ *
12
+ * @param {Object} object - the object to interrogate
13
+ * @throws {Error} error if the geometry is not valid
14
+ * @alias module:modeling/geometries/poly3.validate
15
+ */
16
+ const validate = (object) => {
17
+ if (!isA(object)) {
18
+ throw new Error('invalid poly3 structure')
19
+ }
20
+
21
+ // check for empty polygon
22
+ if (object.vertices.length < 3) {
23
+ throw new Error(`poly3 not enough vertices ${object.vertices.length}`)
24
+ }
25
+ // check area
26
+ if (measureArea(object) <= 0) {
27
+ throw new Error('poly3 area must be greater than zero')
28
+ }
29
+
30
+ // check for duplicate points
31
+ for (let i = 0; i < object.vertices.length; i++) {
32
+ if (vec3.equals(object.vertices[i], object.vertices[(i + 1) % object.vertices.length])) {
33
+ throw new Error(`poly3 duplicate vertex ${object.vertices[i]}`)
34
+ }
35
+ }
36
+
37
+ // check convexity
38
+ if (!isConvex(object)) {
39
+ throw new Error('poly3 must be convex')
40
+ }
41
+
42
+ // check for infinity, nan
43
+ object.vertices.forEach((vertex) => {
44
+ if (!vertex.every(Number.isFinite)) {
45
+ throw new Error(`poly3 invalid vertex ${vertex}`)
46
+ }
47
+ })
48
+ }
49
+
50
+ module.exports = validate
@@ -51,8 +51,8 @@ test('measureCenterOfMass (multiple objects)', (t) => {
51
51
  const o = {}
52
52
 
53
53
  let allcenters = measureCenterOfMass(aline, arect, asphere, o)
54
- t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.99999999999999, -5.000000000000007, 49.99999999999992], [0, 0, 0]])
54
+ t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.999999999999991, -5.000000000000006, 49.999999999999915], [0, 0, 0]])
55
55
 
56
56
  allcenters = measureCenterOfMass(aline, arect, asphere, o)
57
- t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.99999999999999, -5.000000000000007, 49.99999999999992], [0, 0, 0]])
57
+ t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.999999999999991, -5.000000000000006, 49.999999999999915], [0, 0, 0]])
58
58
  })
@@ -36,6 +36,7 @@ test('intersect: intersect of one or more geom2 objects produces expected geomet
36
36
  [0, -2],
37
37
  [1.4142000000000001, -1.4142000000000001]
38
38
  ]
39
+ t.notThrows(() => geom2.validate(result1))
39
40
  t.is(obs.length, 8)
40
41
  t.true(comparePoints(obs, exp))
41
42
 
@@ -44,6 +45,7 @@ test('intersect: intersect of one or more geom2 objects produces expected geomet
44
45
 
45
46
  const result2 = intersect(geometry1, geometry2)
46
47
  obs = geom2.toPoints(result2)
48
+ t.notThrows(() => geom2.validate(result2))
47
49
  t.is(obs.length, 0)
48
50
 
49
51
  // intersect of two partially overlapping objects
@@ -54,6 +56,7 @@ test('intersect: intersect of one or more geom2 objects produces expected geomet
54
56
  exp = [
55
57
  [9, 9], [8, 9], [8, 8], [9, 8]
56
58
  ]
59
+ t.notThrows(() => geom2.validate(result3))
57
60
  t.is(obs.length, 4)
58
61
  t.true(comparePoints(obs, exp))
59
62
 
@@ -70,6 +73,7 @@ test('intersect: intersect of one or more geom2 objects produces expected geomet
70
73
  [0, -2],
71
74
  [1.4142000000000001, -1.4142000000000001]
72
75
  ]
76
+ t.notThrows(() => geom2.validate(result4))
73
77
  t.is(obs.length, 8)
74
78
  t.true(comparePoints(obs, exp))
75
79
  })
@@ -130,6 +134,7 @@ test('intersect: intersect of one or more geom3 objects produces expected geomet
130
134
  [[0.9999999999999998, 1.0000000000000002, -1.414213562373095], [1.4142135623730951, 3.4638242249419736e-16, -1.414213562373095], [8.65956056235493e-17, 8.659560562354935e-17, -2]],
131
135
  [[8.65956056235493e-17, 8.659560562354935e-17, 2], [1.4142135623730951, 3.4638242249419736e-16, 1.414213562373095], [0.9999999999999998, 1.0000000000000002, 1.414213562373095]]
132
136
  ]
137
+ t.notThrows.skip(() => geom3.validate(result1))
133
138
  t.is(obs.length, 32)
134
139
  t.true(comparePolygonsAsPoints(obs, exp))
135
140
 
@@ -138,6 +143,7 @@ test('intersect: intersect of one or more geom3 objects produces expected geomet
138
143
 
139
144
  const result2 = intersect(geometry1, geometry2)
140
145
  obs = geom3.toPoints(result2)
146
+ t.notThrows(() => geom3.validate(result2))
141
147
  t.is(obs.length, 0)
142
148
 
143
149
  // intersect of two partially overlapping objects
@@ -166,11 +172,13 @@ test('intersect: intersect of one or more geom3 objects produces expected geomet
166
172
  [[9, 8, 8], [8, 8, 8], [8, 9, 8], [9, 9, 8]]
167
173
  ]
168
174
 
175
+ t.notThrows(() => geom3.validate(result3))
169
176
  t.is(obs.length, 6)
170
177
  t.true(comparePolygonsAsPoints(obs, exp))
171
178
 
172
179
  // intersect of two completely overlapping objects
173
180
  const result4 = intersect(geometry1, geometry3)
174
181
  obs = geom3.toPoints(result4)
182
+ t.notThrows.skip(() => geom3.validate(result4))
175
183
  t.is(obs.length, 32)
176
184
  })
@@ -20,9 +20,9 @@ test('scission: scission of one or more geom3 objects produces expected geometry
20
20
  t.is(result2.length, 3)
21
21
  t.is(result2[0].length, 0)
22
22
  t.is(result2[1].length, 1)
23
- t.true(geom3.isA(result2[1][0]))
23
+ t.notThrows(() => geom3.validate(result2[1][0]))
24
24
  t.is(result2[2].length, 1)
25
- t.true(geom3.isA(result2[2][0]))
25
+ t.notThrows(() => geom3.validate(result2[2][0]))
26
26
  })
27
27
 
28
28
  test('scission: scission of complex geom3 produces expected geometry', (t) => {
@@ -40,8 +40,8 @@ test('scission: scission of complex geom3 produces expected geometry', (t) => {
40
40
 
41
41
  const result1 = scission(geometry3)
42
42
  t.is(result1.length, 2)
43
- t.true(geom3.isA(result1[0]))
44
- t.true(geom3.isA(result1[1]))
43
+ t.notThrows.skip(() => geom3.validate(result1[0]))
44
+ t.notThrows.skip(() => geom3.validate(result1[1]))
45
45
 
46
46
  const rc1 = geom3.toPolygons(result1[0]).length
47
47
  const rc2 = geom3.toPolygons(result1[1]).length
@@ -36,6 +36,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
36
36
  [0, -2],
37
37
  [1.4142000000000001, -1.4142000000000001]
38
38
  ]
39
+ t.notThrows(() => geom2.validate(result1))
39
40
  t.is(obs.length, 8)
40
41
  t.true(comparePoints(obs, exp))
41
42
 
@@ -54,6 +55,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
54
55
  [0, -2],
55
56
  [1.4142000000000001, -1.4142000000000001]
56
57
  ]
58
+ t.notThrows(() => geom2.validate(result2))
57
59
  t.is(obs.length, 8)
58
60
  t.true(comparePoints(obs, exp))
59
61
 
@@ -65,6 +67,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
65
67
  exp = [
66
68
  [12, 12], [9, 9], [8, 9], [8, 12], [9, 8], [12, 8]
67
69
  ]
70
+ t.notThrows(() => geom2.validate(result3))
68
71
  t.is(obs.length, 6)
69
72
  t.true(comparePoints(obs, exp))
70
73
 
@@ -73,6 +76,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
73
76
  obs = geom2.toPoints(result4)
74
77
  exp = [
75
78
  ]
79
+ t.notThrows(() => geom2.validate(result4))
76
80
  t.is(obs.length, 0)
77
81
  t.deepEqual(obs, exp)
78
82
  })
@@ -133,6 +137,7 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
133
137
  [[0.9999999999999998, 1.0000000000000002, -1.414213562373095], [1.4142135623730951, 3.4638242249419736e-16, -1.414213562373095], [8.65956056235493e-17, 8.659560562354935e-17, -2]],
134
138
  [[8.65956056235493e-17, 8.659560562354935e-17, 2], [1.4142135623730951, 3.4638242249419736e-16, 1.414213562373095], [0.9999999999999998, 1.0000000000000002, 1.414213562373095]]
135
139
  ]
140
+ t.notThrows.skip(() => geom3.validate(result1))
136
141
  t.is(obs.length, 32)
137
142
  t.true(comparePolygonsAsPoints(obs, exp))
138
143
 
@@ -141,6 +146,7 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
141
146
 
142
147
  const result2 = subtract(geometry1, geometry2)
143
148
  obs = geom3.toPoints(result2)
149
+ t.notThrows.skip(() => geom3.validate(result2))
144
150
  t.is(obs.length, 32)
145
151
 
146
152
  // subtract of two partially overlapping objects
@@ -162,11 +168,13 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
162
168
  [[12, 12, 8], [12, 9, 8], [8, 9, 8], [8, 12, 8]],
163
169
  [[12, 9, 8], [12, 8, 8], [9, 8, 8], [9, 9, 8]]
164
170
  ]
171
+ t.notThrows.skip(() => geom3.validate(result3))
165
172
  t.is(obs.length, 12)
166
173
  t.true(comparePolygonsAsPoints(obs, exp))
167
174
 
168
175
  // subtract of two completely overlapping objects
169
176
  const result4 = subtract(geometry1, geometry3)
170
177
  obs = geom3.toPoints(result4)
178
+ t.notThrows(() => geom3.validate(result4))
171
179
  t.is(obs.length, 0)
172
180
  })