@jscad/modeling 2.5.2 → 2.7.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 (64) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/jscad-modeling.min.js +217 -202
  3. package/package.json +2 -2
  4. package/src/colors/colorize.js +4 -5
  5. package/src/colors/colorize.test.js +19 -12
  6. package/src/geometries/geom2/applyTransforms.js +1 -1
  7. package/src/geometries/geom2/clone.js +2 -12
  8. package/src/geometries/geom2/transform.js +2 -6
  9. package/src/geometries/geom3/applyTransforms.js +1 -1
  10. package/src/geometries/geom3/clone.js +2 -14
  11. package/src/geometries/geom3/clone.test.js +0 -2
  12. package/src/geometries/geom3/create.js +0 -2
  13. package/src/geometries/geom3/create.test.js +0 -2
  14. package/src/geometries/geom3/fromCompactBinary.js +4 -6
  15. package/src/geometries/geom3/fromToCompactBinary.test.js +0 -6
  16. package/src/geometries/geom3/invert.test.js +0 -2
  17. package/src/geometries/geom3/toCompactBinary.js +8 -10
  18. package/src/geometries/geom3/transform.js +2 -7
  19. package/src/geometries/geom3/transform.test.js +0 -1
  20. package/src/geometries/geom3/type.d.ts +0 -1
  21. package/src/geometries/path2/applyTransforms.js +1 -1
  22. package/src/geometries/path2/clone.js +2 -13
  23. package/src/geometries/path2/transform.js +2 -7
  24. package/src/geometries/poly3/isConvex.js +1 -1
  25. package/src/geometries/poly3/measureArea.js +12 -13
  26. package/src/geometries/poly3/measureArea.test.js +15 -0
  27. package/src/geometries/poly3/plane.js +1 -2
  28. package/src/maths/mat4/index.js +1 -0
  29. package/src/maths/mat4/isOnlyTransformScale.js +21 -0
  30. package/src/maths/mat4/isOnlyTransformScale.test.js +27 -0
  31. package/src/maths/plane/fromPoints.js +32 -10
  32. package/src/maths/plane/fromPoints.test.js +4 -0
  33. package/src/maths/utils/index.d.ts +0 -1
  34. package/src/maths/utils/index.js +0 -1
  35. package/src/maths/vec2/rotate.js +1 -1
  36. package/src/maths/vec2/rotate.test.js +3 -3
  37. package/src/measurements/index.js +4 -1
  38. package/src/measurements/measureBoundingBox.js +128 -38
  39. package/src/measurements/measureBoundingBox.test.js +8 -0
  40. package/src/measurements/measureBoundingSphere.js +146 -0
  41. package/src/measurements/measureBoundingSphere.test.js +59 -0
  42. package/src/measurements/measureCenter.js +28 -0
  43. package/src/measurements/measureCenter.test.js +58 -0
  44. package/src/measurements/measureCenterOfMass.js +106 -0
  45. package/src/measurements/measureCenterOfMass.test.js +58 -0
  46. package/src/measurements/measureDimensions.js +28 -0
  47. package/src/measurements/measureDimensions.test.js +58 -0
  48. package/src/measurements/measureEpsilon.js +3 -9
  49. package/src/operations/booleans/reTesselateCoplanarPolygons.js +1 -1
  50. package/src/operations/booleans/trees/PolygonTreeNode.js +0 -1
  51. package/src/operations/expansions/expand.test.js +1 -1
  52. package/src/operations/extrusions/extrudeRotate.test.js +18 -10
  53. package/src/operations/modifiers/generalize.js +0 -1
  54. package/src/operations/modifiers/snapPolygons.js +2 -2
  55. package/src/operations/modifiers/snapPolygons.test.js +13 -5
  56. package/src/primitives/index.d.ts +1 -0
  57. package/src/primitives/index.js +2 -1
  58. package/src/primitives/triangle.d.ts +10 -0
  59. package/src/primitives/triangle.js +164 -0
  60. package/src/primitives/triangle.test.js +95 -0
  61. package/src/maths/utils/clamp.d.ts +0 -3
  62. package/src/maths/utils/clamp.js +0 -4
  63. package/src/maths/utils/quantizeForSpace.d.ts +0 -3
  64. package/src/maths/utils/quantizeForSpace.js +0 -6
