@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +4 -4
  3. package/dist/jscad-modeling.min.js +437 -428
  4. package/package.json +3 -2
  5. package/src/colors/colorize.test.js +1 -1
  6. package/src/geometries/geom2/index.d.ts +1 -0
  7. package/src/geometries/geom2/index.js +2 -1
  8. package/src/geometries/geom2/toOutlines.js +66 -52
  9. package/src/geometries/geom2/validate.d.ts +3 -0
  10. package/src/geometries/geom2/validate.js +36 -0
  11. package/src/geometries/geom3/create.js +1 -1
  12. package/src/geometries/geom3/create.test.js +1 -1
  13. package/src/geometries/geom3/fromPoints.js +1 -1
  14. package/src/geometries/geom3/index.d.ts +1 -0
  15. package/src/geometries/geom3/index.js +2 -1
  16. package/src/geometries/geom3/isA.js +1 -1
  17. package/src/geometries/geom3/validate.d.ts +3 -0
  18. package/src/geometries/geom3/validate.js +62 -0
  19. package/src/geometries/path2/index.d.ts +1 -1
  20. package/src/geometries/path2/index.js +2 -2
  21. package/src/geometries/path2/validate.d.ts +3 -0
  22. package/src/geometries/path2/validate.js +41 -0
  23. package/src/geometries/poly2/arePointsInside.js +0 -35
  24. package/src/geometries/poly3/create.js +1 -1
  25. package/src/geometries/poly3/index.d.ts +1 -0
  26. package/src/geometries/poly3/index.js +2 -1
  27. package/src/geometries/poly3/measureArea.test.js +16 -16
  28. package/src/geometries/poly3/measureBoundingSphere.test.js +8 -8
  29. package/src/geometries/poly3/validate.d.ts +4 -0
  30. package/src/geometries/poly3/validate.js +64 -0
  31. package/src/maths/constants.d.ts +1 -0
  32. package/src/maths/constants.js +11 -0
  33. package/src/maths/utils/aboutEqualNormals.js +1 -5
  34. package/src/measurements/measureCenterOfMass.test.js +2 -2
  35. package/src/operations/booleans/intersect.test.js +8 -0
  36. package/src/operations/booleans/intersectGeom3.js +2 -1
  37. package/src/operations/booleans/scission.test.js +4 -4
  38. package/src/operations/booleans/subtract.test.js +8 -0
  39. package/src/operations/booleans/subtractGeom3.js +2 -1
  40. package/src/operations/booleans/to3DWalls.js +1 -1
  41. package/src/operations/booleans/trees/Node.js +10 -16
  42. package/src/operations/booleans/trees/PolygonTreeNode.js +13 -14
  43. package/src/operations/booleans/trees/Tree.js +1 -2
  44. package/src/operations/booleans/trees/splitPolygonByPlane.js +2 -3
  45. package/src/operations/booleans/union.test.js +27 -0
  46. package/src/operations/booleans/unionGeom3.js +2 -1
  47. package/src/operations/expansions/expand.test.js +30 -21
  48. package/src/operations/expansions/expandGeom3.test.js +14 -14
  49. package/src/operations/expansions/expandShell.js +5 -4
  50. package/src/operations/expansions/extrudePolygon.js +7 -7
  51. package/src/operations/expansions/offset.test.js +25 -0
  52. package/src/operations/extrusions/earcut/assignHoles.js +7 -3
  53. package/src/operations/extrusions/earcut/assignHoles.test.js +50 -4
  54. package/src/operations/extrusions/earcut/linkedList.js +1 -1
  55. package/src/operations/extrusions/extrudeFromSlices.test.js +16 -10
  56. package/src/operations/extrusions/extrudeLinear.test.js +15 -9
  57. package/src/operations/extrusions/extrudeRectangular.test.js +15 -8
  58. package/src/operations/extrusions/extrudeRotate.js +5 -1
  59. package/src/operations/extrusions/extrudeRotate.test.js +12 -0
  60. package/src/operations/extrusions/extrudeWalls.js +2 -2
  61. package/src/operations/extrusions/project.js +11 -14
  62. package/src/operations/extrusions/project.test.js +55 -55
  63. package/src/operations/hulls/hull.test.js +24 -1
  64. package/src/operations/hulls/hullChain.test.js +6 -4
  65. package/src/operations/hulls/hullGeom2.js +6 -18
  66. package/src/operations/hulls/hullGeom3.js +5 -18
  67. package/src/operations/hulls/hullPath2.js +4 -14
  68. package/src/operations/hulls/hullPath2.test.js +1 -1
  69. package/src/operations/hulls/hullPoints2.js +43 -92
  70. package/src/operations/hulls/toUniquePoints.js +34 -0
  71. package/src/operations/modifiers/generalize.js +2 -13
  72. package/src/operations/modifiers/generalize.test.js +5 -31
  73. package/src/operations/modifiers/insertTjunctions.js +1 -1
  74. package/src/operations/modifiers/insertTjunctions.test.js +21 -21
  75. package/src/operations/modifiers/mergePolygons.js +11 -14
  76. package/src/operations/{booleans → modifiers}/reTesselateCoplanarPolygons.js +1 -1
  77. package/src/operations/{booleans → modifiers}/reTesselateCoplanarPolygons.test.js +5 -5
  78. package/src/operations/{booleans → modifiers}/retessellate.js +2 -9
  79. package/src/operations/{booleans → modifiers}/retessellate.test.js +0 -0
  80. package/src/operations/modifiers/snapPolygons.test.js +12 -12
  81. package/src/operations/modifiers/triangulatePolygons.js +3 -3
  82. package/src/operations/transforms/align.test.js +12 -0
  83. package/src/operations/transforms/center.js +1 -1
  84. package/src/operations/transforms/center.test.js +12 -0
  85. package/src/operations/transforms/mirror.test.js +16 -0
  86. package/src/operations/transforms/rotate.test.js +10 -0
  87. package/src/operations/transforms/scale.test.js +15 -0
  88. package/src/operations/transforms/transform.test.js +5 -0
  89. package/src/operations/transforms/translate.test.js +16 -0
  90. package/src/primitives/arc.test.js +11 -0
  91. package/src/primitives/circle.test.js +15 -9
  92. package/src/primitives/cube.test.js +3 -0
  93. package/src/primitives/cuboid.js +1 -1
  94. package/src/primitives/cuboid.test.js +9 -24
  95. package/src/primitives/cylinder.test.js +7 -4
  96. package/src/primitives/cylinderElliptic.js +14 -7
  97. package/src/primitives/cylinderElliptic.test.js +72 -50
  98. package/src/primitives/ellipse.js +3 -1
  99. package/src/primitives/ellipse.test.js +14 -8
  100. package/src/primitives/ellipsoid.js +8 -6
  101. package/src/primitives/ellipsoid.test.js +84 -80
  102. package/src/primitives/geodesicSphere.test.js +3 -0
  103. package/src/primitives/line.test.js +1 -0
  104. package/src/primitives/polygon.test.js +15 -10
  105. package/src/primitives/polyhedron.js +1 -1
  106. package/src/primitives/polyhedron.test.js +14 -42
  107. package/src/primitives/rectangle.test.js +3 -0
  108. package/src/primitives/roundedCuboid.js +6 -6
  109. package/src/primitives/roundedCuboid.test.js +5 -0
  110. package/src/primitives/roundedCylinder.js +7 -5
  111. package/src/primitives/roundedCylinder.test.js +40 -36
  112. package/src/primitives/roundedRectangle.test.js +5 -0
  113. package/src/primitives/sphere.test.js +52 -73
  114. package/src/primitives/square.test.js +3 -0
  115. package/src/primitives/star.test.js +6 -0
  116. package/src/primitives/torus.test.js +8 -1
  117. package/src/primitives/triangle.js +1 -2
  118. package/src/primitives/triangle.test.js +7 -0
  119. package/src/utils/areAllShapesTheSameType.js +2 -2
  120. package/src/utils/areAllShapesTheSameType.test.js +17 -0
  121. package/src/utils/index.d.ts +1 -0
  122. package/src/utils/index.js +3 -1
  123. package/src/utils/trigonometry.d.ts +2 -0
  124. package/src/utils/trigonometry.js +34 -0
  125. package/src/utils/trigonometry.test.js +25 -0
  126. package/test/helpers/nearlyEqual.js +4 -1
  127. package/src/geometries/path2/eachPoint.d.ts +0 -9
  128. package/src/geometries/path2/eachPoint.js +0 -17
  129. package/src/geometries/path2/eachPoint.test.js +0 -11
  130. package/src/operations/modifiers/edges.js +0 -195
  131. package/src/operations/modifiers/repairTjunctions.js +0 -44
