@jscad/modeling 2.8.0 → 2.9.0

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 (26) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/jscad-modeling.min.js +159 -132
  3. package/package.json +2 -2
  4. package/src/geometries/poly3/invert.js +7 -1
  5. package/src/operations/expansions/expand.test.js +1 -1
  6. package/src/operations/expansions/expandShell.js +2 -2
  7. package/src/operations/extrusions/earcut/assignHoles.js +87 -0
  8. package/src/operations/extrusions/earcut/assignHoles.test.js +28 -0
  9. package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
  10. package/src/operations/extrusions/earcut/index.js +252 -0
  11. package/src/operations/extrusions/earcut/linkedList.js +58 -0
  12. package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
  13. package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
  14. package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
  15. package/src/operations/extrusions/earcut/triangle.js +16 -0
  16. package/src/operations/extrusions/extrudeFromSlices.js +10 -3
  17. package/src/operations/extrusions/extrudeFromSlices.test.js +33 -23
  18. package/src/operations/extrusions/extrudeLinear.js +4 -3
  19. package/src/operations/extrusions/extrudeLinear.test.js +54 -28
  20. package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
  21. package/src/operations/extrusions/extrudeRectangular.test.js +7 -7
  22. package/src/operations/extrusions/extrudeRotate.test.js +19 -27
  23. package/src/operations/extrusions/slice/calculatePlane.js +7 -4
  24. package/src/operations/extrusions/slice/repairSlice.js +47 -0
  25. package/src/operations/extrusions/slice/toPolygons.js +24 -60
  26. package/src/primitives/torus.test.js +1 -1
@@ -29,21 +29,21 @@ test('extrudeRotate: (angle) extruding of a geom2 produces an expected geom3', (
29
29
  [[26, -4.898587196589413e-16, -8], [7.0710678118654755, 7.071067811865475, -8], [18.38477631085024, 18.384776310850235, -8]],
30
30
  [[26, 4.898587196589413e-16, 8], [26, -4.898587196589413e-16, -8], [18.38477631085024, 18.384776310850235, -8]],
31
31
  [[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]]
32
+ [[7.071067811865476, 7.0710678118654755, -8], [7.071067811865476, 7.0710678118654755, 8], [18.384776310850242, 18.384776310850235, 8]],
33
+ [[18.384776310850242, 18.384776310850235, 8], [18.384776310850242, 18.384776310850235, -8], [7.071067811865476, 7.0710678118654755, -8]],
34
+ [[26, 4.898587196589413e-16, 8], [10, 4.898587196589413e-16, 8], [10, -4.898587196589413e-16, -8]],
35
+ [[10, -4.898587196589413e-16, -8], [26, -4.898587196589413e-16, -8], [26, 4.898587196589413e-16, 8]]
36
36
  ]
37
- t.is(pts.length, 10)
37
+ t.is(pts.length, 12)
38
38
  t.true(comparePolygonsAsPoints(pts, exp))
39
39
 
40
40
  geometry3 = extrudeRotate({ segments: 4, angle: -250 * 0.017453292519943295 }, geometry2)
41
41
  pts = geom3.toPoints(geometry3)
42
- t.is(pts.length, 26)
42
+ t.is(pts.length, 28)
43
43
 
44
44
  geometry3 = extrudeRotate({ segments: 4, angle: 250 * 0.017453292519943295 }, geometry2)
45
45
  pts = geom3.toPoints(geometry3)
46
- t.is(pts.length, 26)
46
+ t.is(pts.length, 28)
47
47
  })
48
48
 
49
49
  test('extrudeRotate: (startAngle) extruding of a geom2 produces an expected geom3', (t) => {
@@ -107,16 +107,12 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
107
107
  [[7, -4.898587196589413e-16, -8], [4.898587196589413e-16, -2.999519565323715e-32, -8], [9.184850993605148e-16, 7, -8]],
108
108
  [[7, 4.898587196589413e-16, 8], [7, -4.898587196589413e-16, -8], [9.184850993605148e-16, 7, -8]],
109
109
  [[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
- ]
110
+ [[4.898587196589413e-16, -2.999519565323715e-32, -8], [-4.898587196589413e-16, 2.999519565323715e-32, 8], [-6.123233995736767e-17, 7, 8]],
111
+ [[-6.123233995736767e-17, 7, 8], [9.184850993605148e-16, 7, -8], [4.898587196589413e-16, -2.999519565323715e-32, -8]],
112
+ [[7, 4.898587196589413e-16, 8], [0, 4.898587196589413e-16, 8], [0, -4.898587196589413e-16, -8]],
113
+ [[0, -4.898587196589413e-16, -8], [7, -4.898587196589413e-16, -8], [7, 4.898587196589413e-16, 8]]
118
114
  ]
119
- t.is(pts.length, 6)
115
+ t.is(pts.length, 8)
120
116
  t.true(comparePolygonsAsPoints(pts, exp))
121
117
 
122
118
  // overlap of Y axis; larger number of - points
@@ -137,18 +133,14 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
137
133
  [[0.7071067811865472, 0.7071067811865478, 8], [1.414213562373095, 1.4142135623730951, 4], [-1.2246467991473532e-16, 2, 4]],
138
134
  [[0.7071067811865472, 0.7071067811865478, 8], [-1.2246467991473532e-16, 2, 4], [-4.286263797015736e-16, 1, 8]],
139
135
  [[-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
- ]
136
+ [[5.51091059616309e-16, 1, -8], [4.898587196589413e-16, -2.999519565323715e-32, -8], [-4.898587196589415e-16, 2.9995195653237163e-32, 8]],
137
+ [[-4.898587196589415e-16, 2.9995195653237163e-32, 8], [-4.286263797015738e-16, 1, 8], [-1.2246467991473544e-16, 2, 4]],
138
+ [[-1.2246467991473544e-16, 2, 4], [5.51091059616309e-16, 1, -8], [-4.898587196589415e-16, 2.9995195653237163e-32, 8]],
139
+ [[0, 4.898587196589413e-16, 8], [0, -4.898587196589413e-16, -8], [1, -4.898587196589413e-16, -8]],
140
+ [[2, 2.4492935982947064e-16, 4], [1, 4.898587196589413e-16, 8], [0, 4.898587196589413e-16, 8]],
141
+ [[0, 4.898587196589413e-16, 8], [1, -4.898587196589413e-16, -8], [2, 2.4492935982947064e-16, 4]]
150
142
  ]
151
- t.is(pts.length, 14)
143
+ t.is(pts.length, 18)
152
144
  t.true(comparePolygonsAsPoints(pts, exp))
153
145
  })
154
146
 
@@ -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
 
@@ -30,7 +30,7 @@ test('torus (Simple options)', (t) => {
30
30
  test('torus (complex options)', (t) => {
31
31
  const obs = torus({ innerRadius: 1, outerRadius: 5, innerSegments: 32, outerSegments: 72, startAngle: Math.PI / 2, outerRotation: Math.PI / 2 })
32
32
  const pts = geom3.toPoints(obs)
33
- t.is(pts.length, 1154)
33
+ t.is(pts.length, 1212)
34
34
 
35
35
  const bounds = measureBoundingBox(obs)
36
36
  const expectedBounds = [[-6, 0, -1], [0, 6, 1]]