@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
@@ -12,11 +12,13 @@ test('extrudeRectangular (defaults)', (t) => {
12
12
 
13
13
  let obs = extrudeRectangular({ }, geometry1)
14
14
  let pts = geom3.toPoints(obs)
15
- t.is(pts.length, 34)
15
+ t.notThrows(() => geom3.validate(obs))
16
+ t.is(pts.length, 44)
16
17
 
17
18
  obs = extrudeRectangular({ }, geometry2)
18
19
  pts = geom3.toPoints(obs)
19
- t.is(pts.length, 24)
20
+ t.notThrows(() => geom3.validate(obs))
21
+ t.is(pts.length, 32)
20
22
  })
21
23
 
22
24
  test('extrudeRectangular (chamfer)', (t) => {
@@ -25,11 +27,13 @@ test('extrudeRectangular (chamfer)', (t) => {
25
27
 
26
28
  let obs = extrudeRectangular({ corners: 'chamfer' }, geometry1)
27
29
  let pts = geom3.toPoints(obs)
28
- t.is(pts.length, 42)
30
+ t.notThrows(() => geom3.validate(obs))
31
+ t.is(pts.length, 60)
29
32
 
30
33
  obs = extrudeRectangular({ corners: 'chamfer' }, geometry2)
31
34
  pts = geom3.toPoints(obs)
32
- t.is(pts.length, 32)
35
+ t.notThrows(() => geom3.validate(obs))
36
+ t.is(pts.length, 48)
33
37
  })
34
38
 
35
39
  test('extrudeRectangular (segments = 8, round)', (t) => {
@@ -38,26 +42,29 @@ test('extrudeRectangular (segments = 8, round)', (t) => {
38
42
 
39
43
  let obs = extrudeRectangular({ segments: 8, corners: 'round' }, geometry1)
40
44
  let pts = geom3.toPoints(obs)
41
- t.is(pts.length, 56)
45
+ t.notThrows(() => geom3.validate(obs))
46
+ t.is(pts.length, 84)
42
47
 
43
48
  obs = extrudeRectangular({ segments: 8, corners: 'round' }, geometry2)
44
49
  pts = geom3.toPoints(obs)
45
- t.is(pts.length, 40)
50
+ t.notThrows(() => geom3.validate(obs))
51
+ t.is(pts.length, 64)
46
52
  })
47
53
 
48
54
  test('extrudeRectangular (holes)', (t) => {
49
55
  const geometry2 = geom2.create([
50
- [[15.00000, 15.00000], [-15.00000, 15.00000]],
51
- [[-15.00000, 15.00000], [-15.00000, -15.00000]],
52
- [[-15.00000, -15.00000], [15.00000, -15.00000]],
53
- [[15.00000, -15.00000], [15.00000, 15.00000]],
54
- [[-5.00000, 5.00000], [5.00000, 5.00000]],
55
- [[5.00000, 5.00000], [5.00000, -5.00000]],
56
- [[5.00000, -5.00000], [-5.00000, -5.00000]],
57
- [[-5.00000, -5.00000], [-5.00000, 5.00000]]
56
+ [[15, 15], [-15, 15]],
57
+ [[-15, 15], [-15, -15]],
58
+ [[-15, -15], [15, -15]],
59
+ [[15, -15], [15, 15]],
60
+ [[-5, 5], [5, 5]],
61
+ [[5, 5], [5, -5]],
62
+ [[5, -5], [-5, -5]],
63
+ [[-5, -5], [-5, 5]]
58
64
  ])
59
65
 
60
66
  const obs = extrudeRectangular({ size: 2, height: 15, segments: 16, corners: 'round' }, geometry2)
61
67
  const pts = geom3.toPoints(obs)
62
- t.is(pts.length, 122)
68
+ t.notThrows(() => geom3.validate(obs))
69
+ t.is(pts.length, 192)
63
70
  })
@@ -11,6 +11,7 @@ test('extrudeRotate: (defaults) extruding of a geom2 produces an expected geom3'
11
11
 
12
12
  const geometry3 = extrudeRotate({ }, geometry2)
13
13
  const pts = geom3.toPoints(geometry3)
14
+ t.notThrows.skip(() => geom3.validate(geometry3))
14
15
  t.is(pts.length, 96)
15
16
  })
16
17
 
@@ -29,21 +30,24 @@ test('extrudeRotate: (angle) extruding of a geom2 produces an expected geom3', (
29
30
  [[26, -4.898587196589413e-16, -8], [7.0710678118654755, 7.071067811865475, -8], [18.38477631085024, 18.384776310850235, -8]],
30
31
  [[26, 4.898587196589413e-16, 8], [26, -4.898587196589413e-16, -8], [18.38477631085024, 18.384776310850235, -8]],
31
32
  [[26, 4.898587196589413e-16, 8], [18.38477631085024, 18.384776310850235, -8], [18.38477631085024, 18.384776310850235, 8]],
32
- [[18.38477631085024, 18.384776310850235, 7.999999999999998], [18.38477631085024, 18.384776310850235, -8],
33
- [7.071067811865478, 7.071067811865474, -8], [7.071067811865476, 7.071067811865475, 8]],
34
- [[10, 4.898587196589411e-16, 8], [10, -4.898587196589413e-16, -8],
35
- [26, -4.898587196589412e-16, -8], [26, 4.898587196589414e-16, 8]]
33
+ [[7.071067811865476, 7.0710678118654755, -8], [7.071067811865476, 7.0710678118654755, 8], [18.384776310850242, 18.384776310850235, 8]],
34
+ [[18.384776310850242, 18.384776310850235, 8], [18.384776310850242, 18.384776310850235, -8], [7.071067811865476, 7.0710678118654755, -8]],
35
+ [[26, 4.898587196589413e-16, 8], [10, 4.898587196589413e-16, 8], [10, -4.898587196589413e-16, -8]],
36
+ [[10, -4.898587196589413e-16, -8], [26, -4.898587196589413e-16, -8], [26, 4.898587196589413e-16, 8]]
36
37
  ]
37
- t.is(pts.length, 10)
38
+ t.notThrows(() => geom3.validate(geometry3))
39
+ t.is(pts.length, 12)
38
40
  t.true(comparePolygonsAsPoints(pts, exp))
39
41
 
40
42
  geometry3 = extrudeRotate({ segments: 4, angle: -250 * 0.017453292519943295 }, geometry2)
41
43
  pts = geom3.toPoints(geometry3)
42
- t.is(pts.length, 26)
44
+ t.notThrows(() => geom3.validate(geometry3))
45
+ t.is(pts.length, 28)
43
46
 
44
47
  geometry3 = extrudeRotate({ segments: 4, angle: 250 * 0.017453292519943295 }, geometry2)
45
48
  pts = geom3.toPoints(geometry3)
46
- t.is(pts.length, 26)
49
+ t.notThrows(() => geom3.validate(geometry3))
50
+ t.is(pts.length, 28)
47
51
  })
48
52
 
49
53
  test('extrudeRotate: (startAngle) extruding of a geom2 produces an expected geom3', (t) => {
@@ -57,6 +61,7 @@ test('extrudeRotate: (startAngle) extruding of a geom2 produces an expected geom
57
61
  [18.38477631085024, 18.384776310850235, 8],
58
62
  [-11.803752993228215, 23.166169628897567, 8]
59
63
  ]
64
+ t.notThrows.skip(() => geom3.validate(geometry3))
60
65
  t.is(pts.length, 40)
61
66
  t.true(comparePoints(pts[0], exp))
62
67
 
@@ -67,6 +72,7 @@ test('extrudeRotate: (startAngle) extruding of a geom2 produces an expected geom
67
72
  [18.38477631085024, -18.384776310850235, 8],
68
73
  [23.166169628897567, 11.803752993228215, 8]
69
74
  ]
75
+ t.notThrows.skip(() => geom3.validate(geometry3))
70
76
  t.is(pts.length, 40)
71
77
  t.true(comparePoints(pts[0], exp))
72
78
  })
@@ -77,22 +83,26 @@ test('extrudeRotate: (segments) extruding of a geom2 produces an expected geom3'
77
83
  // test segments
78
84
  let geometry3 = extrudeRotate({ segments: 4 }, geometry2)
79
85
  let pts = geom3.toPoints(geometry3)
86
+ t.notThrows.skip(() => geom3.validate(geometry3))
80
87
  t.is(pts.length, 32)
81
88
 
82
89
  geometry3 = extrudeRotate({ segments: 64 }, geometry2)
83
90
  pts = geom3.toPoints(geometry3)
91
+ t.notThrows.skip(() => geom3.validate(geometry3))
84
92
  t.is(pts.length, 512)
85
93
 
86
94
  // test overlapping edges
87
95
  geometry2 = geom2.fromPoints([[0, 0], [2, 1], [1, 2], [1, 3], [3, 4], [0, 5]])
88
96
  geometry3 = extrudeRotate({ segments: 8 }, geometry2)
89
97
  pts = geom3.toPoints(geometry3)
98
+ t.notThrows.skip(() => geom3.validate(geometry3))
90
99
  t.is(pts.length, 64)
91
100
 
92
101
  // test overlapping edges that produce hollow shape
93
102
  geometry2 = geom2.fromPoints([[30, 0], [30, 60], [0, 60], [0, 50], [10, 40], [10, 30], [0, 20], [0, 10], [10, 0]])
94
103
  geometry3 = extrudeRotate({ segments: 8 }, geometry2)
95
104
  pts = geom3.toPoints(geometry3)
105
+ t.notThrows.skip(() => geom3.validate(geometry3))
96
106
  t.is(pts.length, 80)
97
107
  })
98
108
 
@@ -107,16 +117,13 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
107
117
  [[7, -4.898587196589413e-16, -8], [4.898587196589413e-16, -2.999519565323715e-32, -8], [9.184850993605148e-16, 7, -8]],
108
118
  [[7, 4.898587196589413e-16, 8], [7, -4.898587196589413e-16, -8], [9.184850993605148e-16, 7, -8]],
109
119
  [[7, 4.898587196589413e-16, 8], [9.184850993605148e-16, 7, -8], [-6.123233995736767e-17, 7, 8]],
110
- [
111
- [-4.898587196589414e-16, 0, 8], [-6.123233995736777e-17, 6.999999999999999, 8],
112
- [9.18485099360515e-16, 7.000000000000001, -8], [4.898587196589413e-16, 0, -8]
113
- ],
114
- [
115
- [7, 4.898587196589413e-16, 8], [0, 4.898587196589413e-16, 8],
116
- [0, -4.898587196589413e-16, -8], [7, -4.898587196589413e-16, -8]
117
- ]
120
+ [[4.898587196589413e-16, -2.999519565323715e-32, -8], [-4.898587196589413e-16, 2.999519565323715e-32, 8], [-6.123233995736767e-17, 7, 8]],
121
+ [[-6.123233995736767e-17, 7, 8], [9.184850993605148e-16, 7, -8], [4.898587196589413e-16, -2.999519565323715e-32, -8]],
122
+ [[7, 4.898587196589413e-16, 8], [0, 4.898587196589413e-16, 8], [0, -4.898587196589413e-16, -8]],
123
+ [[0, -4.898587196589413e-16, -8], [7, -4.898587196589413e-16, -8], [7, 4.898587196589413e-16, 8]]
118
124
  ]