@@ -0,0 +1,64 @@
1
+ const signedDistanceToPoint = require('../../maths/plane/signedDistanceToPoint')
2
+ const { NEPS } = require('../../maths/constants')
3
+ const vec3 = require('../../maths/vec3')
4
+ const isA = require('./isA')
5
+ const isConvex = require('./isConvex')
6
+ const measureArea = require('./measureArea')
7
+ const plane = require('./plane')
8
+
9
+ /**
10
+ * Determine if the given object is a valid polygon.
11
+ * Checks for valid data structure, convex polygons, and duplicate points.
12
+ *
13
+ * **If the geometry is not valid, an exception will be thrown with details of the geometry error.**
14
+ *
15
+ * @param {Object} object - the object to interrogate
16
+ * @throws {Error} error if the geometry is not valid
17
+ * @alias module:modeling/geometries/poly3.validate
18
+ */
19
+ const validate = (object) => {
20
+ if (!isA(object)) {
21
+ throw new Error('invalid poly3 structure')
22
+ }
23
+
24
+ // check for empty polygon
25
+ if (object.vertices.length < 3) {
26
+ throw new Error(`poly3 not enough vertices ${object.vertices.length}`)
27
+ }
28
+ // check area
29
+ if (measureArea(object) <= 0) {
30
+ throw new Error('poly3 area must be greater than zero')
31
+ }
32
+
33
+ // check for duplicate points
34
+ for (let i = 0; i < object.vertices.length; i++) {
35
+ if (vec3.equals(object.vertices[i], object.vertices[(i + 1) % object.vertices.length])) {
36
+ throw new Error(`poly3 duplicate vertex ${object.vertices[i]}`)
37
+ }
38
+ }
39
+
40
+ // check convexity
41
+ if (!isConvex(object)) {
42
+ throw new Error('poly3 must be convex')
43
+ }
44
+
45
+ // check for infinity, nan
46
+ object.vertices.forEach((vertex) => {
47
+ if (!vertex.every(Number.isFinite)) {
48
+ throw new Error(`poly3 invalid vertex ${vertex}`)
49
+ }
50
+ })
51
+
52
+ // check that points are co-planar
53
+ if (object.vertices.length > 3) {
54
+ const normal = plane(object)
55
+ object.vertices.forEach((vertex) => {
56
+ const dist = Math.abs(signedDistanceToPoint(normal, vertex))
57
+ if (dist > NEPS) {
58
+ throw new Error(`poly3 must be coplanar: vertex ${vertex} distance ${dist}`)
59
+ }
60
+ })
61
+ }
62
+ }
63
+
64
+ module.exports = validate
@@ -1,2 +1,3 @@
1
1
  export const EPS: number
