@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/jscad-modeling.min.js +338 -335
  3. package/package.json +2 -2
  4. package/src/curves/bezier/arcLengthToT.js +6 -6
  5. package/src/curves/bezier/arcLengthToT.test.js +25 -25
  6. package/src/curves/bezier/length.js +3 -5
  7. package/src/curves/bezier/length.test.js +2 -2
  8. package/src/curves/bezier/lengths.js +10 -10
  9. package/src/curves/bezier/lengths.test.js +3 -3
  10. package/src/geometries/geom2/transform.js +9 -1
  11. package/src/geometries/geom2/transform.test.js +58 -1
  12. package/src/geometries/poly2/arePointsInside.js +0 -7
  13. package/src/geometries/poly3/measureBoundingSphere.js +1 -2
  14. package/src/maths/plane/fromNoisyPoints.d.ts +6 -0
  15. package/src/maths/plane/fromNoisyPoints.js +106 -0
  16. package/src/maths/plane/fromNoisyPoints.test.js +24 -0
  17. package/src/maths/plane/index.d.ts +1 -0
  18. package/src/maths/plane/index.js +1 -0
  19. package/src/operations/booleans/trees/PolygonTreeNode.js +1 -1
  20. package/src/operations/expansions/expand.test.js +1 -1
  21. package/src/operations/extrusions/extrudeHelical.js +6 -7
  22. package/src/operations/extrusions/extrudeHelical.test.js +35 -37
  23. package/src/operations/extrusions/extrudeRotate.js +1 -1
  24. package/src/operations/extrusions/index.d.ts +1 -0
  25. package/src/operations/hulls/hullPoints2.js +3 -2
  26. package/src/operations/modifiers/index.js +1 -1
  27. package/src/operations/modifiers/retessellate.js +66 -27
  28. package/src/operations/transforms/mirror.test.js +9 -3
  29. package/src/primitives/circle.js +2 -2
  30. package/src/primitives/circle.test.js +7 -0
  31. package/src/primitives/cube.js +2 -2
  32. package/src/primitives/cube.test.js +7 -0
  33. package/src/primitives/cuboid.js +4 -1
  34. package/src/primitives/cuboid.test.js +7 -0
  35. package/src/primitives/cylinder.js +7 -2
  36. package/src/primitives/cylinder.test.js +14 -0
  37. package/src/primitives/ellipse.js +4 -1
  38. package/src/primitives/ellipse.test.js +7 -0
  39. package/src/primitives/ellipsoid.js +4 -1
  40. package/src/primitives/ellipsoid.test.js +7 -0
  41. package/src/primitives/geodesicSphere.js +5 -2
  42. package/src/primitives/geodesicSphere.test.js +7 -0
  43. package/src/primitives/polygon.d.ts +1 -0
  44. package/src/primitives/polygon.js +15 -4
  45. package/src/primitives/polygon.test.js +10 -0
  46. package/src/primitives/rectangle.js +4 -1
  47. package/src/primitives/rectangle.test.js +7 -0
  48. package/src/primitives/roundedCuboid.js +10 -3
  49. package/src/primitives/roundedCuboid.test.js +14 -0
  50. package/src/primitives/roundedCylinder.js +12 -5
  51. package/src/primitives/roundedCylinder.test.js +21 -0
  52. package/src/primitives/roundedRectangle.js +10 -3
  53. package/src/primitives/roundedRectangle.test.js +14 -0
  54. package/src/primitives/sphere.js +2 -2
  55. package/src/primitives/sphere.test.js +7 -0
  56. package/src/primitives/square.js +2 -2
  57. 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
- const geometry2 = geom2.fromPoints([[10, 8], [10, -8], [26, -8], [26, 8]])
9
+ const geometry2 = geom2.fromPoints([[10, 8], [10, -8], [26, -8], [26, 8]])
11
10
 
12
- const geometry3 = extrudeHelical({}, geometry2)
13
- const pts = geom3.toPoints(geometry3)
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
- const geometry2 = circle({ size: 3, center: [10, 0] })
16
+ const geometry2 = circle({ size: 3, center: [10, 0] })
19
17
 