119
- t.is(pts.length, 6)
125
+ t.notThrows.skip(() => geom3.validate(obs))
126
+ t.is(pts.length, 8)
120
127
  t.true(comparePolygonsAsPoints(pts, exp))
121
128
 
122
129
  // overlap of Y axis; larger number of - points
@@ -137,18 +144,15 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
137
144
  [[0.7071067811865472, 0.7071067811865478, 8], [1.414213562373095, 1.4142135623730951, 4], [-1.2246467991473532e-16, 2, 4]],
138
145
  [[0.7071067811865472, 0.7071067811865478, 8], [-1.2246467991473532e-16, 2, 4], [-4.286263797015736e-16, 1, 8]],
139
146
  [[-3.4638242249419727e-16, 3.4638242249419736e-16, 8], [0.7071067811865472, 0.7071067811865478, 8], [-4.286263797015736e-16, 1, 8]],
140
- [
141
- [-4.898587196589412e-16, 0, 8], [-4.2862637970157346e-16, 0.9999999999999998, 8],
142
- [-1.2246467991473475e-16, 2.0000000000000004, 3.9999999999999964], [5.510910596163092e-16, 1.0000000000000004, -8],
143
- [4.898587196589414e-16, 0, -8]
144
- ],
145
- [
146
- [0, -4.898587196589413e-16, -8.000000000000002], [1.0000000000000027, -4.898587196589413e-16, -8.000000000000002],
147
- [2.000000000000001, 2.449293598294702e-16, 3.9999999999999964], [1.0000000000000004, 4.898587196589411e-16, 8],
148
- [0, 4.898587196589411e-16, 8]
149
- ]
147
+ [[5.51091059616309e-16, 1, -8], [4.898587196589413e-16, -2.999519565323715e-32, -8], [-4.898587196589415e-16, 2.9995195653237163e-32, 8]],
148
+ [[-4.898587196589415e-16, 2.9995195653237163e-32, 8], [-4.286263797015738e-16, 1, 8], [-1.2246467991473544e-16, 2, 4]],
149
+ [[-1.2246467991473544e-16, 2, 4], [5.51091059616309e-16, 1, -8], [-4.898587196589415e-16, 2.9995195653237163e-32, 8]],
150
+ [[0, 4.898587196589413e-16, 8], [0, -4.898587196589413e-16, -8], [1, -4.898587196589413e-16, -8]],
151
+ [[2, 2.4492935982947064e-16, 4], [1, 4.898587196589413e-16, 8], [0, 4.898587196589413e-16, 8]],
152
+ [[0, 4.898587196589413e-16, 8], [1, -4.898587196589413e-16, -8], [2, 2.4492935982947064e-16, 4]]
150
153
  ]
