@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.
- package/CHANGELOG.md +33 -0
- package/dist/jscad-modeling.min.js +433 -391
- package/package.json +2 -2
- package/src/geometries/geom2/index.d.ts +1 -0
- package/src/geometries/geom2/index.js +2 -1
- package/src/geometries/geom2/validate.d.ts +3 -0
- package/src/geometries/geom2/validate.js +36 -0
- package/src/geometries/geom3/index.d.ts +1 -0
- package/src/geometries/geom3/index.js +2 -1
- package/src/geometries/geom3/isA.js +1 -1
- package/src/geometries/geom3/validate.d.ts +3 -0
- package/src/geometries/geom3/validate.js +62 -0
- package/src/geometries/path2/index.d.ts +1 -0
- package/src/geometries/path2/index.js +2 -1
- package/src/geometries/path2/validate.d.ts +3 -0
- package/src/geometries/path2/validate.js +41 -0
- package/src/geometries/poly2/arePointsInside.js +0 -35
- package/src/geometries/poly3/index.d.ts +1 -0
- package/src/geometries/poly3/index.js +2 -1
- package/src/geometries/poly3/invert.js +7 -1
- package/src/geometries/poly3/measureArea.test.js +16 -16
- package/src/geometries/poly3/measureBoundingSphere.test.js +8 -8
- package/src/geometries/poly3/validate.d.ts +4 -0
- package/src/geometries/poly3/validate.js +50 -0
- package/src/measurements/measureCenterOfMass.test.js +2 -2
- package/src/operations/booleans/intersect.test.js +8 -0
- package/src/operations/booleans/scission.test.js +4 -4
- package/src/operations/booleans/subtract.test.js +8 -0
- package/src/operations/booleans/trees/Node.js +10 -16
- package/src/operations/booleans/trees/PolygonTreeNode.js +13 -14
- package/src/operations/booleans/trees/Tree.js +1 -2
- package/src/operations/booleans/trees/splitPolygonByPlane.js +2 -3
- package/src/operations/booleans/union.test.js +27 -0
- package/src/operations/expansions/expand.test.js +30 -21
- package/src/operations/expansions/expandShell.js +2 -2
- package/src/operations/expansions/offset.test.js +25 -0
- package/src/operations/extrusions/earcut/assignHoles.js +91 -0
- package/src/operations/extrusions/earcut/assignHoles.test.js +74 -0
- package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
- package/src/operations/extrusions/earcut/index.js +252 -0
- package/src/operations/extrusions/earcut/linkedList.js +58 -0
- package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
- package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
- package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
- package/src/operations/extrusions/earcut/triangle.js +16 -0
- package/src/operations/extrusions/extrudeFromSlices.js +10 -3
- package/src/operations/extrusions/extrudeFromSlices.test.js +47 -31
- package/src/operations/extrusions/extrudeLinear.js +4 -3
- package/src/operations/extrusions/extrudeLinear.test.js +69 -37
- package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
- package/src/operations/extrusions/extrudeRectangular.test.js +22 -15
- package/src/operations/extrusions/extrudeRotate.test.js +31 -27
- package/src/operations/extrusions/project.test.js +5 -5
- package/src/operations/extrusions/slice/calculatePlane.js +7 -4
- package/src/operations/extrusions/slice/repairSlice.js +47 -0
- package/src/operations/extrusions/slice/toPolygons.js +24 -60
- package/src/operations/hulls/hull.test.js +24 -1
- package/src/operations/hulls/hullChain.test.js +6 -4
- package/src/operations/hulls/hullPath2.test.js +1 -1
- package/src/operations/modifiers/generalize.test.js +6 -0
- package/src/operations/transforms/align.test.js +12 -0
- package/src/operations/transforms/center.test.js +12 -0
- package/src/operations/transforms/mirror.test.js +16 -0
- package/src/operations/transforms/rotate.test.js +10 -0
- package/src/operations/transforms/scale.test.js +15 -0
- package/src/operations/transforms/transform.test.js +5 -0
- package/src/operations/transforms/translate.test.js +16 -0
- package/src/primitives/arc.test.js +11 -0
- package/src/primitives/circle.test.js +15 -9
- package/src/primitives/cube.test.js +3 -0
- package/src/primitives/cuboid.test.js +9 -24
- package/src/primitives/cylinder.test.js +7 -4
- package/src/primitives/cylinderElliptic.js +13 -6
- package/src/primitives/cylinderElliptic.test.js +72 -50
- package/src/primitives/ellipse.js +3 -1
- package/src/primitives/ellipse.test.js +14 -8
- package/src/primitives/ellipsoid.js +6 -4
- package/src/primitives/ellipsoid.test.js +84 -80
- package/src/primitives/geodesicSphere.test.js +3 -0
- package/src/primitives/line.test.js +1 -0
- package/src/primitives/polygon.test.js +15 -10
- package/src/primitives/polyhedron.test.js +14 -42
- package/src/primitives/rectangle.test.js +3 -0
- package/src/primitives/roundedCuboid.test.js +5 -0
- package/src/primitives/roundedCylinder.js +6 -4
- package/src/primitives/roundedCylinder.test.js +40 -36
- package/src/primitives/roundedRectangle.test.js +5 -0
- package/src/primitives/sphere.test.js +52 -73
- package/src/primitives/square.test.js +3 -0
- package/src/primitives/star.test.js +6 -0
- package/src/primitives/torus.test.js +8 -1
- package/src/primitives/triangle.test.js +7 -0
- package/src/utils/areAllShapesTheSameType.js +2 -2
- package/src/utils/areAllShapesTheSameType.test.js +17 -0
- package/src/utils/index.d.ts +1 -0
- package/src/utils/index.js +3 -1
- package/src/utils/trigonometry.d.ts +2 -0
- package/src/utils/trigonometry.js +35 -0
- package/src/utils/trigonometry.test.js +25 -0
- 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.
|
|
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": "
|
|
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
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Determine if the given object is a 3D geometry.
|
|
3
|
-
* @param {
|
|
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,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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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,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.
|
|
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.
|
|
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.
|
|
23
|
+
t.notThrows(() => geom3.validate(result2[1][0]))
|
|
24
24
|
t.is(result2[2].length, 1)
|
|
25
|
-
t.
|
|
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.
|
|
44
|
-
t.
|
|
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
|
})
|