@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.
- package/CHANGELOG.md +11 -0
- package/dist/jscad-modeling.min.js +159 -132
- package/package.json +2 -2
- package/src/geometries/poly3/invert.js +7 -1
- package/src/operations/expansions/expand.test.js +1 -1
- package/src/operations/expansions/expandShell.js +2 -2
- package/src/operations/extrusions/earcut/assignHoles.js +87 -0
- package/src/operations/extrusions/earcut/assignHoles.test.js +28 -0
- package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
- package/src/operations/extrusions/earcut/index.js +252 -0
- package/src/operations/extrusions/earcut/linkedList.js +58 -0
- package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
- package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
- package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
- package/src/operations/extrusions/earcut/triangle.js +16 -0
- package/src/operations/extrusions/extrudeFromSlices.js +10 -3
- package/src/operations/extrusions/extrudeFromSlices.test.js +33 -23
- package/src/operations/extrusions/extrudeLinear.js +4 -3
- package/src/operations/extrusions/extrudeLinear.test.js +54 -28
- package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
- package/src/operations/extrusions/extrudeRectangular.test.js +7 -7
- package/src/operations/extrusions/extrudeRotate.test.js +19 -27
- package/src/operations/extrusions/slice/calculatePlane.js +7 -4
- package/src/operations/extrusions/slice/repairSlice.js +47 -0
- package/src/operations/extrusions/slice/toPolygons.js +24 -60
- package/src/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
|
-
[[
|
|
33
|
-
|
|
34
|
-
[[10, 4.
|
|
35
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
112
|
-
|
|
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,
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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,
|
|
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
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
distance
|
|
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
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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,
|
|
33
|
+
t.is(pts.length, 1212)
|
|
34
34
|
|
|
35
35
|
const bounds = measureBoundingBox(obs)
|
|
36
36
|
const expectedBounds = [[-6, 0, -1], [0, 6, 1]]
|