151
- t.is(pts.length, 14)
154
+ t.notThrows.skip(() => geom3.validate(obs))
155
+ t.is(pts.length, 18)
152
156
  t.true(comparePolygonsAsPoints(pts, exp))
153
157
  })
154
158
 
@@ -16,14 +16,14 @@ test('project (defaults)', (t) => {
16
16
 
17
17
  const results = project({ }, geometry0, geometry1, geometry2, geometry3, geometry4)
18
18
  t.is(results.length, 5)
19
- t.true(geom2.isA(results[0]))
20
- t.true(geom2.isA(results[1]))
19
+ t.notThrows(() => geom2.validate(results[0]))
20
+ t.notThrows(() => geom2.validate(results[1]))
21
21
  t.is(results[2], geometry2)
22
22
  t.is(results[3], geometry3)
23
23
  t.is(results[4], geometry4)
24
24
 
25
25
  const result = project({ }, torus({ outerSegments: 4 }))
26
- t.true(geom2.isA(result))
26
+ t.notThrows(() => geom2.validate(result))
27
27
  const pts = geom2.toPoints(result)
28
28
  const exp = [
29
29
  [0, -2.9999933333333333],
@@ -40,7 +40,7 @@ test('project (defaults)', (t) => {
40
40
 
41
41
  test('project (X and Y axis)', (t) => {
42
42
  let result = project({ axis: [1, 0, 0], origin: [1, 0, 0] }, torus({ outerSegments: 4 }))
43
- t.true(geom2.isA(result))
43
+ t.notThrows(() => geom2.validate(result))
44
44
  let pts = geom2.toPoints(result)
45
45
  let exp = [
46
46
  [-1.0000200000000001, -3.999986666666667],
@@ -81,7 +81,7 @@ test('project (X and Y axis)', (t) => {
81
81
  t.true(comparePoints(pts, exp))
82
82
 
83
83
  result = project({ axis: [0, 1, 0], origin: [0, -1, 0] }, torus({ outerSegments: 4 }))
84
- t.true(geom2.isA(result))
84
+ t.notThrows(() => geom2.validate(result))
85
85
  pts = geom2.toPoints(result)
86
86
  exp = [
87
87
  [3.999986666666667, -1.0000200000000001],
@@ -23,10 +23,13 @@ const calculatePlane = (slice) => {
23
23
  let farthestEdge
24
24
  let distance = 0
25
25
  edges.forEach((edge) => {
26
- const d = vec3.squaredDistance(midpoint, edge[0])
27
- if (d > distance) {
28
- farthestEdge = edge
29
- distance = d
26
+ // Make sure that the farthest edge is not a self-edge
27
+ if (!vec3.equals(edge[0], edge[1])) {
28
+ const d = vec3.squaredDistance(midpoint, edge[0])
29
+ if (d > distance) {
30
+ farthestEdge = edge
31
+ distance = d
32
+ }
30
33
  }
31
34
  })
32
35
  // find the before edge
@@ -0,0 +1,47 @@
1
+ const vec3 = require('../../../maths/vec3')
2
+
3
+ /*
4
+ * Mend gaps in a 2D slice to make it a closed polygon
5
+ */
6
+ const repairSlice = (slice) => {
7
+ if (!slice.edges) return slice
8
+ const vertexMap = {} // string key to vertex map
9
+ const edgeCount = {} // count of (in - out) edges
10
+ slice.edges.forEach((edge) => {
11
+ const inKey = edge[0].toString()
12
+ const outKey = edge[1].toString()
13
+ vertexMap[inKey] = edge[0]
14
+ vertexMap[outKey] = edge[1]
15
+ edgeCount[inKey] = (edgeCount[inKey] || 0) + 1 // in
16
+ edgeCount[outKey] = (edgeCount[outKey] || 0) - 1 // out
17
+ })
18
+ // find vertices which are missing in or out edges
19
+ const missingIn = Object.keys(edgeCount).filter((e) => edgeCount[e] < 0)
20
+ const missingOut = Object.keys(edgeCount).filter((e) => edgeCount[e] > 0)
21
+ // pairwise distance of bad vertices
22
+ missingIn.forEach((key1) => {
23
+ const v1 = vertexMap[key1]
24
+ // find the closest vertex that is missing an out edge
25
+ let bestDistance = Infinity
26
+ let bestReplacement
27
+ missingOut.forEach((key2) => {
28
+ const v2 = vertexMap[key2]
29
+ const distance = Math.hypot(v1[0] - v2[0], v1[1] - v2[1])
30
+ if (distance < bestDistance) {
31
+ bestDistance = distance
32
+ bestReplacement = v2
33
+ }
34
+ })
35
+ console.warn(`repairSlice: repairing vertex gap ${v1} to ${bestReplacement} distance ${bestDistance}`)
36
+ // merge broken vertices
37
+ slice.edges.forEach((edge) => {
38
+ if (edge[0].toString() === key1) edge[0] = bestReplacement
39
+ if (edge[1].toString() === key1) edge[1] = bestReplacement
40
+ })
41
+ })
42
+ // Remove self-edges
43
+ slice.edges = slice.edges.filter((e) => !vec3.equals(e[0], e[1]))
44
+ return slice
45
+ }
46
+
47
+ module.exports = repairSlice
@@ -1,21 +1,6 @@
1
- const vec3 = require('../../../maths/vec3')
2
-
3
- const geom3 = require('../../../geometries/geom3')
4
1
  const poly3 = require('../../../geometries/poly3')
5
-
6
- const intersectGeom3Sub = require('../../booleans/intersectGeom3Sub')
7
-
8
- const calculatePlane = require('./calculatePlane')
9
-
10
- const toPolygon3D = (vector, edge) => {
11
- const points = [
12
- vec3.subtract(vec3.create(), edge[0], vector),
13
- vec3.subtract(vec3.create(), edge[1], vector),
14
- vec3.add(vec3.create(), edge[1], vector),
15
- vec3.add(vec3.create(), edge[0], vector)
16
- ]
17
- return poly3.fromPoints(points)
18
- }
2
+ const earcut = require('../earcut')
3
+ const PolygonHierarchy = require('../earcut/polygonHierarchy')
19
4
 
20
5
  /**
21
6
  * Return a list of polygons which are enclosed by the slice.
@@ -24,52 +9,31 @@ const toPolygon3D = (vector, edge) => {
24
9
  * @alias module:modeling/extrusions/slice.toPolygons
25
10
  */
26
11
  const toPolygons = (slice) => {
27
- const splane = calculatePlane(slice)
28
-
29
- // find the midpoint of the slice, which will lie on the plane by definition
30
- const edges = slice.edges
31
- const midpoint = edges.reduce((point, edge) => vec3.add(vec3.create(), point, edge[0]), vec3.create())
32
- vec3.scale(midpoint, midpoint, 1 / edges.length)
33
-
34
- // find the farthest edge from the midpoint, which will be on an outside edge
35
- let farthestEdge = [[NaN, NaN, NaN], [NaN, NaN, NaN]]
36
- let distance = 0
37
- edges.forEach((edge) => {
38
- const d = vec3.squaredDistance(midpoint, edge[0])
39
- if (d > distance) {
40
- farthestEdge = edge
41
- distance = d
12
+ const hierarchy = new PolygonHierarchy(slice)
13
+
14
+ const polygons = []
15
+ hierarchy.roots.forEach(({ solid, holes }) => {
16
+ // hole indices
17
+ let index = solid.length
18
+ const holesIndex = []
19
+ holes.forEach((hole, i) => {
20
+ holesIndex.push(index)
21
+ index += hole.length
22
+ })
23
+
24
+ // compute earcut triangulation for each solid
25
+ const vertices = [solid, ...holes].flat()
26
+ const data = vertices.flat()
27
+ // Get original 3D vertex by index
28
+ const getVertex = (i) => hierarchy.to3D(vertices[i])
29
+ const indices = earcut(data, holesIndex)
30
+ for (let i = 0; i < indices.length; i += 3) {
31
+ // Map back to original vertices
32
+ const tri = indices.slice(i, i + 3).map(getVertex)
33
+ polygons.push(poly3.fromPointsAndPlane(tri, hierarchy.plane))
42
34
  }
43
35
  })
44
36
 
45
- // create one LARGE polygon to encompass the side, i.e. base
46
- const direction = vec3.subtract(vec3.create(), farthestEdge[0], midpoint)
47
- const perpendicular = vec3.cross(vec3.create(), splane, direction)
48
-
49
- const p1 = vec3.add(vec3.create(), midpoint, direction)
50
- vec3.add(p1, p1, direction)
51
- const p2 = vec3.add(vec3.create(), midpoint, perpendicular)
52
- vec3.add(p2, p2, perpendicular)
53
- const p3 = vec3.subtract(vec3.create(), midpoint, direction)
54
- vec3.subtract(p3, p3, direction)
55
- const p4 = vec3.subtract(vec3.create(), midpoint, perpendicular)
56
- vec3.subtract(p4, p4, perpendicular)
57
- const poly1 = poly3.fromPoints([p1, p2, p3, p4])
58
- const base = geom3.create([poly1])
59
-
60
- const wallPolygons = edges.map((edge) => toPolygon3D(splane, edge))
61
- const walls = geom3.create(wallPolygons)
62
-
63
- // make an intersection of the base and the walls, creating... a set of polygons!
64
- const geometry3 = intersectGeom3Sub(base, walls)
65
-
66
- // return only those polygons from the base
67
- let polygons = geom3.toPolygons(geometry3)
68
- polygons = polygons.filter((polygon) => {
69
- const a = vec3.angle(splane, poly3.plane(polygon))
70
- // walls should be PI / 2 radians rotated from the base
71
- return Math.abs(a) < (Math.PI / 90)
72
- })
73
37
  return polygons
74
38
  }
75
39
 
@@ -16,12 +16,14 @@ test('hull (single, geom2)', (t) => {
16
16
  let obs = hull(geometry)
17
17
  let pts = geom2.toPoints(obs)
18
18
 
19
+ t.notThrows(() => geom2.validate(geometry))
19
20
  t.is(pts.length, 0)
20
21
 
21
22
  geometry = geom2.fromPoints([[5, 5], [-5, 5], [-5, -5], [5, -5]])
22
23
  obs = hull(geometry)
23
24
  pts = geom2.toPoints(obs)
24
25
 
26
+ t.notThrows(() => geom2.validate(geometry))
25
27
  t.is(pts.length, 4)
26
28
 
27
29
  // convex C shape
@@ -40,6 +42,7 @@ test('hull (single, geom2)', (t) => {
40
42
  obs = hull(geometry)
41
43
  pts = geom2.toPoints(obs)
42
44
 
45
+ t.notThrows(() => geom2.validate(geometry))
43
46
  t.is(pts.length, 7)
44
47
  })
45
48
 
@@ -66,23 +69,27 @@ test('hull (multiple, overlapping, geom2)', (t) => {
66
69
  let obs = hull(geometry1, geometry1)
67
70
  let pts = geom2.toPoints(obs)
68
71
 
72
+ t.notThrows(() => geom2.validate(obs))
69
73
  t.is(pts.length, 4)
70
74
 
71
75
  // one inside another
72
76
  obs = hull(geometry1, geometry2)
73
77
  pts = geom2.toPoints(obs)
74
78
 
79
+ t.notThrows(() => geom2.validate(obs))
75
80
  t.is(pts.length, 4)
76
81
 
77
82
  // one overlapping another
78
83
  obs = hull(geometry1, geometry3)
79
84
  pts = geom2.toPoints(obs)
80
85
 
86
+ t.notThrows(() => geom2.validate(obs))
81
87
  t.is(pts.length, 8)
82
88
 
83
89
  obs = hull(geometry2, geometry4)
84
90
  pts = geom2.toPoints(obs)
85
91
 
92
+ t.notThrows(() => geom2.validate(obs))
86
93
  t.is(pts.length, 7)
87
94
  })
88
95
 
@@ -108,22 +115,27 @@ test('hull (multiple, various, geom2)', (t) => {
108
115
 
109
116
  let obs = hull(geometry1, geometry2)
110
117
  let pts = geom2.toPoints(obs)
118
+ t.notThrows(() => geom2.validate(obs))
111
119
  t.is(pts.length, 5)
112
120
 
113
121
  obs = hull(geometry1, geometry3)
114
122
  pts = geom2.toPoints(obs)
123
+ t.notThrows(() => geom2.validate(obs))
115
124
  t.is(pts.length, 5)
116
125
 
117
126
  obs = hull(geometry2, geometry3)
118
127
  pts = geom2.toPoints(obs)
128
+ t.notThrows(() => geom2.validate(obs))
119
129
  t.is(pts.length, 5)
120
130
 
121
131
  obs = hull(geometry1, geometry2, geometry3)
122
132
  pts = geom2.toPoints(obs)
133
+ t.notThrows(() => geom2.validate(obs))
123
134
  t.is(pts.length, 6)
124
135
 
125
136
  obs = hull(geometry5, geometry4)
126
137
  pts = geom2.toPoints(obs)
138
+ t.notThrows(() => geom2.validate(obs))
127
139
  t.is(pts.length, 8)
128
140
  })
129
141
 
@@ -133,6 +145,7 @@ test('hull (single, path2)', (t) => {
133
145
  let obs = hull(geometry)
134
146
  let pts = path2.toPoints(obs)
135
147
 
148
+ t.notThrows(() => path2.validate(obs))
136
149
  t.is(pts.length, 0)
137
150
 
138
151
  geometry = path2.fromPoints({}, [[0, 0], [5, 0], [5, 10], [4, 1]])
@@ -140,6 +153,7 @@ test('hull (single, path2)', (t) => {
140
153
  obs = hull(geometry)
141
154
  pts = path2.toPoints(obs)
142
155
 
156
+ t.notThrows(() => path2.validate(obs))
143
157
  t.is(pts.length, 3)
144
158
  })
145
159
 
@@ -165,22 +179,27 @@ test('hull (multiple, various, path2)', (t) => {
165
179
 
166
180
  let obs = hull(geometry1, geometry2)
167
181
  let pts = path2.toPoints(obs)
182
+ t.notThrows(() => path2.validate(obs))
168
183
  t.is(pts.length, 5)
169
184
 
170
185
  obs = hull(geometry1, geometry3)
171
186
  pts = path2.toPoints(obs)
187
+ t.notThrows(() => path2.validate(obs))
172
188
  t.is(pts.length, 5)
173
189
 
174
190
  obs = hull(geometry2, geometry3)
175
191
  pts = path2.toPoints(obs)
192
+ t.notThrows(() => path2.validate(obs))
176
193
  t.is(pts.length, 5)
177
194
 
178
195
  obs = hull(geometry1, geometry2, geometry3)
179
196
  pts = path2.toPoints(obs)
197
+ t.notThrows(() => path2.validate(obs))
180
198
  t.is(pts.length, 6)
181
199
 
182
200
  obs = hull(geometry5, geometry4)
183
201
  pts = path2.toPoints(obs)
202
+ t.notThrows(() => path2.validate(obs))
184
203
  t.is(pts.length, 8)
185
204
  })
186
205
 
@@ -190,6 +209,7 @@ test('hull (single, geom3)', (t) => {
190
209
  let obs = hull(geometry)
191
210
  let pts = geom3.toPoints(obs)
192
211
 
212
+ t.notThrows(() => geom3.validate(obs))
193
213
  t.is(pts.length, 0)
194
214
 
195
215
  geometry = sphere({ radius: 2, segments: 8 })
@@ -197,6 +217,7 @@ test('hull (single, geom3)', (t) => {
197
217
  obs = hull(geometry)
198
218
  pts = geom3.toPoints(obs)
199
219
 
220
+ t.notThrows.skip(() => geom3.validate(obs))
200
221
  t.is(pts.length, 32)
201
222
  })
202
223
 
@@ -214,6 +235,7 @@ test('hull (multiple, geom3)', (t) => {
214
235
  [[1, -1, 1], [-1, -1, 1], [-1, -1, -1], [1, -1, -1]]
215
236
  ]
216
237
 
238
+ t.notThrows(() => geom3.validate(obs))
217
239
  t.is(pts.length, 6)
218
240
  t.true(comparePolygonsAsPoints(pts, exp))
219
241
 
@@ -236,6 +258,7 @@ test('hull (multiple, geom3)', (t) => {
236
258
  [[1, 1, -1], [6.5, 6.5, 3.5], [6.5, 3.5, 3.5], [1, -1, -1]]
237
259
  ]
238
260
 
261
+ t.notThrows(() => geom3.validate(obs))
239
262
  t.is(pts.length, 12)
240
263
  t.true(comparePolygonsAsPoints(pts, exp))
241
264
  })
@@ -248,6 +271,6 @@ test('hull (multiple, overlapping, geom3)', (t) => {
248
271
  const obs = hull(geometry1, geometry2, geometry3)
249
272
  const pts = geom3.toPoints(obs)
250
273
 
251
- // t.is(pts.length, 160)
274
+ t.notThrows(() => geom3.validate(obs))
252
275
  t.is(pts.length, 92)
253
276
  })
@@ -12,12 +12,14 @@ test('hullChain (two, geom2)', (t) => {
12
12
  let obs = hullChain(geometry1, geometry1)
13
13
  let pts = geom2.toPoints(obs)
14
14
 
15
+ t.notThrows(() => geom2.validate(obs))
15
16
  t.is(pts.length, 4)
16
17
 
17
18
  // different
18
19
  obs = hullChain(geometry1, geometry2)
19
20
  pts = geom2.toPoints(obs)
20
21
 
22
+ t.notThrows(() => geom2.validate(obs))
21
23
  t.is(pts.length, 6)
22
24
  })
23
25
 
@@ -31,16 +33,16 @@ test('hullChain (three, geom2)', (t) => {
31
33
  let pts = geom2.toPoints(obs)
32
34
 
33
35
  // the sides change based on the bestplane chosen in trees/Node.js
36
+ t.notThrows(() => geom2.validate(obs))
34
37
  t.is(pts.length, 10)
35
- // t.is(pts.length, 11)
36
38
 
37
39
  // closed
38
40
  obs = hullChain(geometry1, geometry2, geometry3, geometry1)
39
41
  pts = geom2.toPoints(obs)
40
42
 
41
43
  // the sides change based on the bestplane chosen in trees/Node.js
44
+ t.notThrows(() => geom2.validate(obs))
42
45
  t.is(pts.length, 10)
43
- // t.is(pts.length, 13)
44
46
  })
45
47
 
46
48
  test('hullChain (three, geom3)', (t) => {
@@ -73,13 +75,13 @@ test('hullChain (three, geom3)', (t) => {
73
75
  let obs = hullChain(geometry1, geometry2, geometry3)
74
76
  let pts = geom3.toPoints(obs)
75
77
 
76
- // t.is(pts.length, 27)
78
+ t.notThrows.skip(() => geom3.validate(obs))
77
79
  t.is(pts.length, 23)
78
80
 
79
81
  // closed
80
82
  obs = hullChain(geometry1, geometry2, geometry3, geometry1)
81
83
  pts = geom3.toPoints(obs)
82
84
 
83
- // t.is(pts.length, 45)
85
+ t.notThrows.skip(() => geom3.validate(obs))
84
86
  t.is(pts.length, 28)
85
87
  })
@@ -10,7 +10,7 @@ test('hullPath2', (t) => {
10
10
  const geometry2 = path2.fromPoints({ closed }, [[0, 0], [4, -4], [4, 4]])
11
11
 
12
12
  const obs = hullPath2(geometry1, geometry2)
13
- t.true(path2.isA(obs))
13
+ t.notThrows(() => path2.validate(obs))
14
14
  const pts = path2.toPoints(obs)
15
15
  t.is(pts.length, 4)
16
16
  })
@@ -28,6 +28,7 @@ test('generalize: generalize of a geom3 produces an expected geom3', (t) => {
28
28
  [[-1.5707963267948966, -0.7853981633974483, 3.141592653589793], [1.5707963267948966, -0.7853981633974483, 3.141592653589793],
29
29
  [1.5707963267948966, 0.7853981633974483, 3.141592653589793], [-1.5707963267948966, 0.7853981633974483, 3.141592653589793]]
30
30
  ]
31
+ t.notThrows(() => geom3.validate(result))
31
32
  t.true(comparePolygonsAsPoints(pts, exp))
32
33
 
33
34
  // apply snap only
@@ -47,6 +48,7 @@ test('generalize: generalize of a geom3 produces an expected geom3', (t) => {
47
48
  [[-1.5707910908071407, -0.7854138713607164, 3.1415821816142815], [1.5707910908071407, -0.7854138713607164, 3.1415821816142815],
48
49
  [1.5707910908071407, 0.7854138713607164, 3.1415821816142815], [-1.5707910908071407, 0.7854138713607164, 3.1415821816142815]]
49
50
  ]
51
+ t.notThrows(() => geom3.validate(result))
50
52
  t.true(comparePolygonsAsPoints(pts, exp))
51
53
 
52
54
  // apply triangulate only
@@ -78,6 +80,7 @@ test('generalize: generalize of a geom3 produces an expected geom3', (t) => {
78
80
  [[-1.5707963267948966, -0.7853981633974483, 3.141592653589793], [1.5707963267948966, 0.7853981633974483, 3.141592653589793],
79
81
  [-1.5707963267948966, 0.7853981633974483, 3.141592653589793]]
80
82
  ]
83
+ t.notThrows(() => geom3.validate(result))
81
84
  t.true(comparePolygonsAsPoints(pts, exp))
82
85
 
83
86
  const geometry2 = result // save the triangles for another test
@@ -99,6 +102,7 @@ test('generalize: generalize of a geom3 produces an expected geom3', (t) => {
99
102
  [[-1.5707963267948966, -0.7853981633974483, 3.141592653589793], [1.5707963267948966, -0.7853981633974483, 3.141592653589793],
100
103
  [1.5707963267948966, 0.7853981633974483, 3.141592653589793], [-1.5707963267948966, 0.7853981633974483, 3.141592653589793]]
101
104
  ]
105
+ t.notThrows(() => geom3.validate(result))
102
106
  t.true(comparePolygonsAsPoints(pts, exp))
103
107
 
104
108
  // apply repairs only (triangles)
@@ -130,6 +134,7 @@ test('generalize: generalize of a geom3 produces an expected geom3', (t) => {
130
134
  [[-1.5707963267948966, -0.7853981633974483, 3.141592653589793], [1.5707963267948966, 0.7853981633974483, 3.141592653589793],
131
135
  [-1.5707963267948966, 0.7853981633974483, 3.141592653589793]]
132
136
  ]
137
+ t.notThrows(() => geom3.validate(result))
133
138
  t.true(comparePolygonsAsPoints(pts, exp))
134
139
  })
135
140
 
@@ -190,5 +195,6 @@ test('generalize: generalize of a geom3 with T junctions produces an expected ge
190
195
  [[0, 1, 1], [-1, 1, 1], [0, 0, 1]],
191
196
  [[-1, 1, 1], [-1, 0, 1], [0, 0, 1]]
192
197
  ]
198
+ t.notThrows(() => geom3.validate(result))
193
199
  t.true(comparePolygonsAsPoints(pts, exp))
194
200
  })