@jscad/modeling 2.11.1 → 2.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/jscad-modeling.min.js +338 -335
- package/package.json +2 -2
- package/src/curves/bezier/arcLengthToT.js +6 -6
- package/src/curves/bezier/arcLengthToT.test.js +25 -25
- package/src/curves/bezier/length.js +3 -5
- package/src/curves/bezier/length.test.js +2 -2
- package/src/curves/bezier/lengths.js +10 -10
- package/src/curves/bezier/lengths.test.js +3 -3
- package/src/geometries/geom2/transform.js +9 -1
- package/src/geometries/geom2/transform.test.js +58 -1
- package/src/geometries/poly2/arePointsInside.js +0 -7
- package/src/geometries/poly3/measureBoundingSphere.js +1 -2
- package/src/maths/plane/fromNoisyPoints.d.ts +6 -0
- package/src/maths/plane/fromNoisyPoints.js +106 -0
- package/src/maths/plane/fromNoisyPoints.test.js +24 -0
- package/src/maths/plane/index.d.ts +1 -0
- package/src/maths/plane/index.js +1 -0
- package/src/operations/booleans/trees/PolygonTreeNode.js +1 -1
- package/src/operations/expansions/expand.test.js +1 -1
- package/src/operations/extrusions/extrudeHelical.js +6 -7
- package/src/operations/extrusions/extrudeHelical.test.js +35 -37
- package/src/operations/extrusions/extrudeRotate.js +1 -1
- package/src/operations/extrusions/index.d.ts +1 -0
- package/src/operations/hulls/hullPoints2.js +3 -2
- package/src/operations/modifiers/index.js +1 -1
- package/src/operations/modifiers/retessellate.js +66 -27
- package/src/operations/transforms/mirror.test.js +9 -3
- package/src/primitives/circle.js +2 -2
- package/src/primitives/circle.test.js +7 -0
- package/src/primitives/cube.js +2 -2
- package/src/primitives/cube.test.js +7 -0
- package/src/primitives/cuboid.js +4 -1
- package/src/primitives/cuboid.test.js +7 -0
- package/src/primitives/cylinder.js +7 -2
- package/src/primitives/cylinder.test.js +14 -0
- package/src/primitives/ellipse.js +4 -1
- package/src/primitives/ellipse.test.js +7 -0
- package/src/primitives/ellipsoid.js +4 -1
- package/src/primitives/ellipsoid.test.js +7 -0
- package/src/primitives/geodesicSphere.js +5 -2
- package/src/primitives/geodesicSphere.test.js +7 -0
- package/src/primitives/polygon.d.ts +1 -0
- package/src/primitives/polygon.js +15 -4
- package/src/primitives/polygon.test.js +10 -0
- package/src/primitives/rectangle.js +4 -1
- package/src/primitives/rectangle.test.js +7 -0
- package/src/primitives/roundedCuboid.js +10 -3
- package/src/primitives/roundedCuboid.test.js +14 -0
- package/src/primitives/roundedCylinder.js +12 -5
- package/src/primitives/roundedCylinder.test.js +21 -0
- package/src/primitives/roundedRectangle.js +10 -3
- package/src/primitives/roundedRectangle.test.js +14 -0
- package/src/primitives/sphere.js +2 -2
- package/src/primitives/sphere.test.js +7 -0
- package/src/primitives/square.js +2 -2
- package/src/primitives/square.test.js +7 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const test = require('ava')
|
|
2
|
-
const { comparePoints, comparePolygonsAsPoints } = require('../../../test/helpers')
|
|
3
2
|
const { TAU } = require('../../maths/constants')
|
|
4
3
|
const { geom2, geom3 } = require('../../geometries')
|
|
5
4
|
const { circle } = require('../../primitives')
|
|
@@ -7,56 +6,55 @@ const { circle } = require('../../primitives')
|
|
|
7
6
|
const { extrudeHelical } = require('./index')
|
|
8
7
|
|
|
9
8
|
test('extrudeHelical: (defaults) extruding of a geom2 produces an expected geom3', (t) => {
|
|
10
|
-
|
|
9
|
+
const geometry2 = geom2.fromPoints([[10, 8], [10, -8], [26, -8], [26, 8]])
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
t.notThrows(() => geom3.validate(geometry3))
|
|
11
|
+
const geometry3 = extrudeHelical({}, geometry2)
|
|
12
|
+
t.notThrows(() => geom3.validate(geometry3))
|
|
15
13
|
})
|
|
16
14
|
|
|
17
15
|
test('extrudeHelical: (defaults) extruding of a circle produces an expected geom3', (t) => {
|
|
18
|
-
|
|
16
|
+
const geometry2 = circle({ size: 3, center: [10, 0] })
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const geometry3 = extrudeHelical({}, geometry2)
|
|
19
|
+
t.notThrows(() => geom3.validate(geometry3))
|
|
22
20
|
})
|
|
23
21
|
|
|
24
22
|
test('extrudeHelical: (angle) extruding of a circle produces an expected geom3', (t) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
const maxRevolutions = 10
|
|
24
|
+
const geometry2 = circle({ size: 3, center: [10, 0] })
|
|
25
|
+
for (const index of [...Array(maxRevolutions).keys()]) {
|
|
26
|
+
// also test negative angles
|
|
27
|
+
const geometry3 = extrudeHelical({ angle: TAU * (index - maxRevolutions / 2) }, geometry2)
|
|
28
|
+
t.notThrows(() => geom3.validate(geometry3))
|
|
29
|
+
}
|
|
32
30
|
})
|
|
33
31
|
|
|
34
32
|
test('extrudeHelical: (pitch) extruding of a circle produces an expected geom3', (t) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
const startPitch = -10
|
|
34
|
+
const geometry2 = circle({ size: 3, center: [10, 0] })
|
|
35
|
+
for (const index of [...Array(20).keys()]) {
|
|
36
|
+
// also test negative pitches
|
|
37
|
+
const geometry3 = extrudeHelical({ pitch: startPitch + index }, geometry2)
|
|
38
|
+
t.notThrows(() => geom3.validate(geometry3))
|
|
39
|
+
}
|
|
42
40
|
})
|
|
43
41
|
|
|
44
42
|
test('extrudeHelical: (endRadiusOffset) extruding of a circle produces an expected geom3', (t) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
const startOffset = -5
|
|
44
|
+
const geometry2 = circle({ size: 3, center: [10, 0] })
|
|
45
|
+
for (const index of [...Array(10).keys()]) {
|
|
46
|
+
// also test negative pitches
|
|
47
|
+
const geometry3 = extrudeHelical({ endRadiusOffset: startOffset + index }, geometry2)
|
|
48
|
+
t.notThrows(() => geom3.validate(geometry3))
|
|
49
|
+
}
|
|
52
50
|
})
|
|
53
51
|
|
|
54
52
|
test('extrudeHelical: (segments) extruding of a circle produces an expected geom3', (t) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
53
|
+
const startSegments = 3
|
|
54
|
+
const geometry2 = circle({ size: 3, center: [10, 0] })
|
|
55
|
+
for (const index of [...Array(30).keys()]) {
|
|
56
|
+
// also test negative pitches
|
|
57
|
+
const geometry3 = extrudeHelical({ segments: startSegments + index }, geometry2)
|
|
58
|
+
t.notThrows(() => geom3.validate(geometry3))
|
|
59
|
+
}
|
|
60
|
+
})
|
|
@@ -93,7 +93,7 @@ const extrudeRotate = (options, geometry) => {
|
|
|
93
93
|
return [point0, point1]
|
|
94
94
|
})
|
|
95
95
|
// recreate the geometry from the (-) capped points
|
|
96
|
-
geometry = geom2.
|
|
96
|
+
geometry = geom2.create(shapeSides)
|
|
97
97
|
geometry = mirrorX(geometry)
|
|
98
98
|
} else if (pointsWithPositiveX.length >= pointsWithNegativeX.length) {
|
|
99
99
|
shapeSides = shapeSides.map((side) => {
|
|
@@ -2,6 +2,7 @@ export { default as extrudeFromSlices, ExtrudeFromSlicesOptions } from './extrud
|
|
|
2
2
|
export { default as extrudeLinear, ExtrudeLinearOptions } from './extrudeLinear'
|
|
3
3
|
export { default as extrudeRectangular, ExtrudeRectangularOptions } from './extrudeRectangular'
|
|
4
4
|
export { default as extrudeRotate, ExtrudeRotateOptions } from './extrudeRotate'
|
|
5
|
+
export { default as extrudeHelical, ExtrudeHelicalOptions } from './extrudeHelical'
|
|
5
6
|
export { default as project, ProjectOptions } from './project'
|
|
6
7
|
export * as slice from './slice'
|
|
7
8
|
|
|
@@ -25,8 +25,9 @@ const hullPoints2 = (uniquePoints) => {
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
// sort by polar coordinates
|
|
28
|
-
points.sort((pt1, pt2) => pt1.angle
|
|
29
|
-
|
|
28
|
+
points.sort((pt1, pt2) => pt1.angle !== pt2.angle
|
|
29
|
+
? pt1.angle - pt2.angle
|
|
30
|
+
: pt1.distSq - pt2.distSq)
|
|
30
31
|
|
|
31
32
|
const stack = [] // start with empty stack
|
|
32
33
|
points.forEach((point) => {
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
const geom3 = require('../../geometries/geom3')
|
|
2
2
|
const poly3 = require('../../geometries/poly3')
|
|
3
|
-
|
|
4
|
-
const aboutEqualNormals = require('../../maths/utils/aboutEqualNormals')
|
|
5
|
-
|
|
3
|
+
const { NEPS } = require('../../maths/constants')
|
|
6
4
|
const reTesselateCoplanarPolygons = require('./reTesselateCoplanarPolygons')
|
|
7
5
|
|
|
8
|
-
const coplanar = (plane1, plane2) => {
|
|
9
|
-
// expect the same distance from the origin, within tolerance
|
|
10
|
-
if (Math.abs(plane1[3] - plane2[3]) < 0.00000015) {
|
|
11
|
-
return aboutEqualNormals(plane1, plane2)
|
|
12
|
-
}
|
|
13
|
-
return false
|
|
14
|
-
}
|
|
15
|
-
|
|
16
6
|
/*
|
|
17
7
|
After boolean operations all coplanar polygon fragments are joined by a retesselating
|
|
18
8
|
operation. geom3.reTesselate(geom).
|
|
@@ -26,29 +16,78 @@ const retessellate = (geometry) => {
|
|
|
26
16
|
return geometry
|
|
27
17
|
}
|
|
28
18
|
|
|
29
|
-
const polygons = geom3.toPolygons(geometry)
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
const polygons = geom3.toPolygons(geometry).map((polygon, index) => ({ vertices: polygon.vertices, plane: poly3.plane(polygon), index: index }))
|
|
20
|
+
const classified = classifyPolygons(polygons)
|
|
21
|
+
|
|
22
|
+
const destPolygons = []
|
|
23
|
+
classified.forEach((group) => {
|
|
24
|
+
if (Array.isArray(group)) {
|
|
25
|
+
const reTessellateCoplanarPolygons = reTesselateCoplanarPolygons(group)
|
|
26
|
+
destPolygons.push(...reTessellateCoplanarPolygons)
|
|
36
27
|
} else {
|
|
37
|
-
|
|
28
|
+
destPolygons.push(group)
|
|
38
29
|
}
|
|
39
30
|
})
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
polygonsPerPlane.forEach((mapping) => {
|
|
43
|
-
const sourcepolygons = mapping[1]
|
|
44
|
-
const retesselayedpolygons = reTesselateCoplanarPolygons(sourcepolygons)
|
|
45
|
-
destpolygons = destpolygons.concat(retesselayedpolygons)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
const result = geom3.create(destpolygons)
|
|
32
|
+
const result = geom3.create(destPolygons)
|
|
49
33
|
result.isRetesselated = true
|
|
50
34
|
|
|
51
35
|
return result
|
|
52
36
|
}
|
|
53
37
|
|
|
38
|
+
const classifyPolygons = (polygons) => {
|
|
39
|
+
let clusters = [polygons] // a cluster is an array of potentially coplanar polygons
|
|
40
|
+
const nonCoplanar = [] // polygons that are known to be non-coplanar
|
|
41
|
+
// go through each component of the plane starting with the last one (the distance from origin)
|
|
42
|
+
for (let component = 3; component >= 0; component--) {
|
|
43
|
+
const maybeCoplanar = []
|
|
44
|
+
const tolerance = component === 3 ? 0.000000015 : NEPS
|
|
45
|
+
clusters.forEach((cluster) => {
|
|
46
|
+
// sort the cluster by the current component
|
|
47
|
+
cluster.sort(byPlaneComponent(component, tolerance))
|
|
48
|
+
// iterate through the cluster and check if there are polygons which are not coplanar with the others
|
|
49
|
+
// or if there are sub-clusters of coplanar polygons
|
|
50
|
+
let startIndex = 0
|
|
51
|
+
for (let i = 1; i < cluster.length; i++) {
|
|
52
|
+
// if there's a difference larger than the tolerance, split the cluster
|
|
53
|
+
if (cluster[i].plane[component] - cluster[startIndex].plane[component] > tolerance) {
|
|
54
|
+
// if there's a single polygon it's definitely not coplanar with any others
|
|
55
|
+
if (i - startIndex === 1) {
|
|
56
|
+
nonCoplanar.push(cluster[startIndex])
|
|
57
|
+
} else { // we have a new sub cluster of potentially coplanar polygons
|
|
58
|
+
maybeCoplanar.push(cluster.slice(startIndex, i))
|
|
59
|
+
}
|
|
60
|
+
startIndex = i
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// handle the last elements of the cluster
|
|
64
|
+
if (cluster.length - startIndex === 1) {
|
|
65
|
+
nonCoplanar.push(cluster[startIndex])
|
|
66
|
+
} else {
|
|
67
|
+
maybeCoplanar.push(cluster.slice(startIndex))
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
// replace previous clusters with the new ones
|
|
71
|
+
clusters = maybeCoplanar
|
|
72
|
+
}
|
|
73
|
+
// restore the original order of the polygons
|
|
74
|
+
const result = []
|
|
75
|
+
// polygons inside the cluster should already be sorted by index
|
|
76
|
+
clusters.forEach((cluster) => {
|
|
77
|
+
if (cluster[0]) result[cluster[0].index] = cluster
|
|
78
|
+
})
|
|
79
|
+
nonCoplanar.forEach((polygon) => { result[polygon.index] = polygon })
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const byPlaneComponent = (component, tolerance) => (a, b) => {
|
|
85
|
+
if (a.plane[component] - b.plane[component] > tolerance) {
|
|
86
|
+
return 1
|
|
87
|
+
} else if (b.plane[component] - a.plane[component] > tolerance) {
|
|
88
|
+
return -1
|
|
89
|
+
}
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
module.exports = retessellate
|
|
@@ -2,6 +2,8 @@ const test = require('ava')
|
|
|
2
2
|
|
|
3
3
|
const { comparePoints, comparePolygonsAsPoints } = require('../../../test/helpers')
|
|
4
4
|
|
|
5
|
+
const { measureArea } = require('../../measurements')
|
|
6
|
+
|
|
5
7
|
const { geom2, geom3, path2 } = require('../../geometries')
|
|
6
8
|
|
|
7
9
|
const { mirror, mirrorX, mirrorY, mirrorZ } = require('./index')
|
|
@@ -40,25 +42,29 @@ test('mirror: mirroring of geom2 about X/Y produces expected changes to points',
|
|
|
40
42
|
// mirror about X
|
|
41
43
|
let mirrored = mirror({ normal: [1, 0, 0] }, geometry)
|
|
42
44
|
let obs = geom2.toPoints(mirrored)
|
|
43
|
-
let exp = [[
|
|
45
|
+
let exp = [[0, 5], [5, -5], [-10, -5]]
|
|
44
46
|
t.notThrows(() => geom2.validate(mirrored))
|
|
47
|
+
t.is(measureArea(mirrored), measureArea(geometry))
|
|
45
48
|
t.true(comparePoints(obs, exp))
|
|
46
49
|
|
|
47
50
|
mirrored = mirrorX(geometry)
|
|
48
51
|
obs = geom2.toPoints(mirrored)
|
|
49
52
|
t.notThrows(() => geom2.validate(mirrored))
|
|
53
|
+
t.is(measureArea(mirrored), measureArea(geometry))
|
|
50
54
|
t.true(comparePoints(obs, exp))
|
|
51
55
|
|
|
52
56
|
// mirror about Y
|
|
53
57
|
mirrored = mirror({ normal: [0, 1, 0] }, geometry)
|
|
54
58
|
obs = geom2.toPoints(mirrored)
|
|
55
|
-
exp = [[
|
|
59
|
+
exp = [[0, -5], [-5, 5], [10, 5]]
|
|
56
60
|
t.notThrows(() => geom2.validate(mirrored))
|
|
61
|
+
t.is(measureArea(mirrored), measureArea(geometry))
|
|
57
62
|
t.true(comparePoints(obs, exp))
|
|
58
63
|
|
|
59
64
|
mirrored = mirrorY(geometry)
|
|
60
65
|
obs = geom2.toPoints(mirrored)
|
|
61
66
|
t.notThrows(() => geom2.validate(mirrored))
|
|
67
|
+
t.is(measureArea(mirrored), measureArea(geometry))
|
|
62
68
|
t.true(comparePoints(obs, exp))
|
|
63
69
|
})
|
|
64
70
|
|
|
@@ -146,7 +152,7 @@ test('mirror: mirroring of multiple objects produces an array of mirrored object
|
|
|
146
152
|
t.true(comparePoints(obs, exp))
|
|
147
153
|
|
|
148
154
|
obs = geom2.toPoints(mirrored[2])
|
|
149
|
-
exp = [[
|
|
155
|
+
exp = [[0, -5], [-5, 5], [10, 5]]
|
|
150
156
|
t.notThrows(() => geom2.validate(mirrored[2]))
|
|
151
157
|
t.true(comparePoints(obs, exp))
|
|
152
158
|
})
|
package/src/primitives/circle.js
CHANGED
|
@@ -2,7 +2,7 @@ const { TAU } = require('../maths/constants')
|
|
|
2
2
|
|
|
3
3
|
const ellipse = require('./ellipse')
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { isGTE } = require('./commonChecks')
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Construct a circle in two dimensional space where all points are at the same distance from the center.
|
|
@@ -28,7 +28,7 @@ const circle = (options) => {
|
|
|
28
28
|
}
|
|
29
29
|
let { center, radius, startAngle, endAngle, segments } = Object.assign({}, defaults, options)
|
|
30
30
|
|
|
31
|
-
if (!
|
|
31
|
+
if (!isGTE(radius, 0)) throw new Error('radius must be positive')
|
|
32
32
|
|
|
33
33
|
radius = [radius, radius]
|
|
34
34
|
|
|
@@ -146,3 +146,10 @@ test('circle (options)', (t) => {
|
|
|
146
146
|
t.deepEqual(pts.length, 5)
|
|
147
147
|
t.true(comparePoints(pts, exp))
|
|
148
148
|
})
|
|
149
|
+
|
|
150
|
+
test('circle (radius zero)', (t) => {
|
|
151
|
+
const geometry = circle({ radius: 0 })
|
|
152
|
+
const pts = geom2.toPoints(geometry)
|
|
153
|
+
t.notThrows(() => geom2.validate(geometry))
|
|
154
|
+
t.is(pts.length, 0)
|
|
155
|
+
})
|
package/src/primitives/cube.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const cuboid = require('./cuboid')
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { isGTE } = require('./commonChecks')
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Construct an axis-aligned solid cube in three dimensional space with six square faces.
|
|
@@ -20,7 +20,7 @@ const cube = (options) => {
|
|
|
20
20
|
}
|
|
21
21
|
let { center, size } = Object.assign({}, defaults, options)
|
|
22
22
|
|
|
23
|
-
if (!
|
|
23
|
+
if (!isGTE(size, 0)) throw new Error('size must be positive')
|
|
24
24
|
|
|
25
25
|
size = [size, size, size]
|
|
26
26
|
|
|
@@ -46,3 +46,10 @@ test('cube (options)', (t) => {
|
|
|
46
46
|
t.is(pts.length, 6)
|
|
47
47
|
t.true(comparePolygonsAsPoints(pts, exp))
|
|
48
48
|
})
|
|
49
|
+
|
|
50
|
+
test('cube (zero size)', (t) => {
|
|
51
|
+
const obs = cube({ size: 0 })
|
|
52
|
+
const pts = geom3.toPoints(obs)
|
|
53
|
+
t.notThrows(() => geom3.validate(obs))
|
|
54
|
+
t.is(pts.length, 0)
|
|
55
|
+
})
|
package/src/primitives/cuboid.js
CHANGED
|
@@ -23,7 +23,10 @@ const cuboid = (options) => {
|
|
|
23
23
|
|
|
24
24
|
if (!isNumberArray(center, 3)) throw new Error('center must be an array of X, Y and Z values')
|
|
25
25
|
if (!isNumberArray(size, 3)) throw new Error('size must be an array of width, depth and height values')
|
|
26
|
-
if (!size.every((n) => n
|
|
26
|
+
if (!size.every((n) => n >= 0)) throw new Error('size values must be positive')
|
|
27
|
+
|
|
28
|
+
// if any size is zero return empty geometry
|
|
29
|
+
if (size[0] === 0 || size[1] === 0 || size[2] === 0) return geom3.create()
|
|
27
30
|
|
|
28
31
|
const result = geom3.create(
|
|
29
32
|
// adjust a basic shape to size
|
|
@@ -55,3 +55,10 @@ test('cuboid (options)', (t) => {
|
|
|
55
55
|
t.is(pts.length, 6)
|
|
56
56
|
t.true(comparePolygonsAsPoints(pts, exp))
|
|
57
57
|
})
|
|
58
|
+
|
|
59
|
+
test('cuboid (zero size)', (t) => {
|
|
60
|
+
const obs = cuboid({ size: [1, 1, 0] })
|
|
61
|
+
const pts = geom3.toPoints(obs)
|
|
62
|
+
t.notThrows(() => geom3.validate(obs))
|
|
63
|
+
t.is(pts.length, 0)
|
|
64
|
+
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
const geom3 = require('../geometries/geom3')
|
|
2
|
+
|
|
1
3
|
const cylinderElliptic = require('./cylinderElliptic')
|
|
2
4
|
|
|
3
|
-
const {
|
|
5
|
+
const { isGTE } = require('./commonChecks')
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Construct a Z axis-aligned cylinder in three dimensional space.
|
|
@@ -25,7 +27,10 @@ const cylinder = (options) => {
|
|
|
25
27
|
}
|
|
26
28
|
const { center, height, radius, segments } = Object.assign({}, defaults, options)
|
|
27
29
|
|
|
28
|
-
if (!
|
|
30
|
+
if (!isGTE(radius, 0)) throw new Error('radius must be positive')
|
|
31
|
+
|
|
32
|
+
// if size is zero return empty geometry
|
|
33
|
+
if (height === 0 || radius === 0) return geom3.create()
|
|
29
34
|
|
|
30
35
|
const newoptions = {
|
|
31
36
|
center,
|
|
@@ -14,6 +14,20 @@ test('cylinder (defaults)', (t) => {
|
|
|
14
14
|
t.is(pts.length, 96)
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
test('cylinder (zero height)', (t) => {
|
|
18
|
+
const obs = cylinder({ height: 0 })
|
|
19
|
+
const pts = geom3.toPoints(obs)
|
|
20
|
+
t.notThrows(() => geom3.validate(obs))
|
|
21
|
+
t.is(pts.length, 0)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('cylinder (zero radius)', (t) => {
|
|
25
|
+
const obs = cylinder({ radius: 0 })
|
|
26
|
+
const pts = geom3.toPoints(obs)
|
|
27
|
+
t.notThrows(() => geom3.validate(obs))
|
|
28
|
+
t.is(pts.length, 0)
|
|
29
|
+
})
|
|
30
|
+
|
|
17
31
|
test('cylinder (options)', (t) => {
|
|
18
32
|
let obs = cylinder({ height: 10, radius: 4, segments: 5 })
|
|
19
33
|
let pts = geom3.toPoints(obs)
|
|
@@ -34,11 +34,14 @@ const ellipse = (options) => {
|
|
|
34
34
|
|
|
35
35
|
if (!isNumberArray(center, 2)) throw new Error('center must be an array of X and Y values')
|
|
36
36
|
if (!isNumberArray(radius, 2)) throw new Error('radius must be an array of X and Y values')
|
|
37
|
-
if (!radius.every((n) => n
|
|
37
|
+
if (!radius.every((n) => n >= 0)) throw new Error('radius values must be positive')
|
|
38
38
|
if (!isGTE(startAngle, 0)) throw new Error('startAngle must be positive')
|
|
39
39
|
if (!isGTE(endAngle, 0)) throw new Error('endAngle must be positive')
|
|
40
40
|
if (!isGTE(segments, 3)) throw new Error('segments must be three or more')
|
|
41
41
|
|
|
42
|
+
// if any radius is zero return empty geometry
|
|
43
|
+
if (radius[0] === 0 || radius[1] === 0) return geom2.create()
|
|
44
|
+
|
|
42
45
|
startAngle = startAngle % TAU
|
|
43
46
|
endAngle = endAngle % TAU
|
|
44
47
|
|
|
@@ -137,3 +137,10 @@ test('ellipse (options)', (t) => {
|
|
|
137
137
|
t.notThrows(() => geom2.validate(geometry))
|
|
138
138
|
t.deepEqual(obs.length, 72)
|
|
139
139
|
})
|
|
140
|
+
|
|
141
|
+
test('ellipse (zero radius)', (t) => {
|
|
142
|
+
const geometry = ellipse({ radius: [1, 0] })
|
|
143
|
+
const obs = geom2.toPoints(geometry)
|
|
144
|
+
t.notThrows(() => geom2.validate(geometry))
|
|
145
|
+
t.is(obs.length, 0)
|
|
146
|
+
})
|
|
@@ -32,9 +32,12 @@ const ellipsoid = (options) => {
|
|
|
32
32
|
|
|
33
33
|
if (!isNumberArray(center, 3)) throw new Error('center must be an array of X, Y and Z values')
|
|
34
34
|
if (!isNumberArray(radius, 3)) throw new Error('radius must be an array of X, Y and Z values')
|
|
35
|
-
if (!radius.every((n) => n
|
|
35
|
+
if (!radius.every((n) => n >= 0)) throw new Error('radius values must be positive')
|
|
36
36
|
if (!isGTE(segments, 4)) throw new Error('segments must be four or more')
|
|
37
37
|
|
|
38
|
+
// if any radius is zero return empty geometry
|
|
39
|
+
if (radius[0] === 0 || radius[1] === 0 || radius[2] === 0) return geom3.create()
|
|
40
|
+
|
|
38
41
|
const xvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), axes[0]), radius[0])
|
|
39
42
|
const yvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), axes[1]), radius[1])
|
|
40
43
|
const zvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), axes[2]), radius[2])
|
|
@@ -207,3 +207,10 @@ test('ellipsoid (options)', (t) => {
|
|
|
207
207
|
t.is(pts.length, 32)
|
|
208
208
|
t.true(comparePolygonsAsPoints(pts, exp))
|
|
209
209
|
})
|
|
210
|
+
|
|
211
|
+
test('ellipsoid (zero radius)', (t) => {
|
|
212
|
+
const obs = ellipsoid({ radius: [1, 1, 0] })
|
|
213
|
+
const pts = geom3.toPoints(obs)
|
|
214
|
+
t.notThrows(() => geom3.validate(obs))
|
|
215
|
+
t.is(pts.length, 0)
|
|
216
|
+
})
|
|
@@ -5,7 +5,7 @@ const geom3 = require('../geometries/geom3')
|
|
|
5
5
|
|
|
6
6
|
const polyhedron = require('./polyhedron')
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const { isGTE } = require('./commonChecks')
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Construct a geodesic sphere based on icosahedron symmetry.
|
|
@@ -25,9 +25,12 @@ const geodesicSphere = (options) => {
|
|
|
25
25
|
}
|
|
26
26
|
let { radius, frequency } = Object.assign({}, defaults, options)
|
|
27
27
|
|
|
28
|
-
if (!
|
|
28
|
+
if (!isGTE(radius, 0)) throw new Error('radius must be positive')
|
|
29
29
|
if (!isGTE(frequency, 6)) throw new Error('frequency must be six or more')
|
|
30
30
|
|
|
31
|
+
// if radius is zero return empty geometry
|
|
32
|
+
if (radius === 0) return geom3.create()
|
|
33
|
+
|
|
31
34
|
// adjust the frequency to base 6
|
|
32
35
|
frequency = Math.floor(frequency / 6)
|
|
33
36
|
|
|
@@ -51,3 +51,10 @@ test('geodesicSphere (options)', (t) => {
|
|
|
51
51
|
t.notThrows.skip(() => geom3.validate(obs))
|
|
52
52
|
t.is(pts.length, 180)
|
|
53
53
|
})
|
|
54
|
+
|
|
55
|
+
test('geodesicSphere (zero radius)', (t) => {
|
|
56
|
+
const obs = geodesicSphere({ radius: 0 })
|
|
57
|
+
const pts = geom3.toPoints(obs)
|
|
58
|
+
t.notThrows(() => geom3.validate(obs))
|
|
59
|
+
t.is(pts.length, 0)
|
|
60
|
+
})
|
|
@@ -2,10 +2,13 @@ const geom2 = require('../geometries/geom2')
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Construct a polygon in two dimensional space from a list of points, or a list of points and paths.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* NOTE: The ordering of points is important, and must define a counter clockwise rotation of points.
|
|
7
|
+
*
|
|
6
8
|
* @param {Object} options - options for construction
|
|
7
9
|
* @param {Array} options.points - points of the polygon : either flat or nested array of 2D points
|
|
8
10
|
* @param {Array} [options.paths] - paths of the polygon : either flat or nested array of point indexes
|
|
11
|
+
* @param {String} [options.orientation='counterclockwise'] - orientation of points
|
|
9
12
|
* @returns {geom2} new 2D geometry
|
|
10
13
|
* @alias module:modeling/primitives.polygon
|
|
11
14
|
*
|
|
@@ -24,9 +27,10 @@ const geom2 = require('../geometries/geom2')
|
|
|
24
27
|
const polygon = (options) => {
|
|
25
28
|
const defaults = {
|
|
26
29
|
points: [],
|
|
27
|
-
paths: []
|
|
30
|
+
paths: [],
|
|
31
|
+
orientation: 'counterclockwise'
|
|
28
32
|
}
|
|
29
|
-
const { points, paths } = Object.assign({}, defaults, options)
|
|
33
|
+
const { points, paths, orientation } = Object.assign({}, defaults, options)
|
|
30
34
|
|
|
31
35
|
if (!(Array.isArray(points) && Array.isArray(paths))) throw new Error('points and paths must be arrays')
|
|
32
36
|
|
|
@@ -58,13 +62,20 @@ const polygon = (options) => {
|
|
|
58
62
|
const allpoints = []
|
|
59
63
|
listofpolys.forEach((list) => list.forEach((point) => allpoints.push(point)))
|
|
60
64
|
|
|
65
|
+
// convert the list of paths into a list of sides, and accumulate
|
|
61
66
|
let sides = []
|
|
62
67
|
listofpaths.forEach((path) => {
|
|
63
68
|
const setofpoints = path.map((index) => allpoints[index])
|
|
64
69
|
const geometry = geom2.fromPoints(setofpoints)
|
|
65
70
|
sides = sides.concat(geom2.toSides(geometry))
|
|
66
71
|
})
|
|
67
|
-
|
|
72
|
+
|
|
73
|
+
// convert the list of sides into a geometry
|
|
74
|
+
let geometry = geom2.create(sides)
|
|
75
|
+
if (orientation == "clockwise") {
|
|
76
|
+
geometry = geom2.reverse(geometry)
|
|
77
|
+
}
|
|
78
|
+
return geometry
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
module.exports = polygon
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const test = require('ava')
|
|
2
2
|
|
|
3
3
|
const geom2 = require('../geometries/geom2')
|
|
4
|
+
const measureArea = require('../measurements/measureArea')
|
|
4
5
|
|
|
5
6
|
const { polygon } = require('./index')
|
|
6
7
|
|
|
@@ -51,3 +52,12 @@ test('polygon: providing object.points (array) and object.path (array) creates e
|
|
|
51
52
|
t.notThrows(() => geom2.validate(geometry))
|
|
52
53
|
t.true(comparePoints(obs, exp))
|
|
53
54
|
})
|
|
55
|
+
|
|
56
|
+
test('polygon: clockwise points', (t) => {
|
|
57
|
+
const poly = polygon({
|
|
58
|
+
points: [[-10, -0], [-10, -10], [-15, -5]],
|
|
59
|
+
orientation: "clockwise",
|
|
60
|
+
})
|
|
61
|
+
t.is(poly.sides.length, 3)
|
|
62
|
+
t.is(measureArea(poly), 25)
|
|
63
|
+
})
|
|
@@ -24,7 +24,10 @@ const rectangle = (options) => {
|
|
|
24
24
|
|
|
25
25
|
if (!isNumberArray(center, 2)) throw new Error('center must be an array of X and Y values')
|
|
26
26
|
if (!isNumberArray(size, 2)) throw new Error('size must be an array of X and Y values')
|
|
27
|
-
if (!size.every((n) => n
|
|
27
|
+
if (!size.every((n) => n >= 0)) throw new Error('size values must be positive')
|
|
28
|
+
|
|
29
|
+
// if any size is zero return empty geometry
|
|
30
|
+
if (size[0] === 0 || size[1] === 0) return geom2.create()
|
|
28
31
|
|
|
29
32
|
const point = [size[0] / 2, size[1] / 2]
|
|
30
33
|
const pswap = [point[0], -point[1]]
|
|
@@ -50,3 +50,10 @@ test('rectangle (options)', (t) => {
|
|
|
50
50
|
t.deepEqual(obs.length, 4)
|
|
51
51
|
t.true(comparePoints(obs, exp))
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
test('rectangle (zero size)', (t) => {
|
|
55
|
+
const geometry = rectangle({ size: [1, 0] })
|
|
56
|
+
const obs = geom2.toPoints(geometry)
|
|
57
|
+
t.notThrows(() => geom2.validate(geometry))
|
|
58
|
+
t.is(obs.length, 0)
|
|
59
|
+
})
|