@@ -0,0 +1,106 @@
1
+ const flatten = require('../utils/flatten')
2
+
3
+ const vec3 = require('../maths/vec3')
4
+
5
+ const geom2 = require('../geometries/geom2')
6
+ const geom3 = require('../geometries/geom3')
7
+
8
+ const cacheOfCenterOfMass = new WeakMap()
9
+
10
+ /*
11
+ * Measure the center of mass for the given geometry.
12
+ *
13
+ * @see http://paulbourke.net/geometry/polygonmesh/
14
+ * @return {Array} the center of mass for the geometry
15
+ */
16
+ const measureCenterOfMassGeom2 = (geometry) => {
17
+ let centerOfMass = cacheOfCenterOfMass.get(geometry)
18
+ if (centerOfMass !== undefined) return centerOfMass
19
+
20
+ const sides = geom2.toSides(geometry)
21
+
22
+ let area = 0
23
+ let x = 0
24
+ let y = 0
25
+ if (sides.length > 0) {
26
+ for (let i = 0; i < sides.length; i++) {
27
+ const p1 = sides[i][0]
28
+ const p2 = sides[i][1]
29
+
30
+ const a = p1[0] * p2[1] - p1[1] * p2[0]
31
+ area += a
32
+ x += (p1[0] + p2[0]) * a
33
+ y += (p1[1] + p2[1]) * a
34
+ }
35
+ area /= 2
36
+
37
+ const f = 1 / (area * 6)
38
+ x *= f
39
+ y *= f
40
+ }
41
+
42
+ centerOfMass = vec3.fromValues(x, y, 0)
43
+
44
+ cacheOfCenterOfMass.set(geometry, centerOfMass)
45
+ return centerOfMass
46
+ }
47
+
48
+ /*
49
+ * Measure the center of mass for the given geometry.
50
+ * @return {Array} the center of mass for the geometry
51
+ */
52
+ const measureCenterOfMassGeom3 = (geometry) => {
53
+ let centerOfMass = cacheOfCenterOfMass.get(geometry)
54
+ if (centerOfMass !== undefined) return centerOfMass
55
+
56
+ centerOfMass = vec3.create() // 0, 0, 0
57
+
58
+ const polygons = geom3.toPolygons(geometry)
59
+ if (polygons.length === 0) return centerOfMass
60
+
61
+ let totalVolume = 0
62
+ const vector = vec3.create() // for speed
63
+ polygons.forEach((polygon) => {
64
+ // calculate volume and center of each tetrahedon
65
+ const vertices = polygon.vertices
66
+ for (let i = 0; i < vertices.length - 2; i++) {
67
+ vec3.cross(vector, vertices[i + 1], vertices[i + 2])
68
+ const volume = vec3.dot(vertices[0], vector) / 6
69
+
70
+ totalVolume += volume
71
+
72
+ vec3.add(vector, vertices[0], vertices[i + 1])
73
+ vec3.add(vector, vector, vertices[i + 2])
74
+ const weightedCenter = vec3.scale(vector, vector, 1 / 4 * volume)
75
+
76
+ vec3.add(centerOfMass, centerOfMass, weightedCenter)
77
+ }
78
+ })
79
+ vec3.scale(centerOfMass, centerOfMass, 1 / totalVolume)
80
+
81
+ cacheOfCenterOfMass.set(geometry, centerOfMass)
82
+ return centerOfMass
83
+ }
84
+
85
+ /**
86
+ * Measure the center of mass for the given geometries.
87
+ * @param {...Object} geometries - the geometries to measure
88
+ * @return {Array} the center of mass for each geometry, i.e. [X, Y, Z]
89
+ * @alias module:modeling/measurements.measureCenterOfMass
90
+ *
91
+ * @example
92
+ * let center = measureCenterOfMass(sphere())
93
+ */
94
+ const measureCenterOfMass = (...geometries) => {
95
+ geometries = flatten(geometries)
96
+
97
+ const results = geometries.map((geometry) => {
98
+ // NOTE: center of mass for geometry path2 is not possible
99
+ if (geom2.isA(geometry)) return measureCenterOfMassGeom2(geometry)
100
+ if (geom3.isA(geometry)) return measureCenterOfMassGeom3(geometry)
101
+ return [0, 0, 0]
102
+ })
103
+ return results.length === 1 ? results[0] : results
104
+ }
105
+
106
+ module.exports = measureCenterOfMass
@@ -0,0 +1,58 @@
1
+ const test = require('ava')
2
+
3
+ const { geom2, geom3, path2 } = require('../geometries')
4
+
5
+ const { ellipsoid, line, rectangle, cuboid } = require('../primitives')
6
+
7
+ const { measureCenterOfMass } = require('./index')
8
+
9
+ test('measureCenterOfMass (single objects)', (t) => {
10
+ const aline = line([[10, 10], [15, 15]])
11
+ const arect = rectangle({ center: [5, 5] })
12
+ const acube = cuboid({ size: [3, 3, 3], center: [-15, -5, -10] })
13
+
14
+ const apath2 = path2.create()
15
+ const ageom2 = geom2.create()
16
+ const ageom3 = geom3.create()
17
+
18
+ const n = null
19
+ const o = {}
20
+ const x = 'hi'
21
+
22
+ const lcenter = measureCenterOfMass(aline)
23
+ const rcenter = measureCenterOfMass(arect)
24
+ const ccenter = measureCenterOfMass(acube)
25
+
26
+ const p2center = measureCenterOfMass(apath2)
27
+ const g2center = measureCenterOfMass(ageom2)
28
+ const g3center = measureCenterOfMass(ageom3)
29
+
30
+ const ncenter = measureCenterOfMass(n)
31
+ const ocenter = measureCenterOfMass(o)
32
+ const xcenter = measureCenterOfMass(x)
33
+
34
+ t.deepEqual(lcenter, [0, 0, 0])
35
+ t.deepEqual(rcenter, [5, 5, 0])
36
+ t.deepEqual(ccenter, [-15, -5, -10])
37
+
38
+ t.deepEqual(p2center, [0, 0, 0])
39
+ t.deepEqual(g2center, [0, 0, 0])
40
+ t.deepEqual(g3center, [0, 0, 0])
41
+
42
+ t.deepEqual(ncenter, [0, 0, 0])
43
+ t.deepEqual(ocenter, [0, 0, 0])
44
+ t.deepEqual(xcenter, [0, 0, 0])
45
+ })
46
+
47
+ test('measureCenterOfMass (multiple objects)', (t) => {
48
+ const aline = line([[10, 10], [15, 15]])
49
+ const arect = rectangle({ size: [10, 20], center: [10, -10] })
50
+ const asphere = ellipsoid({ radius: [5, 10, 15], center: [5, -5, 50] })
51
+ const o = {}
52
+
53
+ let allcenters = measureCenterOfMass(aline, arect, asphere, o)
54
+ t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.99999999999999, -5.000000000000007, 49.99999999999992], [0, 0, 0]])
55
+
56
+ allcenters = measureCenterOfMass(aline, arect, asphere, o)
57
+ t.deepEqual(allcenters, [[0, 0, 0], [10, -10, 0], [4.99999999999999, -5.000000000000007, 49.99999999999992], [0, 0, 0]])
58
+ })
@@ -0,0 +1,28 @@
1
+ const flatten = require('../utils/flatten')
2
+
3
+ const measureBoundingBox = require('./measureBoundingBox')
4
+
5
+ /**
6
+ * Measure the dimensions of the given geometries.
7
+ * @param {...Object} geometries - the geometries to measure
8
+ * @return {Array} the dimensions for each geometry, i.e. [width, depth, height]
9
+ * @alias module:modeling/measurements.measureDimensions
10
+ *
11
+ * @example
12
+ * let dimensions = measureDimensions(sphere())
13
+ */
14
+ const measureDimensions = (...geometries) => {
15
+ geometries = flatten(geometries)
16
+
17
+ const results = geometries.map((geometry) => {
18
+ const boundingBox = measureBoundingBox(geometry)
19
+ return [
20
+ boundingBox[1][0] - boundingBox[0][0],
21
+ boundingBox[1][1] - boundingBox[0][1],
22
+ boundingBox[1][2] - boundingBox[0][2]
23
+ ]
24
+ })
25
+ return results.length === 1 ? results[0] : results
26
+ }
27
+
28
+ module.exports = measureDimensions
@@ -0,0 +1,58 @@
1
+ const test = require('ava')
2
+
3
+ const { geom2, geom3, path2 } = require('../geometries')
4
+
5
+ const { line, rectangle, cuboid } = require('../primitives')
6
+
7
+ const { measureDimensions } = require('./index')
8
+
9
+ test('measureDimensions (single objects)', (t) => {
10
+ const aline = line([[10, 10], [15, 15]])
11
+ const arect = rectangle()
12
+ const acube = cuboid()
13
+
14
+ const apath2 = path2.create()
15
+ const ageom2 = geom2.create()
16
+ const ageom3 = geom3.create()
17
+
18
+ const n = null
19
+ const o = {}
20
+ const x = 'hi'
21
+
22
+ const lbounds = measureDimensions(aline)
23
+ const rbounds = measureDimensions(arect)
24
+ const cbounds = measureDimensions(acube)
25
+
26
+ const p2bounds = measureDimensions(apath2)
27
+ const g2bounds = measureDimensions(ageom2)
28
+ const g3bounds = measureDimensions(ageom3)
29
+
30
+ const nbounds = measureDimensions(n)
31
+ const obounds = measureDimensions(o)
32
+ const xbounds = measureDimensions(x)
33
+
34
+ t.deepEqual(lbounds, [5, 5, 0])
35
+ t.deepEqual(rbounds, [2, 2, 0])
36
+ t.deepEqual(cbounds, [2, 2, 2])
37
+
38
+ t.deepEqual(p2bounds, [0, 0, 0])
39
+ t.deepEqual(g2bounds, [0, 0, 0])
40
+ t.deepEqual(g3bounds, [0, 0, 0])
41
+
42
+ t.deepEqual(nbounds, [0, 0, 0])
43
+ t.deepEqual(obounds, [0, 0, 0])
44
+ t.deepEqual(xbounds, [0, 0, 0])
45
+ })
46
+
47
+ test('measureDimensions (multiple objects)', (t) => {
48
+ const aline = line([[10, 10], [15, 15]])
49
+ const arect = rectangle({ size: [10, 20] })
50
+ const acube = cuboid()
51
+ const o = {}
52
+
53
+ let allbounds = measureDimensions(aline, arect, acube, o)
54
+ t.deepEqual(allbounds, [[5, 5, 0], [10, 20, 0], [2, 2, 2], [0, 0, 0]])
55
+
56
+ allbounds = measureDimensions(aline, arect, acube, o)
57
+ t.deepEqual(allbounds, [[5, 5, 0], [10, 20, 0], [2, 2, 2], [0, 0, 0]])
58
+ })
@@ -8,25 +8,19 @@ const measureBoundingBox = require('./measureBoundingBox')
8
8
  * Measure the epsilon of the given (path2) geometry.
