@jscad/modeling 2.6.0 → 2.7.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 (73) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/jscad-modeling.min.js +97 -94
  3. package/package.json +2 -2
  4. package/src/colors/colorize.js +35 -1
  5. package/src/colors/colorize.test.js +19 -4
  6. package/src/colors/hslToRgb.js +1 -1
  7. package/src/colors/hueToColorComponent.js +1 -0
  8. package/src/curves/bezier/create.js +3 -8
  9. package/src/curves/bezier/tangentAt.js +2 -2
  10. package/src/curves/index.js +1 -1
  11. package/src/geometries/geom2/clone.js +2 -12
  12. package/src/geometries/geom2/toOutlines.js +6 -11
  13. package/src/geometries/geom2/transform.js +0 -2
  14. package/src/geometries/geom3/clone.js +2 -14
  15. package/src/geometries/geom3/clone.test.js +0 -2
  16. package/src/geometries/geom3/create.js +1 -3
  17. package/src/geometries/geom3/create.test.js +0 -2
  18. package/src/geometries/geom3/fromCompactBinary.js +4 -6
  19. package/src/geometries/geom3/fromToCompactBinary.test.js +0 -6
  20. package/src/geometries/geom3/invert.js +2 -2
  21. package/src/geometries/geom3/invert.test.js +0 -2
  22. package/src/geometries/geom3/toCompactBinary.js +8 -10
  23. package/src/geometries/geom3/toPoints.js +1 -0
  24. package/src/geometries/geom3/transform.js +0 -2
  25. package/src/geometries/geom3/transform.test.js +0 -1
  26. package/src/geometries/geom3/type.d.ts +0 -1
  27. package/src/geometries/path2/clone.js +2 -13
  28. package/src/geometries/path2/transform.js +0 -2
  29. package/src/geometries/poly3/isConvex.js +1 -1
  30. package/src/geometries/poly3/measureArea.js +12 -13
  31. package/src/geometries/poly3/measureArea.test.js +15 -0
  32. package/src/geometries/poly3/plane.js +1 -2
  33. package/src/maths/line3/create.js +2 -1
  34. package/src/maths/mat4/fromRotation.js +1 -1
  35. package/src/maths/mat4/isIdentity.test.js +0 -2
  36. package/src/maths/mat4/isOnlyTransformScale.js +5 -4
  37. package/src/maths/mat4/rotate.js +1 -1
  38. package/src/maths/plane/fromPoints.js +32 -10
  39. package/src/maths/plane/fromPoints.test.js +4 -0
  40. package/src/maths/vec2/length.test.js +10 -0
  41. package/src/maths/vec3/angle.js +2 -2
  42. package/src/maths/vec3/angle.test.js +17 -0
  43. package/src/maths/vec3/length.test.js +10 -0
  44. package/src/measurements/measureBoundingBox.js +37 -121
  45. package/src/measurements/measureBoundingBox.test.js +8 -0
  46. package/src/measurements/measureCenterOfMass.js +0 -1
  47. package/src/measurements/measureEpsilon.js +3 -9
  48. package/src/operations/booleans/reTesselateCoplanarPolygons.js +1 -1
  49. package/src/operations/booleans/retessellate.js +1 -3
  50. package/src/operations/booleans/trees/PolygonTreeNode.js +0 -1
  51. package/src/operations/expansions/expand.js +2 -0
  52. package/src/operations/expansions/expand.test.js +1 -1
  53. package/src/operations/expansions/offset.js +1 -0
  54. package/src/operations/extrusions/extrudeLinear.js +1 -1
  55. package/src/operations/extrusions/extrudeRectangular.js +2 -1
  56. package/src/operations/extrusions/extrudeRotate.test.js +18 -10
  57. package/src/operations/hulls/quickhull/QuickHull.js +2 -2
  58. package/src/operations/modifiers/edges.js +1 -3
  59. package/src/operations/modifiers/generalize.js +3 -7
  60. package/src/operations/modifiers/snapPolygons.js +2 -2
  61. package/src/operations/modifiers/snapPolygons.test.js +13 -5
  62. package/src/operations/transforms/mirror.js +1 -1
  63. package/src/primitives/ellipse.js +1 -1
  64. package/src/primitives/geodesicSphere.js +2 -2
  65. package/src/primitives/index.d.ts +1 -0
  66. package/src/primitives/index.js +2 -1
  67. package/src/primitives/roundedCylinder.js +1 -1
  68. package/src/primitives/triangle.d.ts +10 -0
  69. package/src/primitives/triangle.js +164 -0
  70. package/src/primitives/triangle.test.js +95 -0
  71. package/src/utils/insertSorted.js +1 -0
  72. package/test/helpers/comparePolygons.js +1 -3
  73. package/test/helpers/nearlyEqual.js +2 -6
