@jscad/modeling 2.11.1 → 2.12.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 +21 -0
- package/dist/jscad-modeling.min.js +25 -25
- 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/poly2/arePointsInside.js +0 -7
- package/src/geometries/poly3/measureBoundingSphere.js +1 -2
- 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/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/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/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
|
@@ -25,7 +25,7 @@ class PolygonTreeNode {
|
|
|
25
25
|
this.parent = parent
|
|
26
26
|
this.children = []
|
|
27
27
|
this.polygon = polygon
|
|
28
|
-
this.removed = false
|
|
28
|
+
this.removed = false // state of branch or leaf
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// fill the tree with polygons. Should be called on the root node only; child nodes must
|
|
@@ -53,7 +53,7 @@ test('expand: round-expanding a bent line produces expected geometry', (t) => {
|
|
|
53
53
|
const expandedPoints = geom2.toPoints(expandedPathGeom2)
|
|
54
54
|
|
|
55
55
|
t.notThrows(() => geom2.validate(expandedPathGeom2))
|
|
56
|
-
const expectedArea = 56 + TAU
|
|
56
|
+
const expectedArea = 56 + TAU * delta * 1.25 // shape will have 1 and 1/4 circles
|
|
57
57
|
nearlyEqual(t, area(expandedPoints), expectedArea, 0.01, 'Measured area should be pretty close')
|
|
58
58
|
const boundingBox = measureBoundingBox(expandedPathGeom2)
|
|
59
59
|
t.true(comparePoints(boundingBox, [[-7, -2, 0], [2, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
@@ -40,7 +40,7 @@ const extrudeHelical = (options, geometry) => {
|
|
|
40
40
|
|
|
41
41
|
let pitch
|
|
42
42
|
// ignore height if pitch is set
|
|
43
|
-
if(!options.pitch && options.height) {
|
|
43
|
+
if (!options.pitch && options.height) {
|
|
44
44
|
pitch = options.height / (angle / TAU)
|
|
45
45
|
} else {
|
|
46
46
|
pitch = options.pitch ? options.pitch : defaults.pitch
|
|
@@ -49,18 +49,17 @@ const extrudeHelical = (options, geometry) => {
|
|
|
49
49
|
// needs at least 3 segments for each revolution
|
|
50
50
|
const minNumberOfSegments = 3
|
|
51
51
|
|
|
52
|
-
if (segmentsPerRotation < minNumberOfSegments)
|
|
53
|
-
throw new Error(`The number of segments per rotation needs to be at least 3.`)
|
|
52
|
+
if (segmentsPerRotation < minNumberOfSegments) { throw new Error('The number of segments per rotation needs to be at least 3.') }
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
const shapeSides = geom2.toSides(geometry)
|
|
56
55
|
if (shapeSides.length === 0) throw new Error('the given geometry cannot be empty')
|
|
57
56
|
|
|
58
57
|
// const pointsWithNegativeX = shapeSides.filter((s) => (s[0][0] < 0))
|
|
59
58
|
const pointsWithPositiveX = shapeSides.filter((s) => (s[0][0] >= 0))
|
|
60
|
-
|
|
59
|
+
|
|
61
60
|
let baseSlice = slice.fromSides(shapeSides)
|
|
62
|
-
|
|
63
|
-
if(pointsWithPositiveX.length === 0) {
|
|
61
|
+
|
|
62
|
+
if (pointsWithPositiveX.length === 0) {
|
|
64
63
|
// only points in negative x plane, reverse
|
|
65
64
|
baseSlice = slice.reverse(baseSlice)
|
|
66
65
|
}
|
|
@@ -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
|
+
})
|
|
@@ -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
|
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
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -8,7 +8,8 @@ const poly3 = require('../geometries/poly3')
|
|
|
8
8
|
|
|
9
9
|
const { sin, cos } = require('../maths/utils/trigonometry')
|
|
10
10
|
|
|
11
|
-
const {
|
|
11
|
+
const { isGTE, isNumberArray } = require('./commonChecks')
|
|
12
|
+
const cuboid = require('./cuboid')
|
|
12
13
|
|
|
13
14
|
const createCorners = (center, size, radius, segments, slice, positive) => {
|
|
14
15
|
const pitch = (TAU / 4) * slice / segments
|
|
@@ -135,10 +136,16 @@ const roundedCuboid = (options) => {
|
|
|
135
136
|
|
|
136
137
|
if (!isNumberArray(center, 3)) throw new Error('center must be an array of X, Y and Z values')
|
|
137
138
|
if (!isNumberArray(size, 3)) throw new Error('size must be an array of X, Y and Z values')
|
|
138
|
-
if (!size.every((n) => n
|
|
139
|
-
if (!
|
|
139
|
+
if (!size.every((n) => n >= 0)) throw new Error('size values must be positive')
|
|
140
|
+
if (!isGTE(roundRadius, 0)) throw new Error('roundRadius must be positive')
|
|
140
141
|
if (!isGTE(segments, 4)) throw new Error('segments must be four or more')
|
|
141
142
|
|
|
143
|
+
// if any size is zero return empty geometry
|
|
144
|
+
if (size[0] === 0 || size[1] === 0 || size[2] === 0) return geom3.create()
|
|
145
|
+
|
|
146
|
+
// if roundRadius is zero, return cuboid
|
|
147
|
+
if (roundRadius === 0) return cuboid({ center, size })
|
|
148
|
+
|
|
142
149
|
size = size.map((v) => v / 2) // convert to radius
|
|
143
150
|
|
|
144
151
|
if (roundRadius > (size[0] - EPS) ||
|
|
@@ -14,6 +14,20 @@ test('roundedCuboid (defaults)', (t) => {
|
|
|
14
14
|
t.deepEqual(pts.length, 614)
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
test('roundedCuboid (zero size)', (t) => {
|
|
18
|
+
const obs = roundedCuboid({ size: [1, 1, 0] })
|
|
19
|
+
const pts = geom3.toPoints(obs)
|
|
20
|
+
t.notThrows(() => geom3.validate(obs))
|
|
21
|
+
t.is(pts.length, 0)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('roundedCuboid (zero radius)', (t) => {
|
|
25
|
+
const obs = roundedCuboid({ roundRadius: 0 })
|
|
26
|
+
const pts = geom3.toPoints(obs)
|
|
27
|
+
t.notThrows(() => geom3.validate(obs))
|
|
28
|
+
t.deepEqual(pts.length, 6)
|
|
29
|
+
})
|
|
30
|
+
|
|
17
31
|
test('roundedCuboid (options)', (t) => {
|
|
18
32
|
// test segments
|
|
19
33
|
let obs = roundedCuboid({ segments: 8 })
|
|
@@ -7,7 +7,8 @@ const poly3 = require('../geometries/poly3')
|
|
|
7
7
|
|
|
8
8
|
const { sin, cos } = require('../maths/utils/trigonometry')
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const { isGTE, isNumberArray } = require('./commonChecks')
|
|
11
|
+
const cylinder = require('./cylinder')
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Construct a Z axis-aligned solid cylinder in three dimensional space with rounded ends.
|
|
@@ -34,12 +35,18 @@ const roundedCylinder = (options) => {
|
|
|
34
35
|
const { center, height, radius, roundRadius, segments } = Object.assign({}, defaults, options)
|
|
35
36
|
|
|
36
37
|
if (!isNumberArray(center, 3)) throw new Error('center must be an array of X, Y and Z values')
|
|
37
|
-
if (!
|
|
38
|
-
if (!
|
|
39
|
-
if (!
|
|
40
|
-
if (roundRadius >
|
|
38
|
+
if (!isGTE(height, 0)) throw new Error('height must be positive')
|
|
39
|
+
if (!isGTE(radius, 0)) throw new Error('radius must be positive')
|
|
40
|
+
if (!isGTE(roundRadius, 0)) throw new Error('roundRadius must be positive')
|
|
41
|
+
if (roundRadius > radius) throw new Error('roundRadius must be smaller then the radius')
|
|
41
42
|
if (!isGTE(segments, 4)) throw new Error('segments must be four or more')
|
|
42
43
|
|
|
44
|
+
// if size is zero return empty geometry
|
|
45
|
+
if (height === 0 || radius === 0) return geom3.create()
|
|
46
|
+
|
|
47
|
+
// if roundRadius is zero, return cylinder
|
|
48
|
+
if (roundRadius === 0) return cylinder({ center, height, radius })
|
|
49
|
+
|
|
43
50
|
const start = [0, 0, -(height / 2)]
|
|
44
51
|
const end = [0, 0, height / 2]
|
|
45
52
|
const direction = vec3.subtract(vec3.create(), end, start)
|