@jscad/modeling 2.8.0 → 2.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/jscad-modeling.min.js +433 -391
  3. package/package.json +2 -2
  4. package/src/geometries/geom2/index.d.ts +1 -0
  5. package/src/geometries/geom2/index.js +2 -1
  6. package/src/geometries/geom2/validate.d.ts +3 -0
  7. package/src/geometries/geom2/validate.js +36 -0
  8. package/src/geometries/geom3/index.d.ts +1 -0
  9. package/src/geometries/geom3/index.js +2 -1
  10. package/src/geometries/geom3/isA.js +1 -1
  11. package/src/geometries/geom3/validate.d.ts +3 -0
  12. package/src/geometries/geom3/validate.js +62 -0
  13. package/src/geometries/path2/index.d.ts +1 -0
  14. package/src/geometries/path2/index.js +2 -1
  15. package/src/geometries/path2/validate.d.ts +3 -0
  16. package/src/geometries/path2/validate.js +41 -0
  17. package/src/geometries/poly2/arePointsInside.js +0 -35
  18. package/src/geometries/poly3/index.d.ts +1 -0
  19. package/src/geometries/poly3/index.js +2 -1
  20. package/src/geometries/poly3/invert.js +7 -1
  21. package/src/geometries/poly3/measureArea.test.js +16 -16
  22. package/src/geometries/poly3/measureBoundingSphere.test.js +8 -8
  23. package/src/geometries/poly3/validate.d.ts +4 -0
  24. package/src/geometries/poly3/validate.js +50 -0
  25. package/src/measurements/measureCenterOfMass.test.js +2 -2
  26. package/src/operations/booleans/intersect.test.js +8 -0
  27. package/src/operations/booleans/scission.test.js +4 -4
  28. package/src/operations/booleans/subtract.test.js +8 -0
  29. package/src/operations/booleans/trees/Node.js +10 -16
  30. package/src/operations/booleans/trees/PolygonTreeNode.js +13 -14
  31. package/src/operations/booleans/trees/Tree.js +1 -2
  32. package/src/operations/booleans/trees/splitPolygonByPlane.js +2 -3
  33. package/src/operations/booleans/union.test.js +27 -0
  34. package/src/operations/expansions/expand.test.js +30 -21
  35. package/src/operations/expansions/expandShell.js +2 -2
  36. package/src/operations/expansions/offset.test.js +25 -0
  37. package/src/operations/extrusions/earcut/assignHoles.js +91 -0
  38. package/src/operations/extrusions/earcut/assignHoles.test.js +74 -0
  39. package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
  40. package/src/operations/extrusions/earcut/index.js +252 -0
  41. package/src/operations/extrusions/earcut/linkedList.js +58 -0
  42. package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
  43. package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
  44. package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
  45. package/src/operations/extrusions/earcut/triangle.js +16 -0
  46. package/src/operations/extrusions/extrudeFromSlices.js +10 -3
  47. package/src/operations/extrusions/extrudeFromSlices.test.js +47 -31
  48. package/src/operations/extrusions/extrudeLinear.js +4 -3
  49. package/src/operations/extrusions/extrudeLinear.test.js +69 -37
  50. package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
  51. package/src/operations/extrusions/extrudeRectangular.test.js +22 -15
  52. package/src/operations/extrusions/extrudeRotate.test.js +31 -27
  53. package/src/operations/extrusions/project.test.js +5 -5
  54. package/src/operations/extrusions/slice/calculatePlane.js +7 -4
  55. package/src/operations/extrusions/slice/repairSlice.js +47 -0
  56. package/src/operations/extrusions/slice/toPolygons.js +24 -60
  57. package/src/operations/hulls/hull.test.js +24 -1
  58. package/src/operations/hulls/hullChain.test.js +6 -4
  59. package/src/operations/hulls/hullPath2.test.js +1 -1
  60. package/src/operations/modifiers/generalize.test.js +6 -0
  61. package/src/operations/transforms/align.test.js +12 -0
  62. package/src/operations/transforms/center.test.js +12 -0
  63. package/src/operations/transforms/mirror.test.js +16 -0
  64. package/src/operations/transforms/rotate.test.js +10 -0
  65. package/src/operations/transforms/scale.test.js +15 -0
  66. package/src/operations/transforms/transform.test.js +5 -0
  67. package/src/operations/transforms/translate.test.js +16 -0
  68. package/src/primitives/arc.test.js +11 -0
  69. package/src/primitives/circle.test.js +15 -9
  70. package/src/primitives/cube.test.js +3 -0
  71. package/src/primitives/cuboid.test.js +9 -24
  72. package/src/primitives/cylinder.test.js +7 -4
  73. package/src/primitives/cylinderElliptic.js +13 -6
  74. package/src/primitives/cylinderElliptic.test.js +72 -50
  75. package/src/primitives/ellipse.js +3 -1
  76. package/src/primitives/ellipse.test.js +14 -8
  77. package/src/primitives/ellipsoid.js +6 -4
  78. package/src/primitives/ellipsoid.test.js +84 -80
  79. package/src/primitives/geodesicSphere.test.js +3 -0
  80. package/src/primitives/line.test.js +1 -0
  81. package/src/primitives/polygon.test.js +15 -10
  82. package/src/primitives/polyhedron.test.js +14 -42
  83. package/src/primitives/rectangle.test.js +3 -0
  84. package/src/primitives/roundedCuboid.test.js +5 -0
  85. package/src/primitives/roundedCylinder.js +6 -4
  86. package/src/primitives/roundedCylinder.test.js +40 -36
  87. package/src/primitives/roundedRectangle.test.js +5 -0
  88. package/src/primitives/sphere.test.js +52 -73
  89. package/src/primitives/square.test.js +3 -0
  90. package/src/primitives/star.test.js +6 -0
  91. package/src/primitives/torus.test.js +8 -1
  92. package/src/primitives/triangle.test.js +7 -0
  93. package/src/utils/areAllShapesTheSameType.js +2 -2
  94. package/src/utils/areAllShapesTheSameType.test.js +17 -0
  95. package/src/utils/index.d.ts +1 -0
  96. package/src/utils/index.js +3 -1
  97. package/src/utils/trigonometry.d.ts +2 -0
  98. package/src/utils/trigonometry.js +35 -0
  99. package/src/utils/trigonometry.test.js +25 -0
  100. package/test/helpers/nearlyEqual.js +4 -1