@@ -34,29 +34,37 @@ test('snapPolygons: snap of polygons produces expected results', (t) => {
34
34
  [-24.445112000000115, 19.346837333333426, 46.47572533333356],
35
35
  [-24.44446933333345, 19.346837333333426, 46.47508266666689],
36
36
  [-23.70540266666678, 18.79864266666676, 39.56448800000019],
37
- [-23.70540266666678 - 0.00001234, 18.79864266666676 + 0.000001234, 39.56448800000019 + 0.00001234],
37
+ [-23.70540266666678 - 0.00001234, 18.79864266666676 + 0.000001234, 39.56448800000019 + 0.00001234]
38
38
  ]), // OK
39
39
  poly3.fromPoints([
40
40
  [-24.445112000000115, 19.346837333333426, 46.47572533333356],
41
41
  [-23.70540266666678 - 0.00001234, 18.79864266666676 + 0.000001234, 39.56448800000019 + 0.00001234],
42
42
  [-23.70540266666678, 18.79864266666676, 39.56448800000019],
43
- [-23.70540266666678 - 0.00001234, 18.79864266666676 + 0.000001234, 39.56448800000019 + 0.00001234],
43
+ [-23.70540266666678 - 0.00001234, 18.79864266666676 + 0.000001234, 39.56448800000019 + 0.00001234]
44
44
  ]),
45
45
  poly3.fromPoints([
46
46
  [-23.70540266666678, 18.79864266666676, 39.56448800000019],
47
47
  [-23.70540266666678 - 0.00001234, 18.79864266666676 + 0.000001234, 39.56448800000019 + 0.00001234],
48
48
  [-23.70540266666678, 18.79864266666676, 39.56448800000019],
49
- [-23.70540266666678 - 0.00001234, 18.79864266666676 + 0.000001234, 39.56448800000019 + 0.00001234],
49
+ [-23.70540266666678 - 0.00001234, 18.79864266666676 + 0.000001234, 39.56448800000019 + 0.00001234]
50
50
  ]),
51
+ // inverted polygon
52
+ poly3.fromPoints([
53
+ [20.109133333333336, -4.894033333333335, -1.0001266666666668],
54
+ [20.021120000000003, -5.1802133333333344, -1.0001266666666668],
55
+ [20.020300000000002, -5.182946666666668, -1.0001266666666668],
56
+ [10.097753333333335, -5.182946666666668, -1.0001266666666668],
57
+ [10.287720000000002, -4.894033333333335, -1.0001266666666668]
58
+ ])
51
59
  ]
52
60
 
53
61
  const results = snapPolygons(0.0001, polygons)
54
- t.is(results.length, 4)
62
+ t.is(results.length, 5)
55
63
 
56
64
  const exp3 = poly3.fromPoints([
57
65
  [-24.4451, 19.3468, 46.4757],
58
66
  [-24.4445, 19.3468, 46.475100000000005],
59
67
  [-23.7054, 18.7986, 39.5645]
60
68
  ])
61
- t.deepEqual(results[3], exp3)
69
+ t.deepEqual(results[3].vertices, exp3.vertices)
62
70
  })
@@ -64,7 +64,7 @@ const mirrorY = (...objects) => mirror({ normal: [0, 1, 0] }, objects)
64
64
 
65
65
  /**
66
66
  * Mirror the given object(s) about the Z axis.
67
- * @param {...Object} objects - the geometries to mirror
67
+ * @param {...Object} geometries - the geometries to mirror
68
68
  * @return {Object|Array} the mirrored geometry, or a list of mirrored geometry
69
69
  * @alias module:modeling/transforms.mirrorZ
70
70
  */