2
+ export const NEPS: number
2
3
  export const spatialResolution: number
@@ -14,7 +14,18 @@ const spatialResolution = 1e5
14
14
  */
15
15
  const EPS = 1e-5
16
16
 
17
+ /**
18
+ * Smaller epsilon used for measuring near zero distances.
19
+ * @default
20
+ * @alias module:modeling/maths.NEPS
21
+ */
22
+ const NEPS = 1e-13
23
+ // NEPS is derived from a series of tests to determine the optimal precision
24
+ // for comparing coplanar polygons, as provided by the sphere primitive at high
25
+ // segmentation. NEPS is for 64 bit Number values.
26
+
17
27
  module.exports = {
18
28
  EPS,
29
+ NEPS,
19
30
  spatialResolution
20
31
  }
@@ -1,8 +1,4 @@
1
- // Normals are directional vectors with component values from 0 to 1.0, requiring specialized comparison
2
- // This EPS is derived from a series of tests to determine the optimal precision for comparing coplanar polygons,
3
- // as provided by the sphere primitive at high segmentation
4
- // This EPS is for 64 bit Number values
5
- const NEPS = 1e-13
1
+ const { NEPS } = require('../constants')
6
2
 
7
3
  /**
8
4
  * Compare two normals (unit vectors) for near equality.
@@ -51,8 +51,8 @@ test('measureCenterOfMass (multiple objects)', (t) => {
51
51
  const o = {}
52
52
 
53
53
  let allcenters = measureCenterOfMass(aline, arect, asphere, o)
54
- t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.99999999999999, -5.000000000000007, 49.99999999999992], [0, 0, 0]])
54
+ t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.999999999999991, -5.000000000000006, 49.999999999999915], [0, 0, 0]])
55
55
 
56
56
  allcenters = measureCenterOfMass(aline, arect, asphere, o)
57
- t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.99999999999999, -5.000000000000007, 49.99999999999992], [0, 0, 0]])
57
+ t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.999999999999991, -5.000000000000006, 49.999999999999915], [0, 0, 0]])
58
58
  })
@@ -36,6 +36,7 @@ test('intersect: intersect of one or more geom2 objects produces expected geomet
36
36
  [0, -2],
37
37
  [1.4142000000000001, -1.4142000000000001]
38
38
  ]
39
+ t.notThrows(() => geom2.validate(result1))
39
40
  t.is(obs.length, 8)
40
41
  t.true(comparePoints(obs, exp))
41
42
 
@@ -44,6 +45,7 @@ test('intersect: intersect of one or more geom2 objects produces expected geomet
44
45
 
45
46
  const result2 = intersect(geometry1, geometry2)
46
47
  obs = geom2.toPoints(result2)
48
+ t.notThrows(() => geom2.validate(result2))
47
49
  t.is(obs.length, 0)
48
50
 
49
51
  // intersect of two partially overlapping objects
@@ -54,6 +56,7 @@ test('intersect: intersect of one or more geom2 objects produces expected geomet
54
56
  exp = [
55
57
  [9, 9], [8, 9], [8, 8], [9, 8]
56
58
  ]
59
+ t.notThrows(() => geom2.validate(result3))
57
60
  t.is(obs.length, 4)
58
61
  t.true(comparePoints(obs, exp))
59
62
 
@@ -70,6 +73,7 @@ test('intersect: intersect of one or more geom2 objects produces expected geomet
70
73
  [0, -2],
71
74
  [1.4142000000000001, -1.4142000000000001]
72
75
  ]
76
+ t.notThrows(() => geom2.validate(result4))
73
77
  t.is(obs.length, 8)
74
78
  t.true(comparePoints(obs, exp))
75
79
  })
@@ -130,6 +134,7 @@ test('intersect: intersect of one or more geom3 objects produces expected geomet
130
134
  [[0.9999999999999998, 1.0000000000000002, -1.414213562373095], [1.4142135623730951, 3.4638242249419736e-16, -1.414213562373095], [8.65956056235493e-17, 8.659560562354935e-17, -2]],
131
135
  [[8.65956056235493e-17, 8.659560562354935e-17, 2], [1.4142135623730951, 3.4638242249419736e-16, 1.414213562373095], [0.9999999999999998, 1.0000000000000002, 1.414213562373095]]
132
136
  ]
137
+ t.notThrows.skip(() => geom3.validate(result1))
133
138
  t.is(obs.length, 32)
134
139
  t.true(comparePolygonsAsPoints(obs, exp))
135
140
 
@@ -138,6 +143,7 @@ test('intersect: intersect of one or more geom3 objects produces expected geomet
138
143
 
139
144
  const result2 = intersect(geometry1, geometry2)
140
145
  obs = geom3.toPoints(result2)
146
+ t.notThrows(() => geom3.validate(result2))
141
147
  t.is(obs.length, 0)
142
148
 
143
149
  // intersect of two partially overlapping objects
@@ -166,11 +172,13 @@ test('intersect: intersect of one or more geom3 objects produces expected geomet
166
172
  [[9, 8, 8], [8, 8, 8], [8, 9, 8], [9, 9, 8]]
167
173
  ]
168
174
 
175
+ t.notThrows(() => geom3.validate(result3))
169
176
  t.is(obs.length, 6)
170
177
  t.true(comparePolygonsAsPoints(obs, exp))
171
178
 
172
179
  // intersect of two completely overlapping objects
173
180
  const result4 = intersect(geometry1, geometry3)
174
181
  obs = geom3.toPoints(result4)
182
+ t.notThrows.skip(() => geom3.validate(result4))
175
183
  t.is(obs.length, 32)
176
184
  })
@@ -1,6 +1,7 @@
1
1
  const flatten = require('../../utils/flatten')
2
2
 
3
- const retessellate = require('./retessellate')
3
+ const retessellate = require('../modifiers/retessellate')
4
+
4
5
  const intersectSub = require('./intersectGeom3Sub')
5
6
 
6
7
  /*
@@ -20,9 +20,9 @@ test('scission: scission of one or more geom3 objects produces expected geometry
20
20
  t.is(result2.length, 3)
21
21
  t.is(result2[0].length, 0)
22
22
  t.is(result2[1].length, 1)
23
- t.true(geom3.isA(result2[1][0]))
23
+ t.notThrows(() => geom3.validate(result2[1][0]))
24
24
  t.is(result2[2].length, 1)
25
- t.true(geom3.isA(result2[2][0]))
25
+ t.notThrows(() => geom3.validate(result2[2][0]))
26
26
  })
27
27
 
28
28
  test('scission: scission of complex geom3 produces expected geometry', (t) => {
@@ -40,8 +40,8 @@ test('scission: scission of complex geom3 produces expected geometry', (t) => {
40
40
 
41
41
  const result1 = scission(geometry3)
42
42
  t.is(result1.length, 2)
43
- t.true(geom3.isA(result1[0]))
44
- t.true(geom3.isA(result1[1]))
43
+ t.notThrows.skip(() => geom3.validate(result1[0]))
44
+ t.notThrows.skip(() => geom3.validate(result1[1]))
45
45
 
46
46
  const rc1 = geom3.toPolygons(result1[0]).length
47
47
  const rc2 = geom3.toPolygons(result1[1]).length
@@ -36,6 +36,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
36
36
  [0, -2],
37
37
  [1.4142000000000001, -1.4142000000000001]
38
38
  ]
39
+ t.notThrows(() => geom2.validate(result1))
39
40
  t.is(obs.length, 8)
40
41
  t.true(comparePoints(obs, exp))
41
42
 
@@ -54,6 +55,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
54
55
  [0, -2],
55
56
  [1.4142000000000001, -1.4142000000000001]
56
57
  ]
58
+ t.notThrows(() => geom2.validate(result2))
57
59
  t.is(obs.length, 8)
58
60
  t.true(comparePoints(obs, exp))
59
61
 
@@ -65,6 +67,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
65
67
  exp = [
66
68
  [12, 12], [9, 9], [8, 9], [8, 12], [9, 8], [12, 8]
67
69
  ]
70
+ t.notThrows(() => geom2.validate(result3))
68
71
  t.is(obs.length, 6)
69
72
  t.true(comparePoints(obs, exp))
70
73
 
@@ -73,6 +76,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
73
76
  obs = geom2.toPoints(result4)
74
77
  exp = [
75
78
  ]
79
+ t.notThrows(() => geom2.validate(result4))
76
80
  t.is(obs.length, 0)
77
81
  t.deepEqual(obs, exp)
78
82
  })
@@ -133,6 +137,7 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
133
137
  [[0.9999999999999998, 1.0000000000000002, -1.414213562373095], [1.4142135623730951, 3.4638242249419736e-16, -1.414213562373095], [8.65956056235493e-17, 8.659560562354935e-17, -2]],
134
138
  [[8.65956056235493e-17, 8.659560562354935e-17, 2], [1.4142135623730951, 3.4638242249419736e-16, 1.414213562373095], [0.9999999999999998, 1.0000000000000002, 1.414213562373095]]
135
139
  ]
140
+ t.notThrows.skip(() => geom3.validate(result1))
136
141
  t.is(obs.length, 32)
137
142
  t.true(comparePolygonsAsPoints(obs, exp))
138
143
 
@@ -141,6 +146,7 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
141
146
 
142
147
  const result2 = subtract(geometry1, geometry2)
143
148
  obs = geom3.toPoints(result2)
149
+ t.notThrows.skip(() => geom3.validate(result2))
144
150
  t.is(obs.length, 32)
145
151
 
146
152
  // subtract of two partially overlapping objects
@@ -162,11 +168,13 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
162
168
  [[12, 12, 8], [12, 9, 8], [8, 9, 8], [8, 12, 8]],
163
169
  [[12, 9, 8], [12, 8, 8], [9, 8, 8], [9, 9, 8]]
164
170
  ]
171
+ t.notThrows.skip(() => geom3.validate(result3))
165
172
  t.is(obs.length, 12)
166
173
  t.true(comparePolygonsAsPoints(obs, exp))
167
174
 
168
175
  // subtract of two completely overlapping objects
169
176
  const result4 = subtract(geometry1, geometry3)
170
177
  obs = geom3.toPoints(result4)
178
+ t.notThrows(() => geom3.validate(result4))
171
179
  t.is(obs.length, 0)
172
180
  })
@@ -1,6 +1,7 @@
1
1
  const flatten = require('../../utils/flatten')
2
2
 
3
- const retessellate = require('./retessellate')
3
+ const retessellate = require('../modifiers/retessellate')
4
+
4
5
  const subtractSub = require('./subtractGeom3Sub')
5
6
 
6
7
  /*
@@ -14,7 +14,7 @@ const to3DWall = (z0, z1, side) => {
14
14
  vec3.fromVec2(vec3.create(), side[1], z1),
15
15
  vec3.fromVec2(vec3.create(), side[0], z1)
16
16
  ]
17
- return poly3.fromPoints(points)
17
+ return poly3.create(points)
18
18
  }
19
19
 
20
20
  /*
@@ -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
  })
@@ -1,6 +1,7 @@
1
1
  const flatten = require('../../utils/flatten')
2
2
 
3
- const retessellate = require('./retessellate')
3
+ const retessellate = require('../modifiers/retessellate')
4
+
4
5
  const unionSub = require('./unionGeom3Sub')
5
6
 
6
7
  /*
@@ -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, 1588)
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
  })