9
9
  * @return {Number} the epsilon (precision) of the geometry
10
10
  */
11
- const measureEpsilonOfPath2 = (geometry) => {
12
- return calculateEpsilonFromBounds(measureBoundingBox(geometry), 2)
13
- }
11
+ const measureEpsilonOfPath2 = (geometry) => calculateEpsilonFromBounds(measureBoundingBox(geometry), 2)
14
12
 
15
13
  /*
16
14
  * Measure the epsilon of the given (geom2) geometry.
17
15
  * @return {Number} the epsilon (precision) of the geometry
18
16
  */
19
- const measureEpsilonOfGeom2 = (geometry) => {
20
- return calculateEpsilonFromBounds(measureBoundingBox(geometry), 2)
21
- }
17
+ const measureEpsilonOfGeom2 = (geometry) => calculateEpsilonFromBounds(measureBoundingBox(geometry), 2)
22
18
 
23
19
  /*
24
20
  * Measure the epsilon of the given (geom3) geometry.
25
21
  * @return {Float} the epsilon (precision) of the geometry
26
22
  */
27
- const measureEpsilonOfGeom3 = (geometry) => {
28
- return calculateEpsilonFromBounds(measureBoundingBox(geometry), 3)
29
- }
23
+ const measureEpsilonOfGeom3 = (geometry) => calculateEpsilonFromBounds(measureBoundingBox(geometry), 3)
30
24
 