@@ -7,7 +7,7 @@ const geom2 = require('../geometries/geom2')
7
7
  const { isGTE, isNumberArray } = require('./commonChecks')
8
8
 
9
9
  /**
10
- * Construct an axis-aligned ellispe in two dimensional space.
10
+ * Construct an axis-aligned ellipse in two dimensional space.
11
11
  * @see https://en.wikipedia.org/wiki/Ellipse
12
12
  * @param {Object} [options] - options for construction
13
13
  * @param {Array} [options.center=[0,0]] - center of ellipse
@@ -79,7 +79,7 @@ const geodesicSphere = (options) => {
79
79
 
80
80
  // -- normalize
81
81
  for (let k = 0; k < 3; k++) {
82
- const r = Math.sqrt(q[k][0] * q[k][0] + q[k][1] * q[k][1] + q[k][2] * q[k][2])
82
+ const r = Math.hypot(q[k][0], q[k][1], q[k][2])
83
83
  for (let l = 0; l < 3; l++) {
84
84
  q[k][l] /= r
85
85
  }
@@ -95,7 +95,7 @@ const geodesicSphere = (options) => {
95
95
 
96
96
  // -- normalize
97
97
  for (let k = 0; k < 3; k++) {
98
- const r = Math.sqrt(q[k][0] * q[k][0] + q[k][1] * q[k][1] + q[k][2] * q[k][2])
98
+ const r = Math.hypot(q[k][0], q[k][1], q[k][2])
99
99
  for (let l = 0; l < 3; l++) {
100
100
  q[k][l] /= r
101
101
  }
@@ -18,5 +18,6 @@ export { default as sphere, SphereOptions } from './sphere'
18
18
  export { default as square, SquareOptions } from './square'
19
19
  export { default as star, StarOptions } from './star'
20
20
  export { default as torus, TorusOptions } from './torus'
21
+ export { default as triangle, TriangleOptions } from './triangle'
21
22
 
22
23
  export as namespace primitives
@@ -26,5 +26,6 @@ module.exports = {
26
26
  sphere: require('./sphere'),
27
27
  square: require('./square'),
28
28
  star: require('./star'),
29
- torus: require('./torus')
29
+ torus: require('./torus'),
30
+ triangle: require('./triangle')
30
31
  }
@@ -11,7 +11,7 @@ const { isGT, isGTE, isNumberArray } = require('./commonChecks')
11
11
  * Construct a Z axis-aligned solid cylinder in three dimensional space with rounded ends.
12
12
  * @param {Object} [options] - options for construction
13
13
  * @param {Array} [options.center=[0,0,0]] - center of cylinder
14
- * @param {Array} [options.height=2] - height of cylinder
14
+ * @param {Number} [options.height=2] - height of cylinder
15
15
  * @param {Number} [options.radius=1] - radius of cylinder
16
16
  * @param {Number} [options.roundRadius=0.2] - radius of rounded edges
17
17
  * @param {Number} [options.segments=32] - number of segments to create per full rotation
@@ -0,0 +1,10 @@
1
+ import Geom2 from '../geometries/geom2/type'
2
+
3
+ export default triangle
4
+
5
+ export interface TriangleOptions {
6
+ type?: 'AAA' | 'AAS' | 'ASA' | 'SAS' | 'SSA' | 'SSS'
7
+ values?: [number, number, number]
8
+ }
9
+
10
+ declare function triangle(options?: TriangleOptions): Geom2
@@ -0,0 +1,164 @@
1
+ const vec2 = require('../maths/vec2')
2
+
3
+ const geom2 = require('../geometries/geom2')
4
+
5
+ const { isNumberArray } = require('./commonChecks')
6
+
7
+ const NEPS = 1e-13
8
+
9
+ // returns angle C
10
+ const solveAngleFromSSS = (a, b, c) => Math.acos(((a * a) + (b * b) - (c * c)) / (2 * a * b))
11
+
12
+ // returns side c
13
+ const solveSideFromSAS = (a, C, b) => {
14
+ if (C > NEPS) {
15
+ return Math.sqrt(a * a + b * b - 2 * a * b * Math.cos(C))
16
+ }
17
+
18
+ // Explained in https://www.nayuki.io/page/numerically-stable-law-of-cosines
19
+ return Math.sqrt((a - b) * (a - b) + a * b * C * C * (1 - C * C / 12))
20
+ }
21
+
22
+ // AAA is when three angles of a triangle, but no sides
23
+ const solveAAA = (angles) => {
24
+ const eps = Math.abs(angles[0] + angles[1] + angles[2] - Math.PI)
25
+ if (eps > NEPS) throw new Error('AAA triangles require angles that sum to PI')
26
+
27
+ const A = angles[0]
28
+ const B = angles[1]
29
+ const C = Math.PI - A - B
30
+
31
+ // Note: This is not 100% proper but...
32
+ // default the side c length to 1
33
+ // solve the other lengths
34
+ const c = 1
35
+ const a = (c / Math.sin(C)) * Math.sin(A)
36
+ const b = (c / Math.sin(C)) * Math.sin(B)
37
+ return createTriangle(A, B, C, a, b, c)
38
+ }
39
+
40
+ // AAS is when two angles and one side are known, and the side is not between the angles
41
+ const solveAAS = (values) => {
42
+ const A = values[0]
43
+ const B = values[1]
44
+ const C = Math.PI + NEPS - A - B
45
+
46
+ if (C < NEPS) throw new Error('AAS triangles require angles that sum to PI')
47
+
48
+ const a = values[2]
49
+ const b = (a / Math.sin(A)) * Math.sin(B)
50
+ const c = (a / Math.sin(A)) * Math.sin(C)
51
+ return createTriangle(A, B, C, a, b, c)
52
+ }
53
+
54
+ // ASA is when two angles and the side between the angles are known
55
+ const solveASA = (values) => {
56
+ const A = values[0]
57
+ const B = values[2]
58
+ const C = Math.PI + NEPS - A - B
59
+
60
+ if (C < NEPS) throw new Error('ASA triangles require angles that sum to PI')
61
+
62
+ const c = values[1]
63
+ const a = (c / Math.sin(C)) * Math.sin(A)
64
+ const b = (c / Math.sin(C)) * Math.sin(B)
65
+ return createTriangle(A, B, C, a, b, c)
66
+ }
67
+
68
+ // SAS is when two sides and the angle between them are known
69
+ const solveSAS = (values) => {
70
+ const c = values[0]
71
+ const B = values[1]
72
+ const a = values[2]
73
+
74
+ const b = solveSideFromSAS(c, B, a)
75
+
76
+ const A = solveAngleFromSSS(b, c, a) // solve for A
77
+ const C = Math.PI - A - B
78
+ return createTriangle(A, B, C, a, b, c)
79
+ }
80
+
81
+ // SSA is when two sides and an angle that is not the angle between the sides are known
82
+ const solveSSA = (values) => {
83
+ const c = values[0]
84
+ const a = values[1]
85
+ const C = values[2]
86
+
87
+ const A = Math.asin(a * Math.sin(C) / c)
88
+ const B = Math.PI - A - C
89
+
90
+ const b = (c / Math.sin(C)) * Math.sin(B)
91
+ return createTriangle(A, B, C, a, b, c)
92
+ }
93
+
94
+ // SSS is when we know three sides of the triangle
95
+ const solveSSS = (lengths) => {
96
+ const a = lengths[1]
97
+ const b = lengths[2]
98
+ const c = lengths[0]
99
+ if (((a + b) <= c) || ((b + c) <= a) || ((c + a) <= b)) {
100
+ throw new Error('SSS triangle is incorrect, as the longest side is longer than the sum of the other sides')
101
+ }
102
+
103
+ const A = solveAngleFromSSS(b, c, a) // solve for A
104
+ const B = solveAngleFromSSS(c, a, b) // solve for B
105
+ const C = Math.PI - A - B
106
+ return createTriangle(A, B, C, a, b, c)
107
+ }
108
+
109
+ const createTriangle = (A, B, C, a, b, c) => {
110
+ const p0 = vec2.fromValues(0, 0) // everything starts from 0, 0
111
+ const p1 = vec2.fromValues(c, 0)
112
+ const p2 = vec2.fromValues(a, 0)
113
+ vec2.add(p2, vec2.rotate(p2, p2, [0, 0], Math.PI - B), p1)
114
+ return geom2.fromPoints([p0, p1, p2])
115
+ }
116
+
117
+ /**
118
+ * Construct a triangle in two dimensional space from the given options.
119
+ * The triangle is always constructed CCW from the origin, [0, 0, 0].
120
+ * @see https://www.mathsisfun.com/algebra/trig-solving-triangles.html
121
+ * @param {Object} [options] - options for construction
122
+ * @param {String} [options.type='SSS' - type of triangle to construct; A ~ angle, S ~ side
123
+ * @param {Array} [options.values=[1,1,1]] - angle (radians) of corners or length of sides
124
+ * @returns {geom2} new 2D geometry
125
+ * @alias module:modeling/primitives.triangle
126
+ *
127
+ * @example
128
+ * let myshape = triangle({type: 'AAS', values: [degToRad(62), degToRad(35), 7]})
129
+ */
130
+ const triangle = (options) => {
131
+ const defaults = {
132
+ type: 'SSS',
133
+ values: [1, 1, 1]
134
+ }
135
+ let { type, values } = Object.assign({}, defaults, options)
136
+
137
+ if (typeof (type) !== 'string') throw new Error('triangle type must be a string')
138
+ type = type.toUpperCase()
139
+ if (!((type[0] === 'A' || type[0] === 'S') &&
140
+ (type[1] === 'A' || type[1] === 'S') &&
141
+ (type[2] === 'A' || type[2] === 'S'))) throw new Error('triangle type must contain three letters; A or S')
142
+
143
+ if (!isNumberArray(values, 3)) throw new Error('triangle values must contain three values')
144
+ if (!values.every((n) => n > 0)) throw new Error('triangle values must be greater than zero')
145
+
146
+ switch (type) {
147
+ case 'AAA':
148
+ return solveAAA(values)
149
+ case 'AAS':
150
+ return solveAAS(values)
151
+ case 'ASA':
152
+ return solveASA(values)
153
+ case 'SAS':
154
+ return solveSAS(values)
155
+ case 'SSA':
156
+ return solveSSA(values)
157
+ case 'SSS':
158
+ return solveSSS(values)
159
+ default:
160
+ throw new Error('invalid triangle type, try again')
161
+ }
162
+ }
163
+
164
+ module.exports = triangle
@@ -0,0 +1,95 @@
1
+ const test = require('ava')
2
+
3
+ const { triangle } = require('./index')
4
+
5
+ const degToRad = require('../utils/degToRad')
6
+ const geom2 = require('../geometries/geom2')
7
+
8
+ const comparePoints = require('../../test/helpers/comparePoints')
9
+
10
+ test('triangle (defaults)', (t) => {
11
+ const geometry = triangle()
12
+ const obs = geom2.toPoints(geometry)
13
+ const exp = [
14
+ [0, 0],
15
+ [1, 0],
16
+ [0.5000000000000002, 0.8660254037844387]
17
+ ]
18
+
19
+ t.deepEqual(obs.length, 3)
20
+ t.true(comparePoints(obs, exp))
21
+ })
22
+
23
+ test('triangle (options)', (t) => {
24
+ // test SSS
25
+ let geometry = triangle({ type: 'SSS', values: [7, 8, 6] })
26
+ let obs = geom2.toPoints(geometry)
27
+ let exp = [
28
+ [0, 0],
29
+ [7, 0],
30
+ [1.5, 5.809475019311125]
31
+ ]
32
+
33
+ t.deepEqual(obs.length, 3)
34
+ t.true(comparePoints(obs, exp))
35
+
36
+ // test AAA
37
+ geometry = triangle({ type: 'AAA', values: [Math.PI / 2, Math.PI / 4, Math.PI / 4] })
38
+ obs = geom2.toPoints(geometry)
39
+ exp = [
40
+ [0, 0],
41
+ [1, 0],
42
+ [0, 1.0000000000000002]
43
+ ]
44
+
45
+ t.deepEqual(obs.length, 3)
46
+ t.true(comparePoints(obs, exp))
47
+
48
+ // test AAS
49
+ geometry = triangle({ type: 'AAS', values: [degToRad(62), degToRad(35), 7] })
50
+ obs = geom2.toPoints(geometry)
51
+ exp = [
52
+ [0, 0],
53
+ [7.86889631692936, 0],
54
+ [2.1348320069064197, 4.015035054457325]
55
+ ]
56
+
57
+ t.deepEqual(obs.length, 3)
58
+ t.true(comparePoints(obs, exp))
59
+
60
+ // test ASA
61
+ geometry = triangle({ type: 'ASA', values: [degToRad(76), 9, degToRad(34)] })
62
+ obs = geom2.toPoints(geometry)
63
+ exp = [
64
+ [0, 0],
65
+ [9, 0],
66
+ [1.295667368233083, 5.196637976713814]
67
+ ]
68
+
69
+ t.deepEqual(obs.length, 3)
70
+ t.true(comparePoints(obs, exp))
71
+
72
+ // test SAS
73
+ geometry = triangle({ type: 'SAS', values: [5, degToRad(49), 7] })
74
+ obs = geom2.toPoints(geometry)
75
+ exp = [
76
+ [0, 0],
77
+ [5, 0],
78
+ [0.4075867970664495, 5.282967061559405]
79
+ ]
80
+
81
+ t.deepEqual(obs.length, 3)
82
+ t.true(comparePoints(obs, exp))
83
+
84
+ // test SSA
85
+ geometry = triangle({ type: 'SSA', values: [8, 13, degToRad(31)] })
86
+ obs = geom2.toPoints(geometry)
87
+ exp = [
88
+ [0, 0],
89
+ [8, 0],
90
+ [8.494946725906148, 12.990574573070846]
91
+ ]
92
+
93
+ t.deepEqual(obs.length, 3)
94
+ t.true(comparePoints(obs, exp))
95
+ })
@@ -1,4 +1,5 @@
1
1
  /**
2
+ * Insert the given element into the give array using the compareFunction.
2
3
  * @alias module:modeling/utils.insertSorted
3
4
  */