@@ -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 node1 = polygontreenodes[i]
54
- if (!node1.isRemoved()) {
55
- node1.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes)
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
- // there's nothing behind this plane. Delete the nodes behind this plane:
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 = null
24
+ constructor (parent, polygon) {
25
+ this.parent = parent
26
26
  this.children = []
27
- this.polygon = null
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
- let node = this
211
- while (node.polygon) {
212
- node.polygon = null
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}poly3\n`
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 < 0)
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, point, nextpoint)
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.is(pts2.length, 1612)
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.00000, 75.00000], [-75.00000, -75.00000]],
129
- [[-75.00000, -75.00000], [75.00000, -75.00000]],
130
- [[75.00000, -75.00000], [75.00000, 75.00000]],
131
- [[-40.00000, 75.00000], [-75.00000, 75.00000]],
132
- [[75.00000, 75.00000], [40.00000, 75.00000]],
133
- [[40.00000, 75.00000], [40.00000, 0.00000]],
134
- [[40.00000, 0.00000], [-40.00000, 0.00000]],
135
- [[-40.00000, 0.00000], [-40.00000, 75.00000]],
136
- [[15.00000, -10.00000], [15.00000, -40.00000]],
137
- [[-15.00000, -10.00000], [15.00000, -10.00000]],
138
- [[-15.00000, -40.00000], [-15.00000, -10.00000]],
139
- [[-8.00000, -40.00000], [-15.00000, -40.00000]],
140
- [[15.00000, -40.00000], [8.00000, -40.00000]],
141
- [[-8.00000, -25.00000], [-8.00000, -40.00000]],
142
- [[8.00000, -25.00000], [-8.00000, -25.00000]],
143
- [[8.00000, -40.00000], [8.00000, -25.00000]],
144
- [[-2.00000, -15.00000], [-2.00000, -19.00000]],
145
- [[-2.00000, -19.00000], [2.00000, -19.00000]],
146
- [[2.00000, -19.00000], [2.00000, -15.00000]],
147
- [[2.00000, -15.00000], [-2.00000, -15.00000]]
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