@jscad/modeling 2.12.0 → 2.12.1
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 +13 -0
- package/dist/jscad-modeling.min.js +319 -316
- package/package.json +2 -2
- package/src/geometries/geom2/transform.js +9 -1
- package/src/geometries/geom2/transform.test.js +58 -1
- package/src/maths/plane/fromNoisyPoints.d.ts +6 -0
- package/src/maths/plane/fromNoisyPoints.js +106 -0
- package/src/maths/plane/fromNoisyPoints.test.js +24 -0
- package/src/maths/plane/index.d.ts +1 -0
- package/src/maths/plane/index.js +1 -0
- package/src/operations/extrusions/extrudeRotate.js +1 -1
- package/src/operations/transforms/mirror.test.js +9 -3
- package/src/primitives/polygon.d.ts +1 -0
- package/src/primitives/polygon.js +15 -4
- package/src/primitives/polygon.test.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jscad/modeling",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.1",
|
|
4
4
|
"description": "Constructive Solid Geometry (CSG) Library for JSCAD",
|
|
5
5
|
"homepage": "https://openjscad.xyz/",
|
|
6
6
|
"repository": "https://github.com/jscad/OpenJSCAD.org",
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
"nyc": "15.1.0",
|
|
62
62
|
"uglifyify": "5.0.2"
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "e07bb27d61f638348c73cc1383dfb5339060a02a"
|
|
65
65
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const mat4 = require('../../maths/mat4')
|
|
2
2
|
|
|
3
|
+
const reverse = require('./reverse.js')
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Transform the given geometry using the given matrix.
|
|
5
7
|
* This is a lazy transform of the sides, as this function only adjusts the transforms.
|
|
@@ -14,7 +16,13 @@ const mat4 = require('../../maths/mat4')
|
|
|
14
16
|
*/
|
|
15
17
|
const transform = (matrix, geometry) => {
|
|
16
18
|
const transforms = mat4.multiply(mat4.create(), matrix, geometry.transforms)
|
|
17
|
-
|
|
19
|
+
const transformed = Object.assign({}, geometry, { transforms })
|
|
20
|
+
// determine if the transform is mirroring in 2D
|
|
21
|
+
if (matrix[0] * matrix[5] - matrix[4] * matrix[1] < 0) {
|
|
22
|
+
// reverse the order to preserve the orientation
|
|
23
|
+
return reverse(transformed)
|
|
24
|
+
}
|
|
25
|
+
return transformed
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
module.exports = transform
|
|
@@ -2,7 +2,13 @@ const test = require('ava')
|
|
|
2
2
|
|
|
3
3
|
const mat4 = require('../../maths/mat4')
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { measureArea } = require('../../measurements/index.js')
|
|
6
|
+
|
|
7
|
+
const { mirrorX, mirrorY, mirrorZ } = require('../../operations/transforms/index.js')
|
|
8
|
+
|
|
9
|
+
const { square } = require('../../primitives/index.js')
|
|
10
|
+
|
|
11
|
+
const { fromPoints, transform, toOutlines, toSides } = require('./index.js')
|
|
6
12
|
|
|
7
13
|
const { comparePoints, compareVectors } = require('../../../test/helpers/')
|
|
8
14
|
|
|
@@ -51,3 +57,54 @@ test('transform: adjusts the transforms of geom2', (t) => {
|
|
|
51
57
|
t.true(comparePoints(another.sides[2], expected.sides[2]))
|
|
52
58
|
t.true(compareVectors(another.transforms, expected.transforms))
|
|
53
59
|
})
|
|
60
|
+
|
|
61
|
+
test('transform: geom2 mirrorX', (t) => {
|
|
62
|
+
const geometry = square()
|
|
63
|
+
const transformed = mirrorX(geometry)
|
|
64
|
+
t.is(measureArea(geometry), 4)
|
|
65
|
+
// area will be negative unless we reversed the points
|
|
66
|
+
t.is(measureArea(transformed), 4)
|
|
67
|
+
const pts = toOutlines(transformed)[0]
|
|
68
|
+
const exp = [[-1, 1], [-1, -1], [1, -1], [1, 1]]
|
|
69
|
+
t.true(comparePoints(pts, exp))
|
|
70
|
+
t.deepEqual(toSides(transformed), [
|
|
71
|
+
[[1, 1], [-1, 1]],
|
|
72
|
+
[[-1, 1], [-1, -1]],
|
|
73
|
+
[[-1, -1], [1, -1]],
|
|
74
|
+
[[1, -1], [1, 1]]
|
|
75
|
+
])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('transform: geom2 mirrorY', (t) => {
|
|
79
|
+
const geometry = square()
|
|
80
|
+
const transformed = mirrorY(geometry)
|
|
81
|
+
t.is(measureArea(geometry), 4)
|
|
82
|
+
// area will be negative unless we reversed the points
|
|
83
|
+
t.is(measureArea(transformed), 4)
|
|
84
|
+
const pts = toOutlines(transformed)[0]
|
|
85
|
+
const exp = [[1, -1], [1, 1], [-1, 1], [-1, -1]]
|
|
86
|
+
t.true(comparePoints(pts, exp))
|
|
87
|
+
t.deepEqual(toSides(transformed), [
|
|
88
|
+
[[-1, -1], [1, -1]],
|
|
89
|
+
[[1, -1], [1, 1]],
|
|
90
|
+
[[1, 1], [-1, 1]],
|
|
91
|
+
[[-1, 1], [-1, -1]]
|
|
92
|
+
])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('transform: geom2 mirrorZ', (t) => {
|
|
96
|
+
const geometry = square()
|
|
97
|
+
const transformed = mirrorZ(geometry)
|
|
98
|
+
t.is(measureArea(geometry), 4)
|
|
99
|
+
// area will be negative unless we DIDN'T reverse the points
|
|
100
|
+
t.is(measureArea(transformed), 4)
|
|
101
|
+
const pts = toOutlines(transformed)[0]
|
|
102
|
+
const exp = [[-1, -1], [1, -1], [1, 1], [-1, 1]]
|
|
103
|
+
t.true(comparePoints(pts, exp))
|
|
104
|
+
t.deepEqual(toSides(transformed), [
|
|
105
|
+
[[-1, 1], [-1, -1]],
|
|
106
|
+
[[-1, -1], [1, -1]],
|
|
107
|
+
[[1, -1], [1, 1]],
|
|
108
|
+
[[1, 1], [-1, 1]]
|
|
109
|
+
])
|
|
110
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const vec3 = require('../vec3')
|
|
2
|
+
const fromNormalAndPoint = require('./fromNormalAndPoint')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a best-fit plane from the given noisy vertices.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: There are two possible orientations for every plane.
|
|
8
|
+
* This function always produces positive orientations.
|
|
9
|
+
*
|
|
10
|
+
* See http://www.ilikebigbits.com for the original discussion
|
|
11
|
+
*
|
|
12
|
+
* @param {Plane} out - receiving plane
|
|
13
|
+
* @param {Array} vertices - list of vertices in any order or position
|
|
14
|
+
* @returns {Plane} out
|
|
15
|
+
* @alias module:modeling/maths/plane.fromNoisyPoints
|
|
16
|
+
*/
|
|
17
|
+
const fromNoisyPoints = (out, ...vertices) => {
|
|
18
|
+
out[0] = 0.0
|
|
19
|
+
out[1] = 0.0
|
|
20
|
+
out[2] = 0.0
|
|
21
|
+
out[3] = 0.0
|
|
22
|
+
|
|
23
|
+
// calculate the centroid of the vertices
|
|
24
|
+
// NOTE: out is the centriod
|
|
25
|
+
const n = vertices.length
|
|
26
|
+
vertices.forEach((v) => {
|
|
27
|
+
vec3.add(out, out, v)
|
|
28
|
+
})
|
|
29
|
+
vec3.scale(out, out, 1.0 / n)
|
|
30
|
+
|
|
31
|
+
// Calculate full 3x3 covariance matrix, excluding symmetries
|
|
32
|
+
let xx = 0.0
|
|
33
|
+
let xy = 0.0
|
|
34
|
+
let xz = 0.0
|
|
35
|
+
let yy = 0.0
|
|
36
|
+
let yz = 0.0
|
|
37
|
+
let zz = 0.0
|
|
38
|
+
|
|
39
|
+
const vn = vec3.create()
|
|
40
|
+
vertices.forEach((v) => {
|
|
41
|
+
// NOTE: out is the centriod
|
|
42
|
+
vec3.subtract(vn, v, out)
|
|
43
|
+
xx += vn[0] * vn[0]
|
|
44
|
+
xy += vn[0] * vn[1]
|
|
45
|
+
xz += vn[0] * vn[2]
|
|
46
|
+
yy += vn[1] * vn[1]
|
|
47
|
+
yz += vn[1] * vn[2]
|
|
48
|
+
zz += vn[2] * vn[2]
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
xx /= n
|
|
52
|
+
xy /= n
|
|
53
|
+
xz /= n
|
|
54
|
+
yy /= n
|
|
55
|
+
yz /= n
|
|
56
|
+
zz /= n
|
|
57
|
+
|
|
58
|
+
// Calculate the smallest Eigenvector of the covariance matrix
|
|
59
|
+
// which becomes the plane normal
|
|
60
|
+
|
|
61
|
+
vn[0] = 0.0
|
|
62
|
+
vn[1] = 0.0
|
|
63
|
+
vn[2] = 0.0
|
|
64
|
+
|
|
65
|
+
// weighted directional vector
|
|
66
|
+
const wdv = vec3.create()
|
|
67
|
+
|
|
68
|
+
// X axis
|
|
69
|
+
let det = yy * zz - yz * yz
|
|
70
|
+
wdv[0] = det
|
|
71
|
+
wdv[1] = xz * yz - xy * zz
|
|
72
|
+
wdv[2] = xy * yz - xz * yy
|
|
73
|
+
|
|
74
|
+
let weight = det * det
|
|
75
|
+
vec3.add(vn, vn, vec3.scale(wdv, wdv, weight))
|
|
76
|
+
|
|
77
|
+
// Y axis
|
|
78
|
+
det = xx * zz - xz * xz
|
|
79
|
+
wdv[0] = xz * yz - xy * zz
|
|
80
|
+
wdv[1] = det
|
|
81
|
+
wdv[2] = xy * xz - yz * xx
|
|
82
|
+
|
|
83
|
+
weight = det * det
|
|
84
|
+
if (vec3.dot(vn, wdv) < 0.0) {
|
|
85
|
+
weight = -weight
|
|
86
|
+
}
|
|
87
|
+
vec3.add(vn, vn, vec3.scale(wdv, wdv, weight))
|
|
88
|
+
|
|
89
|
+
// Z axis
|
|
90
|
+
det = xx * yy - xy * xy
|
|
91
|
+
wdv[0] = xy * yz - xz * yy
|
|
92
|
+
wdv[1] = xy * xz - yz * xx
|
|
93
|
+
wdv[2] = det
|
|
94
|
+
|
|
95
|
+
weight = det * det
|
|
96
|
+
if (vec3.dot(vn, wdv) < 0.0) {
|
|
97
|
+
weight = -weight
|
|
98
|
+
}
|
|
99
|
+
vec3.add(vn, vn, vec3.scale(wdv, wdv, weight))
|
|
100
|
+
|
|
101
|
+
// create the plane from normal and centriod
|
|
102
|
+
// NOTE: out is the centriod
|
|
103
|
+
return fromNormalAndPoint(out, vn, out)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = fromNoisyPoints
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const test = require('ava')
|
|
2
|
+
const { fromNoisyPoints, create } = require('./index')
|
|
3
|
+
|
|
4
|
+
const { compareVectors } = require('../../../test/helpers/index')
|
|
5
|
+
|
|
6
|
+
test('plane: fromNoisyPoints() should return a new plane with correct values', (t) => {
|
|
7
|
+
const obs1 = fromNoisyPoints(create(), [0, 0, 0], [1, 0, 0], [1, 1, 0])
|
|
8
|
+
t.true(compareVectors(obs1, [0, 0, 1, 0]))
|
|
9
|
+
|
|
10
|
+
const obs2 = fromNoisyPoints(obs1, [0, 6, 0], [0, 2, 2], [0, 6, 6])
|
|
11
|
+
t.true(compareVectors(obs2, [1, 0, 0, 0]))
|
|
12
|
+
|
|
13
|
+
// same vertices results in an invalid plane
|
|
14
|
+
const obs3 = fromNoisyPoints(obs1, [0, 6, 0], [0, 6, 0], [0, 6, 0])
|
|
15
|
+
t.true(compareVectors(obs3, [0 / 0, 0 / 0, 0 / 0, 0 / 0]))
|
|
16
|
+
|
|
17
|
+
// co-linear vertices
|
|
18
|
+
const obs4 = fromNoisyPoints(obs1, [0, 0, 0], [1, 0, 0], [2, 0, 0], [0, 1, 0])
|
|
19
|
+
t.true(compareVectors(obs4, [0, 0, 1, 0]))
|
|
20
|
+
|
|
21
|
+
// random vertices
|
|
22
|
+
const obs5 = fromNoisyPoints(obs1, [0, 0, 0], [5, 1, -2], [3, -2, 4], [1, 1, 0])
|
|
23
|
+
t.true(compareVectors(obs5, [0.08054818365229491, 0.8764542170444571, 0.47469990050062555, 0.4185833634679763]))
|
|
24
|
+
})
|
|
@@ -5,6 +5,7 @@ export { default as equals } from './equals'
|
|
|
5
5
|
export { default as flip } from './flip'
|
|
6
6
|
export { default as fromNormalAndPoint } from './fromNormalAndPoint'
|
|
7
7
|
export { default as fromValues } from './fromValues'
|
|
8
|
+
export { default as fromNoisyPoints } from './fromNoisyPoints'
|
|
8
9
|
export { default as fromPoints } from './fromPoints'
|
|
9
10
|
export { default as fromPointsRandom } from './fromPointsRandom'
|
|
10
11
|
export { default as signedDistanceToPoint } from './signedDistanceToPoint'
|
package/src/maths/plane/index.js
CHANGED
|
@@ -32,6 +32,7 @@ module.exports = {
|
|
|
32
32
|
* @function fromValues
|
|
33
33
|
*/
|
|
34
34
|
fromValues: require('../vec4/fromValues'),
|
|
35
|
+
fromNoisyPoints: require('./fromNoisyPoints'),
|
|
35
36
|
fromPoints: require('./fromPoints'),
|
|
36
37
|
fromPointsRandom: require('./fromPointsRandom'),
|
|
37
38
|
projectionOfPoint: require('./projectionOfPoint'),
|
|
@@ -93,7 +93,7 @@ const extrudeRotate = (options, geometry) => {
|
|
|
93
93
|
return [point0, point1]
|
|
94
94
|
})
|
|
95
95
|
// recreate the geometry from the (-) capped points
|
|
96
|
-
geometry = geom2.
|
|
96
|
+
geometry = geom2.create(shapeSides)
|
|
97
97
|
geometry = mirrorX(geometry)
|
|
98
98
|
} else if (pointsWithPositiveX.length >= pointsWithNegativeX.length) {
|
|
99
99
|
shapeSides = shapeSides.map((side) => {
|
|
@@ -2,6 +2,8 @@ const test = require('ava')
|
|
|
2
2
|
|
|
3
3
|
const { comparePoints, comparePolygonsAsPoints } = require('../../../test/helpers')
|
|
4
4
|
|
|
5
|
+
const { measureArea } = require('../../measurements')
|
|
6
|
+
|
|
5
7
|
const { geom2, geom3, path2 } = require('../../geometries')
|
|
6
8
|
|
|
7
9
|
const { mirror, mirrorX, mirrorY, mirrorZ } = require('./index')
|
|
@@ -40,25 +42,29 @@ test('mirror: mirroring of geom2 about X/Y produces expected changes to points',
|
|
|
40
42
|
// mirror about X
|
|
41
43
|
let mirrored = mirror({ normal: [1, 0, 0] }, geometry)
|
|
42
44
|
let obs = geom2.toPoints(mirrored)
|
|
43
|
-
let exp = [[
|
|
45
|
+
let exp = [[0, 5], [5, -5], [-10, -5]]
|
|
44
46
|
t.notThrows(() => geom2.validate(mirrored))
|
|
47
|
+
t.is(measureArea(mirrored), measureArea(geometry))
|
|
45
48
|
t.true(comparePoints(obs, exp))
|
|
46
49
|
|
|
47
50
|
mirrored = mirrorX(geometry)
|
|
48
51
|
obs = geom2.toPoints(mirrored)
|
|
49
52
|
t.notThrows(() => geom2.validate(mirrored))
|
|
53
|
+
t.is(measureArea(mirrored), measureArea(geometry))
|
|
50
54
|
t.true(comparePoints(obs, exp))
|
|
51
55
|
|
|
52
56
|
// mirror about Y
|
|
53
57
|
mirrored = mirror({ normal: [0, 1, 0] }, geometry)
|
|
54
58
|
obs = geom2.toPoints(mirrored)
|
|
55
|
-
exp = [[
|
|
59
|
+
exp = [[0, -5], [-5, 5], [10, 5]]
|
|
56
60
|
t.notThrows(() => geom2.validate(mirrored))
|
|
61
|
+
t.is(measureArea(mirrored), measureArea(geometry))
|
|
57
62
|
t.true(comparePoints(obs, exp))
|
|
58
63
|
|
|
59
64
|
mirrored = mirrorY(geometry)
|
|
60
65
|
obs = geom2.toPoints(mirrored)
|
|
61
66
|
t.notThrows(() => geom2.validate(mirrored))
|
|
67
|
+
t.is(measureArea(mirrored), measureArea(geometry))
|
|
62
68
|
t.true(comparePoints(obs, exp))
|
|
63
69
|
})
|
|
64
70
|
|
|
@@ -146,7 +152,7 @@ test('mirror: mirroring of multiple objects produces an array of mirrored object
|
|
|
146
152
|
t.true(comparePoints(obs, exp))
|
|
147
153
|
|
|
148
154
|
obs = geom2.toPoints(mirrored[2])
|
|
149
|
-
exp = [[
|
|
155
|
+
exp = [[0, -5], [-5, 5], [10, 5]]
|
|
150
156
|
t.notThrows(() => geom2.validate(mirrored[2]))
|
|
151
157
|
t.true(comparePoints(obs, exp))
|
|
152
158
|
})
|
|
@@ -2,10 +2,13 @@ const geom2 = require('../geometries/geom2')
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Construct a polygon in two dimensional space from a list of points, or a list of points and paths.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* NOTE: The ordering of points is important, and must define a counter clockwise rotation of points.
|
|
7
|
+
*
|
|
6
8
|
* @param {Object} options - options for construction
|
|
7
9
|
* @param {Array} options.points - points of the polygon : either flat or nested array of 2D points
|
|
8
10
|
* @param {Array} [options.paths] - paths of the polygon : either flat or nested array of point indexes
|
|
11
|
+
* @param {String} [options.orientation='counterclockwise'] - orientation of points
|
|
9
12
|
* @returns {geom2} new 2D geometry
|
|
10
13
|
* @alias module:modeling/primitives.polygon
|
|
11
14
|
*
|
|
@@ -24,9 +27,10 @@ const geom2 = require('../geometries/geom2')
|
|
|
24
27
|
const polygon = (options) => {
|
|
25
28
|
const defaults = {
|
|
26
29
|
points: [],
|
|
27
|
-
paths: []
|
|
30
|
+
paths: [],
|
|
31
|
+
orientation: 'counterclockwise'
|
|
28
32
|
}
|
|
29
|
-
const { points, paths } = Object.assign({}, defaults, options)
|
|
33
|
+
const { points, paths, orientation } = Object.assign({}, defaults, options)
|
|
30
34
|
|
|
31
35
|
if (!(Array.isArray(points) && Array.isArray(paths))) throw new Error('points and paths must be arrays')
|
|
32
36
|
|
|
@@ -58,13 +62,20 @@ const polygon = (options) => {
|
|
|
58
62
|
const allpoints = []
|
|
59
63
|
listofpolys.forEach((list) => list.forEach((point) => allpoints.push(point)))
|
|
60
64
|
|
|
65
|
+
// convert the list of paths into a list of sides, and accumulate
|
|
61
66
|
let sides = []
|
|
62
67
|
listofpaths.forEach((path) => {
|
|
63
68
|
const setofpoints = path.map((index) => allpoints[index])
|
|
64
69
|
const geometry = geom2.fromPoints(setofpoints)
|
|
65
70
|
sides = sides.concat(geom2.toSides(geometry))
|
|
66
71
|
})
|
|
67
|
-
|
|
72
|
+
|
|
73
|
+
// convert the list of sides into a geometry
|
|
74
|
+
let geometry = geom2.create(sides)
|
|
75
|
+
if (orientation == "clockwise") {
|
|
76
|
+
geometry = geom2.reverse(geometry)
|
|
77
|
+
}
|
|
78
|
+
return geometry
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
module.exports = polygon
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const test = require('ava')
|
|
2
2
|
|
|
3
3
|
const geom2 = require('../geometries/geom2')
|
|
4
|
+
const measureArea = require('../measurements/measureArea')
|
|
4
5
|
|
|
5
6
|
const { polygon } = require('./index')
|
|
6
7
|
|
|
@@ -51,3 +52,12 @@ test('polygon: providing object.points (array) and object.path (array) creates e
|
|
|
51
52
|
t.notThrows(() => geom2.validate(geometry))
|
|
52
53
|
t.true(comparePoints(obs, exp))
|
|
53
54
|
})
|
|
55
|
+
|
|
56
|
+
test('polygon: clockwise points', (t) => {
|
|
57
|
+
const poly = polygon({
|
|
58
|
+
points: [[-10, -0], [-10, -10], [-15, -5]],
|
|
59
|
+
orientation: "clockwise",
|
|
60
|
+
})
|
|
61
|
+
t.is(poly.sides.length, 3)
|
|
62
|
+
t.is(measureArea(poly), 25)
|
|
63
|
+
})
|