@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
|
@@ -42,28 +42,32 @@ class Node {
|
|
|
42
42
|
node = current.node
|
|
43
43
|
polygontreenodes = current.polygontreenodes
|
|
44
44
|
|
|
45
|
-
// begin "function"
|
|
46
45
|
if (node.plane) {
|
|
46
|
+
const plane = node.plane
|
|
47
|
+
|
|
47
48
|
const backnodes = []
|
|
48
49
|
const frontnodes = []
|
|
49
50
|
const coplanarfrontnodes = alsoRemovecoplanarFront ? backnodes : frontnodes
|
|
50
|
-
const plane = node.plane
|
|
51
51
|
const numpolygontreenodes = polygontreenodes.length
|
|
52
52
|
for (let i = 0; i < numpolygontreenodes; i++) {
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
55
|
-
|
|
53
|
+
const treenode = polygontreenodes[i]
|
|
54
|
+
if (!treenode.isRemoved()) {
|
|
55
|
+
// split this polygon tree node using the plane
|
|
56
|
+
// NOTE: children are added to the tree if there are spanning polygons
|
|
57
|
+
treenode.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes)
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
if (node.front && (frontnodes.length > 0)) {
|
|
62
|
+
// add front node for further splitting
|
|
60
63
|
stack.push({ node: node.front, polygontreenodes: frontnodes })
|
|
61
64
|
}
|
|
62
65
|
const numbacknodes = backnodes.length
|
|
63
66
|
if (node.back && (numbacknodes > 0)) {
|
|
67
|
+
// add back node for further splitting
|
|
64
68
|
stack.push({ node: node.back, polygontreenodes: backnodes })
|
|
65
69
|
} else {
|
|
66
|
-
//
|
|
70
|
+
// remove all back nodes from processing
|
|
67
71
|
for (let i = 0; i < numbacknodes; i++) {
|
|
68
72
|
backnodes[i].remove()
|
|
69
73
|
}
|
|
@@ -135,16 +139,6 @@ class Node {
|
|
|
135
139
|
current = stack.pop()
|
|
136
140
|
} while (current !== undefined)
|
|
137
141
|
}
|
|
138
|
-
|
|
139
|
-
// TODO is this still used?
|
|
140
|
-
getParentPlaneNormals (normals, maxdepth) {
|
|
141
|
-
if (maxdepth > 0) {
|
|
142
|
-
if (this.parent) {
|
|
143
|
-
normals.push(this.parent.plane.normal)
|
|
144
|
-
this.parent.getParentPlaneNormals(normals, maxdepth - 1)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
142
|
}
|
|
149
143
|
|
|
150
144
|
module.exports = Node
|
|
@@ -21,11 +21,11 @@ const splitPolygonByPlane = require('./splitPolygonByPlane')
|
|
|
21
21
|
// since they are no longer intact.
|
|
22
22
|
class PolygonTreeNode {
|
|
23
23
|
// constructor creates the root node
|
|
24
|
-
constructor () {
|
|
25
|
-
this.parent =
|
|
24
|
+
constructor (parent, polygon) {
|
|
25
|
+
this.parent = parent
|
|
26
26
|
this.children = []
|
|
27
|
-
this.polygon =
|
|
28
|
-
this.removed = false
|
|
27
|
+
this.polygon = polygon
|
|
28
|
+
this.removed = false // state of branch or leaf
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// fill the tree with polygons. Should be called on the root node only; child nodes must
|
|
@@ -47,6 +47,7 @@ class PolygonTreeNode {
|
|
|
47
47
|
remove () {
|
|
48
48
|
if (!this.removed) {
|
|
49
49
|
this.removed = true
|
|
50
|
+
this.polygon = null
|
|
50
51
|
|
|
51
52
|
// remove ourselves from the parent's children list:
|
|
52
53
|
const parentschildren = this.parent.children
|
|
@@ -183,9 +184,7 @@ class PolygonTreeNode {
|
|
|
183
184
|
// a child should be created for every fragment of the split polygon
|
|
184
185
|
// returns the newly created child
|
|
185
186
|
addChild (polygon) {
|
|
186
|
-
const newchild = new PolygonTreeNode()
|
|
187
|
-
newchild.parent = this
|
|
188
|
-
newchild.polygon = polygon
|
|
187
|
+
const newchild = new PolygonTreeNode(this, polygon)
|
|
189
188
|
this.children.push(newchild)
|
|
190
189
|
return newchild
|
|
191
190
|
}
|
|
@@ -206,13 +205,13 @@ class PolygonTreeNode {
|
|
|
206
205
|
}
|
|
207
206
|
}
|
|
208
207
|
|
|
208
|
+
// private method
|
|
209
|
+
// remove the polygon from the node, and all parent nodes above it
|
|
210
|
+
// called to invalidate parents of removed nodes
|
|
209
211
|
recursivelyInvalidatePolygon () {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (node.parent) {
|
|
214
|
-
node = node.parent
|
|
215
|
-
}
|
|
212
|
+
this.polygon = null
|
|
213
|
+
if (this.parent) {
|
|
214
|
+
this.parent.recursivelyInvalidatePolygon()
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
217
|
|
|
@@ -248,7 +247,7 @@ class PolygonTreeNode {
|
|
|
248
247
|
node = children[j]
|
|
249
248
|
result += `${prefix}PolygonTreeNode (${node.isRootNode()}): ${node.children.length}`
|
|
250
249
|
if (node.polygon) {
|
|
251
|
-
result += `\n ${prefix}
|
|
250
|
+
result += `\n ${prefix}polygon: ${node.polygon.vertices}\n`
|
|
252
251
|
} else {
|
|
253
252
|
result += '\n'
|
|
254
253
|
}
|
|
@@ -19,8 +19,7 @@ class Tree {
|
|
|
19
19
|
|
|
20
20
|
// Remove all polygons in this BSP tree that are inside the other BSP tree
|
|
21
21
|
// `tree`.
|
|
22
|
-
clipTo (tree, alsoRemovecoplanarFront) {
|
|
23
|
-
alsoRemovecoplanarFront = !!alsoRemovecoplanarFront
|
|
22
|
+
clipTo (tree, alsoRemovecoplanarFront = false) {
|
|
24
23
|
this.rootnode.clipTo(tree, alsoRemovecoplanarFront)
|
|
25
24
|
}
|
|
26
25
|
|
|
@@ -36,7 +36,7 @@ const splitPolygonByPlane = (splane, polygon) => {
|
|
|
36
36
|
const MINEPS = -EPS
|
|
37
37
|
for (let i = 0; i < numvertices; i++) {
|
|
38
38
|
const t = vec3.dot(splane, vertices[i]) - splane[3]
|
|
39
|
-
const isback = (t <
|
|
39
|
+
const isback = (t < MINEPS)
|
|
40
40
|
vertexIsBack.push(isback)
|
|
41
41
|
if (t > EPS) hasfront = true
|
|
42
42
|
if (t < MINEPS) hasback = true
|
|
@@ -69,9 +69,8 @@ const splitPolygonByPlane = (splane, polygon) => {
|
|
|
69
69
|
}
|
|
70
70
|
} else {
|
|
71
71
|
// line segment intersects plane:
|
|
72
|
-
const point = vertex
|
|
73
72
|
const nextpoint = vertices[nextvertexindex]
|
|
74
|
-
const intersectionpoint = splitLineSegmentByPlane(splane,
|
|
73
|
+
const intersectionpoint = splitLineSegmentByPlane(splane, vertex, nextpoint)
|
|
75
74
|
if (isback) {
|
|
76
75
|
backvertices.push(vertex)
|
|
77
76
|
backvertices.push(intersectionpoint)
|
|
@@ -9,6 +9,7 @@ const { circle, rectangle, sphere, cuboid } = require('../../primitives')
|
|
|
9
9
|
const { union } = require('./index')
|
|
10
10
|
|
|
11
11
|
const { center } = require('../transforms/center')
|
|
12
|
+
const { translate } = require('../transforms/translate')
|
|
12
13
|
|
|
13
14
|
// test('union: union of a path produces expected changes to points', (t) => {
|
|
14
15
|
// let geometry = path.fromPoints({}, [[0, 1, 0], [1, 0, 0]])
|
|
@@ -36,6 +37,7 @@ test('union of one or more geom2 objects produces expected geometry', (t) => {
|
|
|
36
37
|
[0, -2],
|
|
37
38
|
[1.4142000000000001, -1.4142000000000001]
|
|
38
39
|
]
|
|
40
|
+
t.notThrows(() => geom2.validate(result1))
|
|
39
41
|
t.true(comparePoints(obs, exp))
|
|
40
42
|
|
|
41
43
|
// union of two non-overlapping objects
|
|
@@ -57,6 +59,7 @@ test('union of one or more geom2 objects produces expected geometry', (t) => {
|
|
|
57
59
|
[12, 12],
|
|
58
60
|
[1.4142000000000001, -1.4142000000000001]
|
|
59
61
|
]
|
|
62
|
+
t.notThrows(() => geom2.validate(result2))
|
|
60
63
|
t.true(comparePoints(obs, exp))
|
|
61
64
|
|
|
62
65
|
// union of two partially overlapping objects
|
|
@@ -74,6 +77,7 @@ test('union of one or more geom2 objects produces expected geometry', (t) => {
|
|
|
74
77
|
[7.999933333333333, 9.000053333333334],
|
|
75
78
|
[11.999973333333333, 7.999933333333333]
|
|
76
79
|
]
|
|
80
|
+
t.notThrows(() => geom2.validate(result3))
|
|
77
81
|
t.true(comparePoints(obs, exp))
|
|
78
82
|
|
|
79
83
|
// union of two completely overlapping objects
|
|
@@ -85,7 +89,24 @@ test('union of one or more geom2 objects produces expected geometry', (t) => {
|
|
|
85
89
|
[9.000046666666666, 9.000046666666666],
|
|
86
90
|
[-9.000046666666666, 9.000046666666666]
|
|
87
91
|
]
|
|
92
|
+
t.notThrows(() => geom2.validate(result4))
|
|
88
93
|
t.true(comparePoints(obs, exp))
|
|
94
|
+
|
|
95
|
+
// union of unions of non-overlapping objects (BSP gap from #907)
|
|
96
|
+
const circ = circle({ radius: 1, segments: 32 })
|
|
97
|
+
const result5 = union(
|
|
98
|
+
union(
|
|
99
|
+
translate([17, 21], circ),
|
|
100
|
+
translate([7, 0], circ),
|
|
101
|
+
),
|
|
102
|
+
union(
|
|
103
|
+
translate([3, 21], circ),
|
|
104
|
+
translate([17, 21], circ),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
obs = geom2.toPoints(result5)
|
|
108
|
+
t.notThrows.skip(() => geom2.validate(result5))
|
|
109
|
+
t.is(obs.length, 112)
|
|
89
110
|
})
|
|
90
111
|
|
|
91
112
|
test('union of one or more geom3 objects produces expected geometry', (t) => {
|
|
@@ -144,6 +165,7 @@ test('union of one or more geom3 objects produces expected geometry', (t) => {
|
|
|
144
165
|
[[0.9999999999999998, 1.0000000000000002, -1.414213562373095], [1.4142135623730951, 3.4638242249419736e-16, -1.414213562373095], [8.65956056235493e-17, 8.659560562354935e-17, -2]],
|
|
145
166
|
[[8.65956056235493e-17, 8.659560562354935e-17, 2], [1.4142135623730951, 3.4638242249419736e-16, 1.414213562373095], [0.9999999999999998, 1.0000000000000002, 1.414213562373095]]
|
|
146
167
|
]
|
|
168
|
+
t.notThrows.skip(() => geom3.validate(result1))
|
|
147
169
|
t.true(comparePolygonsAsPoints(obs, exp))
|
|
148
170
|
|
|
149
171
|
// union of two non-overlapping objects
|
|
@@ -151,6 +173,7 @@ test('union of one or more geom3 objects produces expected geometry', (t) => {
|
|
|
151
173
|
|
|
152
174
|
const result2 = union(geometry1, geometry2)
|
|
153
175
|
obs = geom3.toPoints(result2)
|
|
176
|
+
t.notThrows.skip(() => geom3.validate(result2))
|
|
154
177
|
t.is(obs.length, 38)
|
|
155
178
|
|
|
156
179
|
// union of two partially overlapping objects
|
|
@@ -178,6 +201,7 @@ test('union of one or more geom3 objects produces expected geometry', (t) => {
|
|
|
178
201
|
[[-9, 9, 9], [-9, 8, 9], [8, 8, 9], [8, 9, 9]],
|
|
179
202
|
[[-9, 8, 9], [-9, -9, 9], [9, -9, 9], [9, 8, 9]]
|
|
180
203
|
]
|
|
204
|
+
t.notThrows.skip(() => geom3.validate(result3))
|
|
181
205
|
t.is(obs.length, 18)
|
|
182
206
|
t.true(comparePolygonsAsPoints(obs, exp))
|
|
183
207
|
|
|
@@ -192,6 +216,7 @@ test('union of one or more geom3 objects produces expected geometry', (t) => {
|
|
|
192
216
|
[[-9, -9, -9], [-9, 9, -9], [9, 9, -9], [9, -9, -9]],
|
|
193
217
|
[[-9, -9, 9], [9, -9, 9], [9, 9, 9], [-9, 9, 9]]
|
|
194
218
|
]
|
|
219
|
+
t.notThrows(() => geom3.validate(result4))
|
|
195
220
|
t.is(obs.length, 6)
|
|
196
221
|
t.true(comparePolygonsAsPoints(obs, exp))
|
|
197
222
|
})
|
|
@@ -202,6 +227,7 @@ test('union of geom3 with rounding issues #137', (t) => {
|
|
|
202
227
|
|
|
203
228
|
const obs = union(geometry1, geometry2)
|
|
204
229
|
const pts = geom3.toPoints(obs)
|
|
230
|
+
t.notThrows(() => geom3.validate(obs))
|
|
205
231
|
t.is(pts.length, 6) // number of polygons in union
|
|
206
232
|
})
|
|
207
233
|
|
|
@@ -266,6 +292,7 @@ test('union of geom2 with closing issues #15', (t) => {
|
|
|
266
292
|
[-49.34040695243976, -15.797284338334542],
|
|
267
293
|
[-45.82121705016925, -16.857333163105647]
|
|
268
294
|
]
|
|
295
|
+
t.notThrows(() => geom2.validate(obs))
|
|
269
296
|
t.is(pts.length, 20) // number of sides in union
|
|
270
297
|
t.true(comparePoints(pts, exp))
|
|
271
298
|
})
|
|
@@ -14,6 +14,7 @@ test('expand: edge-expanding a straight line produces rectangle', (t) => {
|
|
|
14
14
|
const expandedPathGeom2 = expand({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
|
|
15
15
|
const expandedPoints = geom2.toPoints(expandedPathGeom2)
|
|
16
16
|
|
|
17
|
+
t.notThrows(() => geom2.validate(expandedPathGeom2))
|
|
17
18
|
t.is(area(expandedPoints), 40)
|
|
18
19
|
t.true(comparePoints(measureBoundingBox(expandedPathGeom2), [[-2, 0, 0], [2, 10, 0]]))
|
|
19
20
|
})
|
|
@@ -24,6 +25,7 @@ test('expand: edge-expanding a bent line produces expected geometry', (t) => {
|
|
|
24
25
|
const expandedPathGeom2 = expand({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
|
|
25
26
|
const expandedPoints = geom2.toPoints(expandedPathGeom2)
|
|
26
27
|
|
|
28
|
+
t.notThrows(() => geom2.validate(expandedPathGeom2))
|
|
27
29
|
t.is(area(expandedPoints), 60)
|
|
28
30
|
const boundingBox = measureBoundingBox(expandedPathGeom2)
|
|
29
31
|
t.true(comparePoints(boundingBox, [[-5, 0, 0], [2, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -35,6 +37,7 @@ test('expand: edge-expanding a bent line, reversed points, produces expected geo
|
|
|
35
37
|
const expandedPathGeom2 = expand({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
|
|
36
38
|
const expandedPoints = geom2.toPoints(expandedPathGeom2)
|
|
37
39
|
|
|
40
|
+
t.notThrows(() => geom2.validate(expandedPathGeom2))
|
|
38
41
|
t.is(area(expandedPoints), 60)
|
|
39
42
|
const boundingBox = measureBoundingBox(expandedPathGeom2)
|
|
40
43
|
t.true(comparePoints(boundingBox, [[-5, 0, 0], [2, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -47,6 +50,7 @@ test('expand: round-expanding a bent line produces expected geometry', (t) => {
|
|
|
47
50
|
const expandedPathGeom2 = expand({ delta, corners: 'round', segments: 128 }, linePath2)
|
|
48
51
|
const expandedPoints = geom2.toPoints(expandedPathGeom2)
|
|
49
52
|
|
|
53
|
+
t.notThrows(() => geom2.validate(expandedPathGeom2))
|
|
50
54
|
const expectedArea = 56 + 2 * Math.PI * delta * 1.25 // shape will have 1 and 1/4 circles
|
|
51
55
|
nearlyEqual(t, area(expandedPoints), expectedArea, 0.01, 'Measured area should be pretty close')
|
|
52
56
|
const boundingBox = measureBoundingBox(expandedPathGeom2)
|
|
@@ -60,6 +64,7 @@ test('expand: chamfer-expanding a bent line produces expected geometry', (t) =>
|
|
|
60
64
|
const expandedPathGeom2 = expand({ delta, corners: 'chamfer', segments: 8 }, linePath2)
|
|
61
65
|
const expandedPoints = geom2.toPoints(expandedPathGeom2)
|
|
62
66
|
|
|
67
|
+
t.notThrows(() => geom2.validate(expandedPathGeom2))
|
|
63
68
|
t.is(area(expandedPoints), 58)
|
|
64
69
|
const boundingBox = measureBoundingBox(expandedPathGeom2)
|
|
65
70
|
t.true(comparePoints(boundingBox, [[-5, 0, 0], [2, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -84,6 +89,7 @@ test('expand: expanding of a geom2 produces expected changes to points', (t) =>
|
|
|
84
89
|
[-10, 8],
|
|
85
90
|
[-10, -8]
|
|
86
91
|
]
|
|
92
|
+
t.notThrows(() => geom2.validate(obs))
|
|
87
93
|
t.is(pts.length, 12)
|
|
88
94
|
t.true(comparePoints(pts, exp))
|
|
89
95
|
})
|
|
@@ -113,6 +119,7 @@ test('expand: expanding of a geom3 produces expected changes to polygons', (t) =
|
|
|
113
119
|
[16, -6.414213562373095, 16]
|
|
114
120
|
]
|
|
115
121
|
|
|
122
|
+
t.notThrows.skip(() => geom3.validate(obs))
|
|
116
123
|
t.is(pts.length, 62)
|
|
117
124
|
t.true(comparePoints(pts[0], exp0))
|
|
118
125
|
t.true(comparePoints(pts[61], exp61))
|
|
@@ -120,31 +127,32 @@ test('expand: expanding of a geom3 produces expected changes to polygons', (t) =
|
|
|
120
127
|
const geometry2 = sphere({ radius: 5, segments: 8 })
|
|
121
128
|
const obs2 = expand({ delta: 5 }, geometry2)
|
|
122
129
|
const pts2 = geom3.toPoints(obs2)
|
|
123
|
-
t.
|
|
130
|
+
t.notThrows.skip(() => geom3.validate(obs2))
|
|
131
|
+
t.is(pts2.length, 864)
|
|
124
132
|
})
|
|
125
133
|
|
|
126
134
|
test('expand (options): offsetting of a complex geom2 produces expected offset geom2', (t) => {
|
|
127
135
|
const geometry = geom2.create([
|
|
128
|
-
[[-75
|
|
129
|
-
[[-75
|
|
130
|
-
[[75
|
|
131
|
-
[[-40
|
|
132
|
-
[[75
|
|
133
|
-
[[40
|
|
134
|
-
[[40
|
|
135
|
-
[[-40
|
|
136
|
-
[[15
|
|
137
|
-
[[-15
|
|
138
|
-
[[-15
|
|
139
|
-
[[-8
|
|
140
|
-
[[15
|
|
141
|
-
[[-8
|
|
142
|
-
[[8
|
|
143
|
-
[[8
|
|
144
|
-
[[-2
|
|
145
|
-
[[-2
|
|
146
|
-
[[2
|
|
147
|
-
[[2
|
|
136
|
+
[[-75, 75], [-75, -75]],
|
|
137
|
+
[[-75, -75], [75, -75]],
|
|
138
|
+
[[75, -75], [75, 75]],
|
|
139
|
+
[[-40, 75], [-75, 75]],
|
|
140
|
+
[[75, 75], [40, 75]],
|
|
141
|
+
[[40, 75], [40, 0]],
|
|
142
|
+
[[40, 0], [-40, 0]],
|
|
143
|
+
[[-40, 0], [-40, 75]],
|
|
144
|
+
[[15, -10], [15, -40]],
|
|
145
|
+
[[-15, -10], [15, -10]],
|
|
146
|
+
[[-15, -40], [-15, -10]],
|
|
147
|
+
[[-8, -40], [-15, -40]],
|
|
148
|
+
[[15, -40], [8, -40]],
|
|
149
|
+
[[-8, -25], [-8, -40]],
|
|
150
|
+
[[8, -25], [-8, -25]],
|
|
151
|
+
[[8, -40], [8, -25]],
|
|
152
|
+
[[-2, -15], [-2, -19]],
|
|
153
|
+
[[-2, -19], [2, -19]],
|
|
154
|
+
[[2, -19], [2, -15]],
|
|
155
|
+
[[2, -15], [-2, -15]]
|
|
148
156
|
])
|
|
149
157
|
|
|
150
158
|
// expand +
|
|
@@ -172,6 +180,7 @@ test('expand (options): offsetting of a complex geom2 produces expected offset g
|
|
|
172
180
|
[-4, -13],
|
|
173
181
|
[-77, -77]
|
|
174
182
|
]
|
|
183
|
+
t.notThrows(() => geom2.validate(obs))
|
|
175
184
|
t.is(pts.length, 20)
|
|
176
185
|
t.true(comparePoints(pts, exp))
|
|
177
186
|
})
|
|
@@ -15,7 +15,7 @@ const unionGeom3Sub = require('../booleans/unionGeom3Sub')
|
|
|
15
15
|
|
|
16
16
|
const extrudePolygon = require('./extrudePolygon')
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/*
|
|
19
19
|
* Collect all planes adjacent to each vertex
|
|
20
20
|
*/
|
|
21
21
|
const mapPlaneToVertex = (map, vertex, plane) => {
|
|
@@ -29,7 +29,7 @@ const mapPlaneToVertex = (map, vertex, plane) => {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/*
|
|
33
33
|
* Collect all planes adjacent to each edge.
|
|
34
34
|
* Combine undirected edges, no need for duplicate cylinders.
|
|
35
35
|
*/
|
|
@@ -12,6 +12,7 @@ test('offset: offsetting a straight line produces expected geometry', (t) => {
|
|
|
12
12
|
// offset it by 2.
|
|
13
13
|
let offsetLinePath2 = offset({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
|
|
14
14
|
let offsetPoints = path2.toPoints(offsetLinePath2)
|
|
15
|
+
t.notThrows(() => path2.validate(offsetLinePath2))
|
|
15
16
|
t.is(offsetPoints.length, 2)
|
|
16
17
|
let boundingBox = measureBoundingBox(offsetLinePath2)
|
|
17
18
|
t.true(comparePoints(boundingBox, [[2, 0, 0], [2, 10, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -19,6 +20,7 @@ test('offset: offsetting a straight line produces expected geometry', (t) => {
|
|
|
19
20
|
// offset it by -2.
|
|
20
21
|
offsetLinePath2 = offset({ delta: -2, corners: 'edge', segments: 8 }, linePath2)
|
|
21
22
|
offsetPoints = path2.toPoints(offsetLinePath2)
|
|
23
|
+
t.notThrows(() => path2.validate(offsetLinePath2))
|
|
22
24
|
t.is(offsetPoints.length, 2)
|
|
23
25
|
boundingBox = measureBoundingBox(offsetLinePath2)
|
|
24
26
|
t.true(comparePoints(boundingBox, [[-2, 0, 0], [-2, 10, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -27,6 +29,7 @@ test('offset: offsetting a straight line produces expected geometry', (t) => {
|
|
|
27
29
|
linePath2 = path2.fromPoints({ closed: false }, points.reverse())
|
|
28
30
|
offsetLinePath2 = offset({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
|
|
29
31
|
offsetPoints = path2.toPoints(offsetLinePath2)
|
|
32
|
+
t.notThrows(() => path2.validate(offsetLinePath2))
|
|
30
33
|
t.is(offsetPoints.length, 2)
|
|
31
34
|
boundingBox = measureBoundingBox(offsetLinePath2)
|
|
32
35
|
t.true(comparePoints(boundingBox, [[-2, 0, 0], [-2, 10, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -39,6 +42,7 @@ test('offset: offsetting a bent line produces expected geometry', (t) => {
|
|
|
39
42
|
// offset it by 2.
|
|
40
43
|
let offsetLinePath2 = offset({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
|
|
41
44
|
let offsetPoints = path2.toPoints(offsetLinePath2)
|
|
45
|
+
t.notThrows(() => path2.validate(offsetLinePath2))
|
|
42
46
|
t.is(offsetPoints.length, 5)
|
|
43
47
|
let boundingBox = measureBoundingBox(offsetLinePath2)
|
|
44
48
|
t.true(comparePoints(boundingBox, [[2, 0, 0], [10, 8, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -46,6 +50,7 @@ test('offset: offsetting a bent line produces expected geometry', (t) => {
|
|
|
46
50
|
// offset it by -2.
|
|
47
51
|
offsetLinePath2 = offset({ delta: -2, corners: 'edge', segments: 8 }, linePath2)
|
|
48
52
|
offsetPoints = path2.toPoints(offsetLinePath2)
|
|
53
|
+
t.notThrows(() => path2.validate(offsetLinePath2))
|
|
49
54
|
t.is(offsetPoints.length, 5)
|
|
50
55
|
boundingBox = measureBoundingBox(offsetLinePath2)
|
|
51
56
|
t.true(comparePoints(boundingBox, [[-2, 0, 0], [10, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -56,6 +61,7 @@ test('offset: offsetting a 2 segment straight line produces expected geometry',
|
|
|
56
61
|
const linePath2 = path2.fromPoints({ closed: false }, points)
|
|
57
62
|
const offsetLinePath2 = offset({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
|
|
58
63
|
const offsetPoints = path2.toPoints(offsetLinePath2)
|
|
64
|
+
t.notThrows(() => path2.validate(offsetLinePath2))
|
|
59
65
|
t.is(offsetPoints.length, 3)
|
|
60
66
|
const boundingBox = measureBoundingBox(offsetLinePath2)
|
|
61
67
|
t.true(comparePoints(boundingBox, [[2, 0, 0], [2, 10, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -71,6 +77,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
|
|
|
71
77
|
let pts = path2.toPoints(obs)
|
|
72
78
|
let exp = [
|
|
73
79
|
]
|
|
80
|
+
t.notThrows(() => path2.validate(obs))
|
|
74
81
|
t.true(comparePoints(pts, exp))
|
|
75
82
|
|
|
76
83
|
// expand +
|
|
@@ -82,6 +89,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
|
|
|
82
89
|
[5.707106781186548, 0.7071067811865475],
|
|
83
90
|
[0.7071067811865475, 5.707106781186548]
|
|
84
91
|
]
|
|
92
|
+
t.notThrows(() => path2.validate(obs))
|
|
85
93
|
t.true(comparePoints(pts, exp))
|
|
86
94
|
|
|
87
95
|
obs = offset({ delta: 1, corners: 'chamfer' }, closeline)
|
|
@@ -94,6 +102,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
|
|
|
94
102
|
[-1, 5],
|
|
95
103
|
[-1, 6.123233995736766e-17]
|
|
96
104
|
]
|
|
105
|
+
t.notThrows(() => path2.validate(obs))
|
|
97
106
|
t.true(comparePoints(pts, exp))
|
|
98
107
|
|
|
99
108
|
// contract -
|
|
@@ -104,6 +113,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
|
|
|
104
113
|
[2.5857864376269046, 1],
|
|
105
114
|
[-0.7071067811865475, 4.292893218813452]
|
|
106
115
|
]
|
|
116
|
+
t.notThrows(() => path2.validate(obs))
|
|
107
117
|
t.true(comparePoints(pts, exp))
|
|
108
118
|
|
|
109
119
|
obs = offset({ delta: -1, corners: 'chamfer' }, closeline)
|
|
@@ -113,6 +123,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
|
|
|
113
123
|
[2.5857864376269046, 1],
|
|
114
124
|
[0.9999999999999996, 2.585786437626905]
|
|
115
125
|
]
|
|
126
|
+
t.notThrows(() => path2.validate(obs))
|
|
116
127
|
t.true(comparePoints(pts, exp))
|
|
117
128
|
})
|
|
118
129
|
|
|
@@ -132,6 +143,7 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
132
143
|
[-1.9999999999999996, 6],
|
|
133
144
|
[-5, 6]
|
|
134
145
|
]
|
|
146
|
+
t.notThrows(() => path2.validate(obs))
|
|
135
147
|
t.true(comparePoints(pts, exp))
|
|
136
148
|
|
|
137
149
|
obs = offset({ delta: 1, corners: 'edge' }, closeline)
|
|
@@ -146,6 +158,7 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
146
158
|
[-6, 6],
|
|
147
159
|
[-6, -6]
|
|
148
160
|
]
|
|
161
|
+
t.notThrows(() => path2.validate(obs))
|
|
149
162
|
t.true(comparePoints(pts, exp))
|
|
150
163
|
|
|
151
164
|
obs = offset({ delta: -0.5, corners: 'edge' }, openline)
|
|
@@ -160,6 +173,7 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
160
173
|
[-3.5, 4.5],
|
|
161
174
|
[-5, 4.5]
|
|
162
175
|
]
|
|
176
|
+
t.notThrows(() => path2.validate(obs))
|
|
163
177
|
t.true(comparePoints(pts, exp))
|
|
164
178
|
|
|
165
179
|
obs = offset({ delta: -0.5, corners: 'edge' }, closeline)
|
|
@@ -174,6 +188,7 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
174
188
|
[-3.5, 4.5],
|
|
175
189
|
[-4.5, 4.5]
|
|
176
190
|
]
|
|
191
|
+
t.notThrows(() => path2.validate(obs))
|
|
177
192
|
t.true(comparePoints(pts, exp))
|
|
178
193
|
})
|
|
179
194
|
|
|
@@ -209,6 +224,7 @@ test('offset (corners: round): offset of a path2 produces expected offset path2'
|
|
|
209
224
|
[-3, 6],
|
|
210
225
|
[-5, 6]
|
|
211
226
|
]
|
|
227
|
+
t.notThrows(() => path2.validate(obs))
|
|
212
228
|
t.true(comparePoints(pts, exp))
|
|
213
229
|
|
|
214
230
|
obs = offset({ delta: 1, corners: 'round', segments: 16 }, closeline)
|
|
@@ -247,6 +263,7 @@ test('offset (corners: round): offset of a path2 produces expected offset path2'
|
|
|
247
263
|
[-6, 5],
|
|
248
264
|
[-6, -5]
|
|
249
265
|
]
|
|
266
|
+
t.notThrows(() => path2.validate(obs))
|
|
250
267
|
t.true(comparePoints(pts, exp))
|
|
251
268
|
})
|
|
252
269
|
|
|
@@ -289,6 +306,7 @@ test('offset (corners: round): offset of a CW path2 produces expected offset pat
|
|
|
289
306
|
[5, -6],
|
|
290
307
|
[-5, -6]
|
|
291
308
|
]
|
|
309
|
+
t.notThrows(() => path2.validate(obs))
|
|
292
310
|
t.true(comparePoints(pts, exp))
|
|
293
311
|
})
|
|
294
312
|
|
|
@@ -301,6 +319,7 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
|
|
|
301
319
|
let pts = geom2.toPoints(obs)
|
|
302
320
|
let exp = [
|
|
303
321
|
]
|
|
322
|
+
t.notThrows(() => geom2.validate(obs))
|
|
304
323
|
t.true(comparePoints(pts, exp))
|
|
305
324
|
|
|
306
325
|
// expand +
|
|
@@ -322,6 +341,7 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
|
|
|
322
341
|
[-6, 5],
|
|
323
342
|
[-6, -5]
|
|
324
343
|
]
|
|
344
|
+
t.notThrows(() => geom2.validate(obs))
|
|
325
345
|
t.true(comparePoints(pts, exp))
|
|
326
346
|
|
|
327
347
|
// contract -
|
|
@@ -339,6 +359,7 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
|
|
|
339
359
|
[-3.5, 4.5],
|
|
340
360
|
[-4.5, 4.5]
|
|
341
361
|
]
|
|
362
|
+
t.notThrows(() => geom2.validate(obs))
|
|
342
363
|
t.true(comparePoints(pts, exp))
|
|
343
364
|
|
|
344
365
|
// segments 1 - sharp points at corner
|
|
@@ -354,6 +375,7 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
|
|
|
354
375
|
[-6, 6],
|
|
355
376
|
[-6, -6]
|
|
356
377
|
]
|
|
378
|
+
t.notThrows(() => geom2.validate(obs))
|
|
357
379
|
t.true(comparePoints(pts, exp))
|
|
358
380
|
|
|
359
381
|
// segments 16 - rounded corners
|
|
@@ -377,6 +399,7 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
|
|
|
377
399
|
[-3.5, 4.5],
|
|
378
400
|
[-4.5, 4.5]
|
|
379
401
|
]
|
|
402
|
+
t.notThrows(() => geom2.validate(obs))
|
|
380
403
|
t.true(comparePoints(pts, exp))
|
|
381
404
|
})
|
|
382
405
|
|
|
@@ -429,6 +452,7 @@ test('offset (options): offsetting of a complex geom2 produces expected offset g
|
|
|
429
452
|
[-4, -13],
|
|
430
453
|
[-77, -77]
|
|
431
454
|
]
|
|
455
|
+
t.notThrows(() => geom2.validate(obs))
|
|
432
456
|
t.is(pts.length, 20)
|
|
433
457
|
t.true(comparePoints(pts, exp))
|
|
434
458
|
})
|
|
@@ -473,6 +497,7 @@ test('offset (options): offsetting of round geom2 produces expected offset geom2
|
|
|
473
497
|
[6.7105900605102855, -6.710590060510285],
|
|
474
498
|
[8.767810140100096, -3.6317399864658024]
|
|
475
499
|
]
|
|
500
|
+
t.notThrows(() => geom2.validate(obs))
|
|
476
501
|
t.is(pts.length, 16)
|
|
477
502
|
t.true(comparePoints(pts, exp))
|
|
478
503
|
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const { area } = require('../../../maths/utils')
|
|
2
|
+
const { toOutlines } = require('../../../geometries/geom2')
|
|
3
|
+
const { arePointsInside } = require('../../../geometries/poly2')
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Constructs a polygon hierarchy of solids and holes.
|
|
7
|
+
* The hierarchy is represented as a forest of trees. All trees shall be depth at most 2.
|
|
8
|
+
* If a solid exists inside the hole of another solid, it will be split out as its own root.
|
|
9
|
+
*
|
|
10
|
+
* @param {geom2} geometry
|
|
11
|
+
* @returns {Array} an array of polygons with associated holes
|
|
12
|
+
* @alias module:modeling/geometries/geom2.toTree
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const geometry = subtract(rectangle({size: [5, 5]}), rectangle({size: [3, 3]}))
|
|
16
|
+
* console.log(assignHoles(geometry))
|
|
17
|
+
* [{
|
|
18
|
+
* "solid": [[-2.5,-2.5],[2.5,-2.5],[2.5,2.5],[-2.5,2.5]],
|
|
19
|
+
* "holes": [[[-1.5,1.5],[1.5,1.5],[1.5,-1.5],[-1.5,-1.5]]]
|
|
20
|
+
* }]
|
|
21
|
+
*/
|
|
22
|
+
const assignHoles = (geometry) => {
|
|
23
|
+
const outlines = toOutlines(geometry)
|
|
24
|
+
const solids = [] // solid indices
|
|
25
|
+
const holes = [] // hole indices
|
|
26
|
+
outlines.forEach((outline, i) => {
|
|
27
|
+
const a = area(outline)
|
|
28
|
+
if (a < 0) {
|
|
29
|
+
holes.push(i)
|
|
30
|
+
} else if (a > 0) {
|
|
31
|
+
solids.push(i)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// for each hole, determine what solids it is inside of
|
|
36
|
+
const children = [] // child holes of solid[i]
|
|
37
|
+
const parents = [] // parent solids of hole[i]
|
|
38
|
+
solids.forEach((s, i) => {
|
|
39
|
+
const solid = outlines[s]
|
|
40
|
+
children[i] = []
|
|
41
|
+
holes.forEach((h, j) => {
|
|
42
|
+
const hole = outlines[h]
|
|
43
|
+
// check if a point of hole j is inside solid i
|
|
44
|
+
if (arePointsInside([hole[0]], { vertices: solid })) {
|
|
45
|
+
children[i].push(h)
|
|
46
|
+
if (!parents[j]) parents[j] = []
|
|
47
|
+
parents[j].push(i)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// check if holes have multiple parents and choose one with fewest children
|
|
53
|
+
holes.forEach((h, j) => {
|
|
54
|
+
// ensure at least one parent exists
|
|
55
|
+
if (parents[j] && parents[j].length > 1) {
|
|
56
|
+
// the solid directly containing this hole
|
|
57
|
+
const directParent = minIndex(parents[j], (p) => children[p].length)
|
|
58
|
+
parents[j].forEach((p, i) => {
|
|
59
|
+
if (i !== directParent) {
|
|
60
|
+
// Remove hole from skip level parents
|
|
61
|
+
children[p] = children[p].filter((c) => c !== h)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// map indices back to points
|
|
68
|
+
return children.map((holes, i) => ({
|
|
69
|
+
solid: outlines[solids[i]],
|
|
70
|
+
holes: holes.map((h) => outlines[h])
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Find the item in the list with smallest score(item).
|
|
76
|
+
* If the list is empty, return undefined.
|
|
77
|
+
*/
|
|
78
|
+
const minIndex = (list, score) => {
|
|
79
|
+
let bestIndex
|
|
80
|
+
let best
|
|
81
|
+
list.forEach((item, index) => {
|
|
82
|
+
const value = score(item)
|
|
83
|
+
if (best === undefined || value < best) {
|
|
84
|
+
bestIndex = index
|
|
85
|
+
best = value
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
return bestIndex
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = assignHoles
|