20
- const geometry3 = extrudeHelical({}, geometry2)
21
- t.notThrows(() => geom3.validate(geometry3))
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
- const maxRevolutions = 10
26
- const geometry2 = circle({ size: 3, center: [10, 0] })
27
- for (const index of [...Array(maxRevolutions).keys()]) {
28
- // also test negative angles
29
- const geometry3 = extrudeHelical({angle: TAU * (index - maxRevolutions / 2)}, geometry2)
30
- t.notThrows(() => geom3.validate(geometry3))
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
- const startPitch = -10
36
- const geometry2 = circle({ size: 3, center: [10, 0] })
37
- for (const index of [...Array(20).keys()]) {
38
- // also test negative pitches
39
- const geometry3 = extrudeHelical({pitch: startPitch + index}, geometry2)
40
- t.notThrows(() => geom3.validate(geometry3))
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
- const startOffset = -5
46
- const geometry2 = circle({ size: 3, center: [10, 0] })
47
- for (const index of [...Array(10).keys()]) {
48
- // also test negative pitches
49
- const geometry3 = extrudeHelical({endRadiusOffset: startOffset + index}, geometry2)
50
- t.notThrows(() => geom3.validate(geometry3))
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
- const startSegments = 3
56
- const geometry2 = circle({ size: 3, center: [10, 0] })
57
- for (const index of [...Array(30).keys()]) {
58
- // also test negative pitches
59
- const geometry3 = extrudeHelical({segments: startSegments + index}, geometry2)
60
- t.notThrows(() => geom3.validate(geometry3))
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.reverse(geom2.create(shapeSides))
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 < pt2.angle ? -1 : pt1.angle > pt2.angle ? 1 :
29
- pt1.distSq < pt2.distSq ? -1 : pt1.distSq > pt2.distSq ? 1 : 0)
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) => {
@@ -8,5 +8,5 @@
8
8
  module.exports = {
9
9
  generalize: require('./generalize'),
10
10
  snap: require('./snap'),
11
- retessellate: require('./retessellate'),
11
+ retessellate: require('./retessellate')
12
12
  }
@@ -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 polygonsPerPlane = [] // elements: [plane, [poly3...]]
31
- polygons.forEach((polygon) => {
32
- const mapping = polygonsPerPlane.find((element) => coplanar(element[0], poly3.plane(polygon)))
33
- if (mapping) {
34
- const polygons = mapping[1]
35
- polygons.push(polygon)
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
- polygonsPerPlane.push([poly3.plane(polygon), [polygon]])
28
+ destPolygons.push(group)
38
29
  }
39
30
  })
40
31
 
41
- let destpolygons = []
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 = [[5, -5], [0, 5], [-10, -5]]
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 = [[-5, 5], [0, -5], [10, 5]]
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 = [[-5, 5], [0, -5], [10, 5]]
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
  })
@@ -2,7 +2,7 @@ const { TAU } = require('../maths/constants')
2
2
 
3
3
  const ellipse = require('./ellipse')
4
4
 
5
- const { isGT } = require('./commonChecks')
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 (!isGT(radius, 0)) throw new Error('radius must be greater than zero')
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
+ })
@@ -1,6 +1,6 @@
1
1
  const cuboid = require('./cuboid')
2
2
 
3
- const { isGT } = require('./commonChecks')
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 (!isGT(size, 0)) throw new Error('size must be greater than zero')
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
+ })
@@ -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 > 0)) throw new Error('size values must be greater than zero')
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 { isGT } = require('./commonChecks')
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 (!isGT(radius, 0)) throw new Error('radius must be greater than zero')
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 > 0)) throw new Error('radius values must be greater than zero')
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 > 0)) throw new Error('radius values must be greater than zero')
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 { isGT, isGTE } = require('./commonChecks')
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 (!isGT(radius, 0)) throw new Error('radius must be greater than zero')
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
+ })
@@ -6,6 +6,7 @@ export default polygon
6
6
  export interface PolygonOptions {
7
7
  points: Array<Vec2> | Array<Array<Vec2>>
8
8
  paths?: Array<number> | Array<Array<number>>
9
+ orientation?: 'counterclockwise' | 'clockwise'
9
10
  }
10
11
 
11
12
  declare function polygon(options: PolygonOptions): Geom2
@@ -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
- * NOTE: The ordering of points is VERY IMPORTANT.
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
- return geom2.create(sides)
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 > 0)) throw new Error('size values must be greater than zero')
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
+ })