31
25
  /**
32
26
  * Measure the epsilon of the given geometries.
@@ -309,7 +309,7 @@ const reTesselateCoplanarPolygons = (sourcepolygons) => {
309
309
  const polygon = poly3.fromPointsAndPlane(vertices3d, plane) // TODO support shared
310
310
 
311
311
  // if we let empty polygon out, next retesselate will crash
312
- if(polygon.vertices.length) destpolygons.push(polygon)
312
+ if (polygon.vertices.length) destpolygons.push(polygon)
313
313
  }
314
314
  }
315
315
  } // if(yindex > 0)
@@ -20,7 +20,6 @@ const splitPolygonByPlane = require('./splitPolygonByPlane')
20
20
  // remove() removes a polygon from the tree. Once a polygon is removed, the parent polygons are invalidated
21
21
  // since they are no longer intact.
22
22
  class PolygonTreeNode {
23
-
24
23
  // constructor creates the root node
25
24
  constructor () {
26
25
  this.parent = null
@@ -120,7 +120,7 @@ test('expand: expanding of a geom3 produces expected changes to polygons', (t) =
120
120
  const geometry2 = sphere({ radius: 5, segments: 8 })
121
121
  const obs2 = expand({ delta: 5 }, geometry2)
122
122
  const pts2 = geom3.toPoints(obs2)
123
- t.is(pts2.length, 2059)
123
+ t.is(pts2.length, 2065)
124
124
  })
125
125
 
126
126
  test('expand (options): offsetting of a complex geom2 produces expected offset geom2', (t) => {
@@ -107,10 +107,14 @@ 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
- [[-4.898587196589414e-16, 0, 8], [-6.123233995736777e-17, 6.999999999999999, 8],
111
- [9.18485099360515e-16, 7.000000000000001, -8], [4.898587196589413e-16, 0, -8]],
112
- [[7, 4.898587196589413e-16, 8], [0, 4.898587196589413e-16, 8],
113
- [0, -4.898587196589413e-16, -8], [7, -4.898587196589413e-16, -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
+ ]
114
118
  ]
115
119
  t.is(pts.length, 6)
116
120
  t.true(comparePolygonsAsPoints(pts, exp))
@@ -133,12 +137,16 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
133
137
  [[0.7071067811865472, 0.7071067811865478, 8], [1.414213562373095, 1.4142135623730951, 4], [-1.2246467991473532e-16, 2, 4]],
134
138
  [[0.7071067811865472, 0.7071067811865478, 8], [-1.2246467991473532e-16, 2, 4], [-4.286263797015736e-16, 1, 8]],
135
139
  [[-3.4638242249419727e-16, 3.4638242249419736e-16, 8], [0.7071067811865472, 0.7071067811865478, 8], [-4.286263797015736e-16, 1, 8]],
136
- [[-4.898587196589412e-16, 0, 8], [-4.2862637970157346e-16, 0.9999999999999998, 8],
137
- [-1.2246467991473475e-16, 2.0000000000000004, 3.9999999999999964], [5.510910596163092e-16, 1.0000000000000004, -8],
138
- [ 4.898587196589414e-16, 0, -8]],
139
- [[0, -4.898587196589413e-16, -8.000000000000002], [1.0000000000000027, -4.898587196589413e-16, -8.000000000000002],
140
- [2.000000000000001, 2.449293598294702e-16, 3.9999999999999964], [1.0000000000000004, 4.898587196589411e-16, 8],
141
- [0, 4.898587196589411e-16, 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
+ ]
142
150
  ]
143
151
  t.is(pts.length, 14)
144
152
  t.true(comparePolygonsAsPoints(pts, exp))
@@ -19,7 +19,6 @@ const generalizePath2 = (options, geometry) => {
19
19
  return geometry
20
20
  }
21
21
 
22
-
23
22
  /*
24
23
  */
25
24
  const generalizeGeom2 = (options, geometry) => {
@@ -3,7 +3,7 @@ const vec3 = require('../../maths/vec3')
3
3
  const poly3 = require('../../geometries/poly3')
4
4
 
5
5
  const isValidPoly3 = (epsilon, polygon) => {
6
- const area = poly3.measureArea(polygon)
6
+ const area = Math.abs(poly3.measureArea(polygon))
7
7
  return (Number.isFinite(area) && area > epsilon)
8
8
  }
9
9
 
@@ -17,7 +17,7 @@ const snapPolygons = (epsilon, polygons) => {
17
17
  const newvertices = []
18
18
  for (let i = 0; i < snapvertices.length; i++) {
19
19
  const j = (i + 1) % snapvertices.length
20
- if (! vec3.equals(snapvertices[i], snapvertices[j])) newvertices.push(snapvertices[i])
20
+ if (!vec3.equals(snapvertices[i], snapvertices[j])) newvertices.push(snapvertices[i])
21
21
  }
22
22
  const newpolygon = poly3.create(newvertices)
23
23
  if (polygon.color) newpolygon.color = polygon.color
@@ -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
  })
@@ -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
  }
@@ -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: [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