@jscad/modeling 2.9.0 → 2.9.3
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 +42 -0
- package/README.md +4 -4
- package/dist/jscad-modeling.min.js +437 -428
- package/package.json +3 -2
- package/src/colors/colorize.test.js +1 -1
- package/src/geometries/geom2/index.d.ts +1 -0
- package/src/geometries/geom2/index.js +2 -1
- package/src/geometries/geom2/toOutlines.js +66 -52
- package/src/geometries/geom2/validate.d.ts +3 -0
- package/src/geometries/geom2/validate.js +36 -0
- package/src/geometries/geom3/create.js +1 -1
- package/src/geometries/geom3/create.test.js +1 -1
- package/src/geometries/geom3/fromPoints.js +1 -1
- 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 -1
- package/src/geometries/path2/index.js +2 -2
- 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/create.js +1 -1
- package/src/geometries/poly3/index.d.ts +1 -0
- package/src/geometries/poly3/index.js +2 -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 +64 -0
- package/src/maths/constants.d.ts +1 -0
- package/src/maths/constants.js +11 -0
- package/src/maths/utils/aboutEqualNormals.js +1 -5
- package/src/measurements/measureCenterOfMass.test.js +2 -2
- package/src/operations/booleans/intersect.test.js +8 -0
- package/src/operations/booleans/intersectGeom3.js +2 -1
- package/src/operations/booleans/scission.test.js +4 -4
- package/src/operations/booleans/subtract.test.js +8 -0
- package/src/operations/booleans/subtractGeom3.js +2 -1
- package/src/operations/booleans/to3DWalls.js +1 -1
- 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/booleans/unionGeom3.js +2 -1
- package/src/operations/expansions/expand.test.js +30 -21
- package/src/operations/expansions/expandGeom3.test.js +14 -14
- package/src/operations/expansions/expandShell.js +5 -4
- package/src/operations/expansions/extrudePolygon.js +7 -7
- package/src/operations/expansions/offset.test.js +25 -0
- package/src/operations/extrusions/earcut/assignHoles.js +7 -3
- package/src/operations/extrusions/earcut/assignHoles.test.js +50 -4
- package/src/operations/extrusions/earcut/linkedList.js +1 -1
- package/src/operations/extrusions/extrudeFromSlices.test.js +16 -10
- package/src/operations/extrusions/extrudeLinear.test.js +15 -9
- package/src/operations/extrusions/extrudeRectangular.test.js +15 -8
- package/src/operations/extrusions/extrudeRotate.js +5 -1
- package/src/operations/extrusions/extrudeRotate.test.js +12 -0
- package/src/operations/extrusions/extrudeWalls.js +2 -2
- package/src/operations/extrusions/project.js +11 -14
- package/src/operations/extrusions/project.test.js +55 -55
- package/src/operations/hulls/hull.test.js +24 -1
- package/src/operations/hulls/hullChain.test.js +6 -4
- package/src/operations/hulls/hullGeom2.js +6 -18
- package/src/operations/hulls/hullGeom3.js +5 -18
- package/src/operations/hulls/hullPath2.js +4 -14
- package/src/operations/hulls/hullPath2.test.js +1 -1
- package/src/operations/hulls/hullPoints2.js +43 -92
- package/src/operations/hulls/toUniquePoints.js +34 -0
- package/src/operations/modifiers/generalize.js +2 -13
- package/src/operations/modifiers/generalize.test.js +5 -31
- package/src/operations/modifiers/insertTjunctions.js +1 -1
- package/src/operations/modifiers/insertTjunctions.test.js +21 -21
- package/src/operations/modifiers/mergePolygons.js +11 -14
- package/src/operations/{booleans → modifiers}/reTesselateCoplanarPolygons.js +1 -1
- package/src/operations/{booleans → modifiers}/reTesselateCoplanarPolygons.test.js +5 -5
- package/src/operations/{booleans → modifiers}/retessellate.js +2 -9
- package/src/operations/{booleans → modifiers}/retessellate.test.js +0 -0
- package/src/operations/modifiers/snapPolygons.test.js +12 -12
- package/src/operations/modifiers/triangulatePolygons.js +3 -3
- package/src/operations/transforms/align.test.js +12 -0
- package/src/operations/transforms/center.js +1 -1
- 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.js +1 -1
- package/src/primitives/cuboid.test.js +9 -24
- package/src/primitives/cylinder.test.js +7 -4
- package/src/primitives/cylinderElliptic.js +14 -7
- 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 +8 -6
- 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.js +1 -1
- package/src/primitives/polyhedron.test.js +14 -42
- package/src/primitives/rectangle.test.js +3 -0
- package/src/primitives/roundedCuboid.js +6 -6
- package/src/primitives/roundedCuboid.test.js +5 -0
- package/src/primitives/roundedCylinder.js +7 -5
- 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.js +1 -2
- 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 +34 -0
- package/src/utils/trigonometry.test.js +25 -0
- package/test/helpers/nearlyEqual.js +4 -1
- package/src/geometries/path2/eachPoint.d.ts +0 -9
- package/src/geometries/path2/eachPoint.js +0 -17
- package/src/geometries/path2/eachPoint.test.js +0 -11
- package/src/operations/modifiers/edges.js +0 -195
- package/src/operations/modifiers/repairTjunctions.js +0 -44
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jscad/modeling",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.3",
|
|
4
4
|
"description": "Constructive Solid Geometry (CSG) Library for JSCAD",
|
|
5
|
+
"homepage": "https://openjscad.xyz/",
|
|
5
6
|
"repository": "https://github.com/jscad/OpenJSCAD.org",
|
|
6
7
|
"main": "src/index.js",
|
|
7
8
|
"types": "src/index.d.ts",
|
|
@@ -60,5 +61,5 @@
|
|
|
60
61
|
"nyc": "15.1.0",
|
|
61
62
|
"uglifyify": "5.0.2"
|
|
62
63
|
},
|
|
63
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "85fa1fcdfb2d516a201ecf31da242e64b5b3274e"
|
|
64
65
|
}
|
|
@@ -38,7 +38,7 @@ test('color (rgba on geometry)', (t) => {
|
|
|
38
38
|
const obj0 = geom2.fromPoints([[0, 0], [1, 0], [0, 1]])
|
|
39
39
|
const obj1 = geom3.fromPoints([[[0, 0, 0], [1, 0, 0], [1, 0, 1]]])
|
|
40
40
|
const obj2 = path2.fromPoints({ closed: true }, [[0, 0], [1, 0], [1, 1]])
|
|
41
|
-
const obj3 = poly3.
|
|
41
|
+
const obj3 = poly3.create([[0, 0, 0], [1, 0, 0], [1, 1, 0]])
|
|
42
42
|
|
|
43
43
|
const obs = colorize([1, 1, 0.5, 0.8], obj0, obj1, obj2, obj3)
|
|
44
44
|
t.is(obs.length, 4)
|
|
@@ -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
|
|
@@ -6,22 +6,42 @@ const toSides = require('./toSides')
|
|
|
6
6
|
* Create a list of edges which SHARE vertices.
|
|
7
7
|
* This allows the edges to be traversed in order.
|
|
8
8
|
*/
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const toSharedVertices = (sides) => {
|
|
10
|
+
const unique = new Map() // {key: vertex}
|
|
11
11
|
const getUniqueVertex = (vertex) => {
|
|
12
12
|
const key = vertex.toString()
|
|
13
|
-
if (
|
|
14
|
-
|
|
13
|
+
if (unique.has(key)) {
|
|
14
|
+
return unique.get(key)
|
|
15
|
+
} else {
|
|
16
|
+
unique.set(key, vertex)
|
|
17
|
+
return vertex
|
|
15
18
|
}
|
|
16
|
-
return vertices[key]
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
return sides.map((side) => side.map(getUniqueVertex))
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/*
|
|
25
|
+
* Convert a list of sides into a map from vertex to edges.
|
|
26
|
+
*/
|
|
27
|
+
const toVertexMap = (sides) => {
|
|
28
|
+
const vertexMap = new Map()
|
|
29
|
+
// first map to edges with shared vertices
|
|
30
|
+
const edges = toSharedVertices(sides)
|
|
31
|
+
// construct adjacent edges map
|
|
32
|
+
edges.forEach((edge) => {
|
|
33
|
+
if (vertexMap.has(edge[0])) {
|
|
34
|
+
vertexMap.get(edge[0]).push(edge)
|
|
35
|
+
} else {
|
|
36
|
+
vertexMap.set(edge[0], [edge])
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
return vertexMap
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
/**
|
|
23
43
|
* Create the outline(s) of the given geometry.
|
|
24
|
-
* @param
|
|
44
|
+
* @param {geom2} geometry - geometry to create outlines from
|
|
25
45
|
* @returns {Array} an array of outlines, where each outline is an array of ordered points
|
|
26
46
|
* @alias module:modeling/geometries/geom2.toOutlines
|
|
27
47
|
*
|
|
@@ -30,65 +50,35 @@ const toEdges = (sides) => {
|
|
|
30
50
|
* let outlines = toOutlines(geometry) // returns two outlines
|
|
31
51
|
*/
|
|
32
52
|
const toOutlines = (geometry) => {
|
|
33
|
-
const vertexMap =
|
|
34
|
-
const edges = toEdges(toSides(geometry))
|
|
35
|
-
edges.forEach((edge) => {
|
|
36
|
-
if (!(vertexMap.has(edge[0]))) {
|
|
37
|
-
vertexMap.set(edge[0], [])
|
|
38
|
-
}
|
|
39
|
-
const sideslist = vertexMap.get(edge[0])
|
|
40
|
-
sideslist.push(edge)
|
|
41
|
-
})
|
|
42
|
-
|
|
53
|
+
const vertexMap = toVertexMap(toSides(geometry)) // {vertex: [edges]}
|
|
43
54
|
const outlines = []
|
|
44
55
|
while (true) {
|
|
45
|
-
let
|
|
56
|
+
let startSide
|
|
46
57
|
for (const [vertex, edges] of vertexMap) {
|
|
47
|
-
|
|
48
|
-
if (!
|
|
58
|
+
startSide = edges.shift()
|
|
59
|
+
if (!startSide) {
|
|
49
60
|
vertexMap.delete(vertex)
|
|
50
61
|
continue
|
|
51
62
|
}
|
|
52
63
|
break
|
|
53
64
|
}
|
|
54
|
-
if (
|
|
65
|
+
if (startSide === undefined) break // all starting sides have been visited
|
|
55
66
|
|
|
56
67
|
const connectedVertexPoints = []
|
|
57
|
-
const
|
|
58
|
-
const v0 = vec2.create()
|
|
68
|
+
const startVertex = startSide[0]
|
|
59
69
|
while (true) {
|
|
60
|
-
connectedVertexPoints.push(
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
65
|
-
throw new Error(
|
|
70
|
+
connectedVertexPoints.push(startSide[0])
|
|
71
|
+
const nextVertex = startSide[1]
|
|
72
|
+
if (nextVertex === startVertex) break // the outline has been closed
|
|
73
|
+
const nextPossibleSides = vertexMap.get(nextVertex)
|
|
74
|
+
if (!nextPossibleSides) {
|
|
75
|
+
throw new Error(`geometry is not closed at vertex ${nextVertex}`)
|
|
66
76
|
}
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
} else {
|
|
71
|
-
// more than one side starting at the same vertex
|
|
72
|
-
let bestangle
|
|
73
|
-
const startangle = vec2.angleDegrees(vec2.subtract(v0, startside[1], startside[0]))
|
|
74
|
-
for (let sideindex = 0; sideindex < nextpossiblesides.length; sideindex++) {
|
|
75
|
-
const nextpossibleside = nextpossiblesides[sideindex]
|
|
76
|
-
const nextangle = vec2.angleDegrees(vec2.subtract(v0, nextpossibleside[1], nextpossibleside[0]))
|
|
77
|
-
let angledif = nextangle - startangle
|
|
78
|
-
if (angledif < -180) angledif += 360
|
|
79
|
-
if (angledif >= 180) angledif -= 360
|
|
80
|
-
if ((nextsideindex < 0) || (angledif > bestangle)) {
|
|
81
|
-
nextsideindex = sideindex
|
|
82
|
-
bestangle = angledif
|
|
83
|
-
}
|
|
84
|
-
}
|
|
77
|
+
const nextSide = popNextSide(startSide, nextPossibleSides)
|
|
78
|
+
if (nextPossibleSides.length === 0) {
|
|
79
|
+
vertexMap.delete(nextVertex)
|
|
85
80
|
}
|
|
86
|
-
|
|
87
|
-
nextpossiblesides.splice(nextsideindex, 1) // remove side from list
|
|
88
|
-
if (nextpossiblesides.length === 0) {
|
|
89
|
-
vertexMap.delete(nextvertex)
|
|
90
|
-
}
|
|
91
|
-
startside = nextside
|
|
81
|
+
startSide = nextSide
|
|
92
82
|
} // inner loop
|
|
93
83
|
|
|
94
84
|
// due to the logic of fromPoints()
|
|
@@ -102,4 +92,28 @@ const toOutlines = (geometry) => {
|
|
|
102
92
|
return outlines
|
|
103
93
|
}
|
|
104
94
|
|
|
95
|
+
// find the first counter-clockwise edge from startSide and pop from nextSides
|
|
96
|
+
const popNextSide = (startSide, nextSides) => {
|
|
97
|
+
if (nextSides.length === 1) {
|
|
98
|
+
return nextSides.pop()
|
|
99
|
+
}
|
|
100
|
+
const v0 = vec2.create()
|
|
101
|
+
const startAngle = vec2.angleDegrees(vec2.subtract(v0, startSide[1], startSide[0]))
|
|
102
|
+
let bestAngle
|
|
103
|
+
let bestIndex
|
|
104
|
+
nextSides.forEach((nextSide, index) => {
|
|
105
|
+
const nextAngle = vec2.angleDegrees(vec2.subtract(v0, nextSide[1], nextSide[0]))
|
|
106
|
+
let angle = nextAngle - startAngle
|
|
107
|
+
if (angle < -180) angle += 360
|
|
108
|
+
if (angle >= 180) angle -= 360
|
|
109
|
+
if (bestIndex === undefined || angle > bestAngle) {
|
|
110
|
+
bestIndex = index
|
|
111
|
+
bestAngle = angle
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
const nextSide = nextSides[bestIndex]
|
|
115
|
+
nextSides.splice(bestIndex, 1) // remove side from list
|
|
116
|
+
return nextSide
|
|
117
|
+
}
|
|
118
|
+
|
|
105
119
|
module.exports = toOutlines
|
|
@@ -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
|
|
@@ -14,7 +14,7 @@ test('create: Creates an empty geom3', (t) => {
|
|
|
14
14
|
|
|
15
15
|
test('create: Creates a populated geom3', (t) => {
|
|
16
16
|
const points = [[0, 0, 0], [0, 10, 0], [0, 10, 10]]
|
|
17
|
-
const polygon = poly3.
|
|
17
|
+
const polygon = poly3.create(points)
|
|
18
18
|
|
|
19
19
|
const polygons = [polygon]
|
|
20
20
|
const expected = {
|
|
@@ -18,7 +18,7 @@ const fromPoints = (listofpoints) => {
|
|
|
18
18
|
|
|
19
19
|
const polygons = listofpoints.map((points, index) => {
|
|
20
20
|
// TODO catch the error, and rethrow with index
|
|
21
|
-
const polygon = poly3.
|
|
21
|
+
const polygon = poly3.create(points)
|
|
22
22
|
return polygon
|
|
23
23
|
})
|
|
24
24
|
const result = create(polygons)
|
|
@@ -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
|
|
@@ -5,7 +5,6 @@ export { default as clone } from './clone'
|
|
|
5
5
|
export { default as close } from './close'
|
|
6
6
|
export { default as concat } from './concat'
|
|
7
7
|
export { default as create } from './create'
|
|
8
|
-
export { default as eachPoint, EachPointOptions } from './eachPoint'
|
|
9
8
|
export { default as equals } from './equals'
|
|
10
9
|
export { default as fromPoints, FromPointsOptions } from './fromPoints'
|
|
11
10
|
export { default as fromCompactBinary } from './fromCompactBinary'
|
|
@@ -15,6 +14,7 @@ export { default as toPoints } from './toPoints'
|
|
|
15
14
|
export { default as toString } from './toString'
|
|
16
15
|
export { default as toCompactBinary } from './toCompactBinary'
|
|
17
16
|
export { default as transform } from './transform'
|
|
17
|
+
export { default as validate } from './validate'
|
|
18
18
|
|
|
19
19
|
export { default as Path2 } from './type'
|
|
20
20
|
export as namespace path2
|
|
@@ -22,7 +22,6 @@ module.exports = {
|
|
|
22
22
|
close: require('./close'),
|
|
23
23
|
concat: require('./concat'),
|
|
24
24
|
create: require('./create'),
|
|
25
|
-
eachPoint: require('./eachPoint'),
|
|
26
25
|
equals: require('./equals'),
|
|
27
26
|
fromPoints: require('./fromPoints'),
|
|
28
27
|
fromCompactBinary: require('./fromCompactBinary'),
|
|
@@ -31,5 +30,6 @@ module.exports = {
|
|
|
31
30
|
toPoints: require('./toPoints'),
|
|
32
31
|
toString: require('./toString'),
|
|
33
32
|
toCompactBinary: require('./toCompactBinary'),
|
|
34
|
-
transform: require('./transform')
|
|
33
|
+
transform: require('./transform'),
|
|
34
|
+
validate: require('./validate')
|
|
35
35
|
}
|
|
@@ -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
|
|
@@ -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
|
})
|