@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.
- package/CHANGELOG.md +57 -0
- package/dist/jscad-modeling.min.js +217 -202
- package/package.json +2 -2
- package/src/colors/colorize.js +4 -5
- package/src/colors/colorize.test.js +19 -12
- package/src/geometries/geom2/applyTransforms.js +1 -1
- package/src/geometries/geom2/clone.js +2 -12
- package/src/geometries/geom2/transform.js +2 -6
- package/src/geometries/geom3/applyTransforms.js +1 -1
- package/src/geometries/geom3/clone.js +2 -14
- package/src/geometries/geom3/clone.test.js +0 -2
- package/src/geometries/geom3/create.js +0 -2
- package/src/geometries/geom3/create.test.js +0 -2
- package/src/geometries/geom3/fromCompactBinary.js +4 -6
- package/src/geometries/geom3/fromToCompactBinary.test.js +0 -6
- package/src/geometries/geom3/invert.test.js +0 -2
- package/src/geometries/geom3/toCompactBinary.js +8 -10
- package/src/geometries/geom3/transform.js +2 -7
- package/src/geometries/geom3/transform.test.js +0 -1
- package/src/geometries/geom3/type.d.ts +0 -1
- package/src/geometries/path2/applyTransforms.js +1 -1
- package/src/geometries/path2/clone.js +2 -13
- package/src/geometries/path2/transform.js +2 -7
- package/src/geometries/poly3/isConvex.js +1 -1
- package/src/geometries/poly3/measureArea.js +12 -13
- package/src/geometries/poly3/measureArea.test.js +15 -0
- package/src/geometries/poly3/plane.js +1 -2
- package/src/maths/mat4/index.js +1 -0
- package/src/maths/mat4/isOnlyTransformScale.js +21 -0
- package/src/maths/mat4/isOnlyTransformScale.test.js +27 -0
- package/src/maths/plane/fromPoints.js +32 -10
- package/src/maths/plane/fromPoints.test.js +4 -0
- package/src/maths/utils/index.d.ts +0 -1
- package/src/maths/utils/index.js +0 -1
- package/src/maths/vec2/rotate.js +1 -1
- package/src/maths/vec2/rotate.test.js +3 -3
- package/src/measurements/index.js +4 -1
- package/src/measurements/measureBoundingBox.js +128 -38
- package/src/measurements/measureBoundingBox.test.js +8 -0
- package/src/measurements/measureBoundingSphere.js +146 -0
- package/src/measurements/measureBoundingSphere.test.js +59 -0
- package/src/measurements/measureCenter.js +28 -0
- package/src/measurements/measureCenter.test.js +58 -0
- package/src/measurements/measureCenterOfMass.js +106 -0
- package/src/measurements/measureCenterOfMass.test.js +58 -0
- package/src/measurements/measureDimensions.js +28 -0
- package/src/measurements/measureDimensions.test.js +58 -0
- package/src/measurements/measureEpsilon.js +3 -9
- package/src/operations/booleans/reTesselateCoplanarPolygons.js +1 -1
- package/src/operations/booleans/trees/PolygonTreeNode.js +0 -1
- package/src/operations/expansions/expand.test.js +1 -1
- package/src/operations/extrusions/extrudeRotate.test.js +18 -10
- package/src/operations/modifiers/generalize.js +0 -1
- package/src/operations/modifiers/snapPolygons.js +2 -2
- package/src/operations/modifiers/snapPolygons.test.js +13 -5
- package/src/primitives/index.d.ts +1 -0
- package/src/primitives/index.js +2 -1
- package/src/primitives/triangle.d.ts +10 -0
- package/src/primitives/triangle.js +164 -0
- package/src/primitives/triangle.test.js +95 -0
- package/src/maths/utils/clamp.d.ts +0 -3
- package/src/maths/utils/clamp.js +0 -4
- package/src/maths/utils/quantizeForSpace.d.ts +0 -3
- 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,
|
|
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
|
-
[
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
[
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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))
|
|
@@ -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 (!
|
|
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,
|
|
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
|
package/src/primitives/index.js
CHANGED
|
@@ -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
|