@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jscad/modeling",
3
- "version": "2.12.0",
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": "e269f212db5a00cda740d2f7ad3e5206d1eb839f"
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
- return Object.assign({}, geometry, { transforms })
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 { transform, fromPoints, toSides } = require('./index')
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,6 @@
1
+ import Plane from './type'
2
+ import Vec3 from '../vec3/type'
3
+
4
+ export default fromNoisyPoints
5
+
6
+ declare function fromNoisyPoints(out: Plane, ...vertices: Array<Vec3>): Plane
@@ -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'
@@ -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.reverse(geom2.create(shapeSides))
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 = [[5, -5], [0, 5], [-10, -5]]
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 = [[-5, 5], [0, -5], [10, 5]]
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 = [[-5, 5], [0, -5], [10, 5]]
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
  })
@@ -6,6 +6,7 @@ export default polygon
6
6
  export interface PolygonOptions {
7
7
  points: Array<Vec2> | Array<Array<Vec2>>
8
8
  paths?: Array<number> | Array<Array<number>>
9
+ orientation?: 'counterclockwise' | 'clockwise'
9
10
  }
10
11
 
11
12
  declare function polygon(options: PolygonOptions): Geom2
@@ -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
- * NOTE: The ordering of points is VERY IMPORTANT.
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
- return geom2.create(sides)
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
+ })