4
5
  const insertSorted = (array, element, comparefunc) => {
@@ -8,9 +8,7 @@ const compareVectors = require('./compareVectors')
8
8
  */
9
9
  const comparePolygons = (poly1, poly2) => {
10
10
  if (poly1.vertices.length === poly2.vertices.length) {
11
- return poly1.vertices.reduce((valid, vertex, index) => {
12
- return valid && compareVectors(poly1.vertices[index], poly2.vertices[index])
13
- }, true)
11
+ return poly1.vertices.reduce((valid, vertex, index) => valid && compareVectors(poly1.vertices[index], poly2.vertices[index]), true)
14
12
  }
15
13
  return false
16
14
  }
@@ -1,7 +1,6 @@
1
1
  // Compare two numeric values for near equality.
2
2
  // the given test is fails if the numeric values are outside the given epsilon
3
3
  const nearlyEqual = (t, a, b, epsilon, failMessage) => {
4
- // console.log('nearEqual(t,'+a+','+b+','+epsilon+')')
5
4
  if (a === b) { // shortcut, also handles infinities and NaNs
6
5
  return true
7
6
  }
@@ -10,11 +9,9 @@ const nearlyEqual = (t, a, b, epsilon, failMessage) => {
10
9
  const absB = Math.abs(b)
11
10
  const diff = Math.abs(a - b)
12
11
  if (Number.isNaN(diff)) {
13
- return false
12
+ failMessage = failMessage === undefined ? 'difference is not a number' : failMessage
13
+ t.fail(failMessage + '(' + a + ',' + b + ')')
14
14
  }
15
- // console.log(absA)
16
- // console.log(absB)
17
- // console.log(diff)
18
15
  if (a === 0 || b === 0 || diff < Number.EPSILON) {
19
16
  // a or b is zero or both are extremely close to it
20
17
  // relative error is less meaningful here
@@ -25,7 +22,6 @@ const nearlyEqual = (t, a, b, epsilon, failMessage) => {
25
22
  }
26
23
  // use relative error
27
24
  const relative = (diff / Math.min((absA + absB), Number.MAX_VALUE))
28
- // console.log(relative)
29
25
  if (relative > epsilon) {
30
26
  failMessage = failMessage === undefined ? 'Numbers outside of epsilon' : failMessage
31
27
  t.fail(failMessage + '(' + a + ',' + b + ')')