@jscad/modeling 3.0.4-alpha.0 → 3.0.6-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/jscad-modeling.es.js +2 -2
  3. package/dist/jscad-modeling.min.js +2 -2
  4. package/package.json +4 -4
  5. package/rollup.config.js +1 -1
  6. package/src/colors/colorize.js +17 -1
  7. package/src/colors/hsvToRgb.js +1 -0
  8. package/src/colors/rgbToHsl.js +3 -1
  9. package/src/colors/rgbToHsv.js +1 -0
  10. package/src/curves/bezier/create.js +1 -1
  11. package/src/geometries/geom2/fromPoints.d.ts +4 -0
  12. package/src/geometries/geom2/fromPoints.js +28 -0
  13. package/src/geometries/geom2/fromPoints.test.js +22 -0
  14. package/src/geometries/geom2/index.d.ts +1 -0
  15. package/src/geometries/geom2/index.js +1 -0
  16. package/src/geometries/geom2/transform.js +1 -0
  17. package/src/geometries/geom3/transform.js +1 -0
  18. package/src/geometries/geom3/validate.js +11 -1
  19. package/src/geometries/path2/appendArc.js +19 -25
  20. package/src/geometries/path2/appendBezier.js +8 -11
  21. package/src/geometries/path2/appendPoints.js +21 -3
  22. package/src/geometries/path2/appendPoints.test.js +16 -4
  23. package/src/geometries/path2/close.js +7 -10
  24. package/src/geometries/path2/concat.js +2 -2
  25. package/src/geometries/path2/fromPoints.js +7 -7
  26. package/src/geometries/path2/reverse.js +3 -3
  27. package/src/geometries/path2/transform.js +1 -0
  28. package/src/geometries/path2/validate.js +2 -2
  29. package/src/geometries/path3/clone.d.ts +3 -0
  30. package/src/geometries/path3/clone.js +11 -0
  31. package/src/geometries/path3/close.js +7 -8
  32. package/src/geometries/path3/index.d.ts +1 -0
  33. package/src/geometries/path3/index.js +1 -0
  34. package/src/geometries/path3/transform.js +1 -0
  35. package/src/geometries/poly2/type.d.ts +1 -5
  36. package/src/geometries/slice/earcut/index.js +4 -2
  37. package/src/geometries/slice/earcut/linkedPolygon.js +4 -2
  38. package/src/geometries/slice/fromOutlines.d.ts +5 -0
  39. package/src/geometries/slice/fromOutlines.js +16 -0
  40. package/src/geometries/slice/fromOutlines.test.js +17 -0
  41. package/src/geometries/slice/index.d.ts +1 -1
  42. package/src/geometries/slice/index.js +1 -1
  43. package/src/geometries/slice/transform.js +2 -1
  44. package/src/maths/line2/intersectPointOfLines.js +1 -4
  45. package/src/maths/plane/fromNormalAndPoint.js +4 -6
  46. package/src/maths/plane/fromPoints.js +8 -7
  47. package/src/maths/plane/fromPointsRandom.js +13 -13
  48. package/src/measurements/measureAggregateEpsilon.js +3 -1
  49. package/src/measurements/measureAggregateEpsilon.test.js +1 -1
  50. package/src/measurements/measureArea.js +6 -4
  51. package/src/measurements/measureArea.test.js +4 -1
  52. package/src/measurements/measureBoundingBox.js +16 -2
  53. package/src/measurements/measureBoundingBox.test.js +4 -1
  54. package/src/measurements/measureBoundingSphere.js +38 -29
  55. package/src/measurements/measureBoundingSphere.test.js +4 -1
  56. package/src/measurements/measureCenterOfMass.js +3 -2
  57. package/src/measurements/measureEpsilon.js +4 -2
  58. package/src/operations/booleans/martinez/computeFields.js +6 -0
  59. package/src/operations/booleans/martinez/divideSegment.js +2 -1
  60. package/src/operations/booleans/martinez/fillQueue.js +3 -1
  61. package/src/operations/booleans/martinez/subdivideSegments.js +5 -4
  62. package/src/operations/booleans/subtractGeom3Sub.js +1 -1
  63. package/src/operations/booleans/trees/PolygonTreeNode.js +3 -0
  64. package/src/operations/booleans/trees/splitPolygonByPlane.d.ts +1 -3
  65. package/src/operations/booleans/union.js +1 -1
  66. package/src/operations/extrusions/extrudeFromSlices.js +1 -1
  67. package/src/operations/extrusions/extrudeFromSlices.test.js +1 -1
  68. package/src/operations/extrusions/extrudeHelical.js +2 -1
  69. package/src/operations/extrusions/extrudeLinear.js +1 -1
  70. package/src/operations/extrusions/extrudeLinearGeom2.js +12 -8
  71. package/src/operations/extrusions/extrudeRotate.js +1 -1
  72. package/src/operations/hulls/hull.js +3 -2
  73. package/src/operations/hulls/quickhull/Face.js +8 -3
  74. package/src/operations/hulls/quickhull/QuickHull.js +10 -5
  75. package/src/operations/hulls/quickhull/VertexList.js +2 -1
  76. package/src/operations/hulls/toUniquePoints.js +3 -0
  77. package/src/operations/modifiers/generalize.js +9 -2
  78. package/src/operations/modifiers/reTesselateCoplanarPolygons.js +2 -2
  79. package/src/operations/modifiers/reTesselateCoplanarPolygons.test.js +29 -3
  80. package/src/operations/modifiers/retessellate.js +1 -0
  81. package/src/operations/modifiers/snap.js +22 -3
  82. package/src/operations/modifiers/snap.test.js +12 -2
  83. package/src/operations/offsets/offsetGeom3.test.js +2 -2
  84. package/src/operations/transforms/align.js +2 -1
  85. package/src/operations/transforms/align.test.js +1 -1
  86. package/src/operations/transforms/mirror.js +6 -2
  87. package/src/operations/transforms/rotate.js +6 -2
  88. package/src/operations/transforms/scale.js +6 -2
  89. package/src/operations/transforms/transform.js +6 -2
  90. package/src/operations/transforms/transform.test.js +16 -5
  91. package/src/operations/transforms/translate.js +12 -4
  92. package/src/primitives/arc.js +14 -14
  93. package/src/primitives/circle.js +10 -9
  94. package/src/primitives/cube.js +5 -6
  95. package/src/primitives/cuboid.js +6 -6
  96. package/src/primitives/cylinder.js +8 -8
  97. package/src/primitives/cylinderElliptic.js +11 -11
  98. package/src/primitives/ellipse.js +12 -12
  99. package/src/primitives/ellipsoid.js +8 -8
  100. package/src/primitives/geodesicSphere.js +6 -6
  101. package/src/primitives/line.js +2 -0
  102. package/src/primitives/polygon.js +6 -7
  103. package/src/primitives/polyhedron.js +7 -8
  104. package/src/primitives/rectangle.js +6 -6
  105. package/src/primitives/roundedCuboid.js +8 -8
  106. package/src/primitives/roundedCylinder.js +9 -9
  107. package/src/primitives/roundedRectangle.js +8 -8
  108. package/src/primitives/sphere.js +7 -8
  109. package/src/primitives/square.js +6 -6
  110. package/src/primitives/star.js +12 -14
  111. package/src/primitives/torus.js +11 -11
  112. package/src/primitives/triangle.js +7 -6
  113. package/src/utils/areAllShapesTheSameType.js +4 -0
  114. package/test/helpers/nearlyEqual.js +1 -0
  115. package/src/geometries/slice/fromGeom2.d.ts +0 -5
  116. package/src/geometries/slice/fromGeom2.js +0 -17
@@ -28,6 +28,4 @@ interface SplitRes
28
28
  // In case the polygon is spanning, returns:
29
29
  // .front: a Polygon3 of the front part
30
30
  // .back: a Polygon3 of the back part
31
- declare function splitPolygonByPlane(plane: Plane, polygon: Poly3): SplitRes;
32
-
33
- export default splitPolygonByPlane;
31
+ export declare function splitPolygonByPlane(plane: Plane, polygon: Poly3): SplitRes;
@@ -37,7 +37,7 @@ export const union = (...geometries) => {
37
37
  }
38
38
 
39
39
  const geometry = geometries[0]
40
- // if (path.isA(geometry)) return unionPath(matrix, geometries)
40
+ // TODO if (path2.isA(geometry)) return unionPath(geometries)
41
41
  if (geom2.isA(geometry)) return unionGeom2(geometries)
42
42
  if (geom3.isA(geometry)) return unionGeom3(geometries)
43
43
  throw new Error('union unsupported geometry type')
@@ -9,7 +9,7 @@ import { extrudeWalls } from './extrudeWalls.js'
9
9
 
10
10
  const defaultCallback = (progress, index, base) => {
11
11
  let baseSlice = null
12
- if (geom2.isA(base)) baseSlice = slice.fromGeom2(base)
12
+ if (geom2.isA(base)) baseSlice = slice.fromOutlines(geom2.toOutlines(base))
13
13
  if (poly3.isA(base)) baseSlice = slice.fromVertices(poly3.toVertices(base))
14
14
 
15
15
  return progress === 0 || progress === 1 ? slice.transform(mat4.fromTranslation(mat4.create(), [0, 0, progress]), baseSlice) : null
@@ -111,7 +111,7 @@ test('extrudeFromSlices (changing shape, changing dimensions)', (t) => {
111
111
  numberOfSlices: 5,
112
112
  callback: (progress, count, base) => {
113
113
  const newShape = circle({ radius: 5 + count, segments: 4 + count })
114
- let newSlice = slice.fromGeom2(newShape)
114
+ let newSlice = slice.fromOutlines(geom2.toOutlines(newShape))
115
115
  newSlice = slice.transform(mat4.fromTranslation(mat4.create(), [0, 0, count * 10]), newSlice)
116
116
  return newSlice
117
117
  }
@@ -1,4 +1,5 @@
1
1
  import { TAU } from '../../maths/constants.js'
2
+ import * as geom2 from '../../geometries/geom2/index.js'
2
3
  import * as slice from '../../geometries/slice/index.js'
3
4
  import * as mat4 from '../../maths/mat4/index.js'
4
5
  import { measureBoundingBox } from '../../measurements/measureBoundingBox.js'
@@ -45,7 +46,7 @@ export const extrudeHelical = (options, geometry) => {
45
46
 
46
47
  if (segmentsPerRotation < minNumberOfSegments) { throw new Error('The number of segments per rotation needs to be at least 3.') }
47
48
 
48
- let baseSlice = slice.fromGeom2(geometry)
49
+ let baseSlice = slice.fromOutlines(geom2.toOutlines(geometry))
49
50
 
50
51
  const bounds = measureBoundingBox(geometry)
51
52
  if (bounds[1][0] <= 0) {
@@ -28,7 +28,7 @@ export const extrudeLinear = (options, ...objects) => {
28
28
  }
29
29
  const { height, twistAngle, twistSteps, repair } = Object.assign({ }, defaults, options)
30
30
 
31
- options = { offset: [0, 0, height], twistAngle, twistSteps, repair }
31
+ options = { height, twistAngle, twistSteps, repair, offset: [0, 0, height] }
32
32
 
33
33
  const results = objects.map((object) => {
34
34
  if (path2.isA(object)) return extrudeLinearPath2(options, object)
@@ -1,8 +1,11 @@
1
1
  import * as mat4 from '../../maths/mat4/index.js'
2
2
  import * as vec3 from '../../maths/vec3/index.js'
3
3
 
4
+ import * as geom2 from '../../geometries/geom2/index.js'
4
5
  import * as slice from '../../geometries/slice/index.js'
5
6
 
7
+ import { isNumberArray } from '../../primitives/commonChecks.js'
8
+
6
9
  import { extrudeFromSlices } from './extrudeFromSlices.js'
7
10
 
8
11
  /*
@@ -25,6 +28,7 @@ export const extrudeLinearGeom2 = (options, geometry) => {
25
28
  }
26
29
  let { offset, twistAngle, twistSteps, repair } = Object.assign({ }, defaults, options)
27
30
 
31
+ if (!isNumberArray(offset, 3)) throw new Error('offset must be an array of three numbers')
28
32
  if (twistSteps < 1) throw new Error('twistSteps must be 1 or more')
29
33
 
30
34
  if (twistAngle === 0) {
@@ -32,18 +36,18 @@ export const extrudeLinearGeom2 = (options, geometry) => {
32
36
  }
33
37
 
34
38
  // convert to vector in order to perform transforms
35
- const offsetV = vec3.clone(offset)
36
-
37
- let baseSlice = slice.fromGeom2(geometry)
38
- if (offsetV[2] < 0) baseSlice = slice.reverse(baseSlice)
39
+ let baseSlice = slice.fromOutlines(geom2.toOutlines(geometry))
40
+ if (offset[2] < 0) baseSlice = slice.reverse(baseSlice)
39
41
 
40
- const matrix = mat4.create()
42
+ const matrix1 = mat4.create()
43
+ const matrix2 = mat4.create()
44
+ const offset1 = vec3.create()
41
45
  const createTwist = (progress, index, base) => {
42
46
  const Zrotation = index / twistSteps * twistAngle
43
- const Zoffset = vec3.scale(vec3.create(), offsetV, index / twistSteps)
44
- mat4.multiply(matrix, mat4.fromZRotation(matrix, Zrotation), mat4.fromTranslation(mat4.create(), Zoffset))
47
+ const Zoffset = vec3.scale(offset1, offset, index / twistSteps)
48
+ mat4.multiply(matrix1, mat4.fromZRotation(matrix1, Zrotation), mat4.fromTranslation(matrix2, Zoffset))
45
49
 
46
- return slice.transform(matrix, base)
50
+ return slice.transform(matrix1, base)
47
51
  }
48
52
 
49
53
  options = {
@@ -106,7 +106,7 @@ export const extrudeRotate = (options, geometry) => {
106
106
 
107
107
  const rotationPerSlice = totalRotation / segments
108
108
  const isCapped = Math.abs(totalRotation) < TAU
109
- let baseSlice = slice.fromGeom2(sliceGeometry)
109
+ let baseSlice = slice.fromOutlines(geom2.toOutlines(sliceGeometry))
110
110
  baseSlice = slice.reverse(baseSlice)
111
111
 
112
112
  const matrix = mat4.create()
@@ -42,9 +42,10 @@ export const hull = (...geometries) => {
42
42
  }
43
43
 
44
44
  const geometry = geometries[0]
45
- if (path2.isA(geometry)) return hullPath2(geometries)
46
- if (geom2.isA(geometry)) return hullGeom2(geometries)
47
45
  if (geom3.isA(geometry)) return hullGeom3(geometries)
46
+ if (geom2.isA(geometry)) return hullGeom2(geometries)
47
+ if (path2.isA(geometry)) return hullPath2(geometries)
48
+ // FIXME return geom3? if (path3.isA(geometry)) return hullPath3(geometries)
48
49
 
49
50
  // FIXME should this throw an error for unknown geometries?
50
51
  return geometry
@@ -308,9 +308,14 @@ export class Face {
308
308
  const e2 = new HalfEdge(v2, face)
309
309
 
310
310
  // join edges
311
- e0.next = e2.prev = e1
312
- e1.next = e0.prev = e2
313
- e2.next = e1.prev = e0
311
+ e0.next = e1
312
+ e2.prev = e1
313
+
314
+ e1.next = e2
315
+ e0.prev = e2
316
+
317
+ e2.next = e0
318
+ e1.prev = e0
314
319
 
315
320
  // main half edge reference
316
321
  face.edge = e0
@@ -118,6 +118,7 @@ export class QuickHull {
118
118
  end.next = null
119
119
  return face.outside
120
120
  }
121
+ return undefined
121
122
  }
122
123
 
123
124
  /**
@@ -215,11 +216,13 @@ export class QuickHull {
215
216
 
216
217
  // initially assume that the first vertex is the min/max
217
218
  for (i = 0; i < 3; i += 1) {
218
- minVertices[i] = maxVertices[i] = this.vertices[0]
219
+ minVertices[i] = this.vertices[0]
220
+ maxVertices[i] = this.vertices[0]
219
221
  }
220
222
  // copy the coordinates of the first vertex to min/max
221
223
  for (i = 0; i < 3; i += 1) {
222
- min[i] = max[i] = this.vertices[0].point[i]
224
+ min[i] = this.vertices[0].point[i]
225
+ max[i] = this.vertices[0].point[i]
223
226
  }
224
227
 
225
228
  // compute the min/max vertex on all 6 directions
@@ -456,6 +459,7 @@ export class QuickHull {
456
459
  }
457
460
  return eyeVertex
458
461
  }
462
+ return undefined
459
463
  }
460
464
 
461
465
  /**
@@ -477,7 +481,8 @@ export class QuickHull {
477
481
 
478
482
  let edge
479
483
  if (!crossEdge) {
480
- edge = crossEdge = face.getEdge(0)
484
+ crossEdge = face.getEdge(0)
485
+ edge = crossEdge
481
486
  } else {
482
487
  // start from the next edge since `crossEdge` was already analyzed
483
488
  // (actually `crossEdge.opposite` was the face who called this method
@@ -722,7 +727,7 @@ export class QuickHull {
722
727
  for (let i = 0; i < this.newFaces.length; i += 1) {
723
728
  const face = this.newFaces[i]
724
729
  if (face.mark === VISIBLE) {
725
- while (this.doAdjacentMerge(face, MERGE_NON_CONVEX_WRT_LARGER_FACE)) {} // eslint-disable-line no-empty
730
+ while (this.doAdjacentMerge(face, MERGE_NON_CONVEX_WRT_LARGER_FACE)) {}
726
731
  }
727
732
  }
728
733
 
@@ -733,7 +738,7 @@ export class QuickHull {
733
738
  const face = this.newFaces[i]
734
739
  if (face.mark === NON_CONVEX) {
735
740
  face.mark = VISIBLE
736
- while (this.doAdjacentMerge(face, MERGE_NON_CONVEX)) {} // eslint-disable-line no-empty
741
+ while (this.doAdjacentMerge(face, MERGE_NON_CONVEX)) {}
737
742
  }
738
743
  }
739
744
 
@@ -11,7 +11,8 @@ export class VertexList {
11
11
  }
12
12
 
13
13
  clear () {
14
- this.head = this.tail = null
14
+ this.head = null
15
+ this.tail = null
15
16
  }
16
17
 
17
18
  /**
@@ -1,6 +1,7 @@
1
1
  import * as geom2 from '../../geometries/geom2/index.js'
2
2
  import * as geom3 from '../../geometries/geom3/index.js'
3
3
  import * as path2 from '../../geometries/path2/index.js'
4
+ import * as path3 from '../../geometries/path3/index.js'
4
5
 
5
6
  /*
6
7
  * Return the unique vertices of a geometry
@@ -25,6 +26,8 @@ export const toUniquePoints = (geometries) => {
25
26
  geom3.toVertices(geometry).forEach((vertices) => vertices.forEach(addPoint))
26
27
  } else if (path2.isA(geometry)) {
27
28
  path2.toPoints(geometry).forEach(addPoint)
29
+ } else if (path3.isA(geometry)) {
30
+ path3.toVertices(geometry).forEach(addPoint)
28
31
  }
29
32
  })
30
33
 
@@ -3,6 +3,8 @@ import { measureEpsilon } from '../../measurements/measureEpsilon.js'
3
3
  import * as geom2 from '../../geometries/geom2/index.js'
4
4
  import * as geom3 from '../../geometries/geom3/index.js'
5
5
  import * as path2 from '../../geometries/path2/index.js'
6
+ import * as path3 from '../../geometries/path3/index.js'
7
+ import * as slice from '../../geometries/slice/index.js'
6
8
 
7
9
  import { snapPolygons } from './snapPolygons.js'
8
10
  import { mergePolygons } from './mergePolygons.js'
@@ -13,6 +15,10 @@ import { triangulatePolygons } from './triangulatePolygons.js'
13
15
  */
14
16
  const generalizePath2 = (options, geometry) => geometry
15
17
 
18
+ /*
19
+ */
20
+ const generalizePath3 = (options, geometry) => geometry
21
+
16
22
  /*
17
23
  */
18
24
  const generalizeGeom2 = (options, geometry) => geometry
@@ -66,9 +72,10 @@ const generalizeGeom3 = (options, geometry) => {
66
72
  */
67
73
  export const generalize = (options, ...geometries) => {
68
74
  const results = geometries.map((geometry) => {
69
- if (path2.isA(geometry)) return generalizePath2(options, geometry)
70
- if (geom2.isA(geometry)) return generalizeGeom2(options, geometry)
71
75
  if (geom3.isA(geometry)) return generalizeGeom3(options, geometry)
76
+ if (geom2.isA(geometry)) return generalizeGeom2(options, geometry)
77
+ if (path2.isA(geometry)) return generalizePath2(options, geometry)
78
+ if (path3.isA(geometry)) return generalizePath3(options, geometry)
72
79
  if (Array.isArray(geometry)) return generalize(options, ...geometry)
73
80
  return geometry
74
81
  })
@@ -317,8 +317,8 @@ export const reTesselateCoplanarPolygons = (sourcePolygons) => {
317
317
  const vertices3d = points2d.map((point2d) => orthonormalFormula.to3D(point2d))
318
318
  const polygon = poly3.fromVerticesAndPlane(vertices3d, plane) // TODO support shared
319
319
 
320
- // if we let empty polygon out, next retesselate will crash
321
- if (polygon.vertices.length) destPolygons.push(polygon)
320
+ // if we let invalid polygons out, next retesselate will crash
321
+ if (polygon.vertices.length > 2) destPolygons.push(polygon)
322
322
  }
323
323
  }
324
324
  } // if(yIndex > 0)
@@ -66,7 +66,9 @@ test('retessellateCoplanarPolygons: should merge coplanar polygons', (t) => {
66
66
  polyL = translatePoly3([-15, -15, -15], polyE)
67
67
 
68
68
  obs = reTesselateCoplanarPolygons([polyH, polyI, polyJ, polyK, polyL])
69
- t.is(obs.length, 1)
69
+ obs.forEach((polygon) => {
70
+ poly3.validate(polygon)
71
+ })
70
72
  })
71
73
 
72
74
  // Test for mark-and-filter optimization: multiple polygons that reach their
@@ -85,9 +87,8 @@ test('retessellateCoplanarPolygons: should correctly handle multiple polygon rem
85
87
  // Each triangle should be preserved (they don't overlap)
86
88
  t.is(obs.length, 3)
87
89
 
88
- // Verify each polygon has 3 vertices (triangles)
89
90
  obs.forEach((polygon) => {
90
- t.is(polygon.vertices.length, 3)
91
+ poly3.validate(polygon)
91
92
  })
92
93
  })
93
94
 
@@ -103,3 +104,28 @@ test('retessellateCoplanarPolygons: should merge adjacent polygons with shared e
103
104
  t.is(obs.length, 1)
104
105
  t.is(obs[0].vertices.length, 4) // rectangle has 4 vertices
105
106
  })
107
+
108
+ test('retessellateCoplanarPolygons: should not return invalid polygons', (t) => {
109
+ const polyA = poly3.create([
110
+ [ 3.72172376113686, -3.7661089598376325, 13 ],
111
+ [ 3.72172376113686, -3.7661089598376325, 13.515937566757202 ],
112
+ [ 3.721663581864801, -3.766195461144145, 13.515937566757202 ]
113
+ ])
114
+ const polyB = poly3.create([
115
+ [ 3.721663581864801, -3.766195461144145, 13.0084828728813 ],
116
+ [ 3.721663581864801, -3.766195461144145, 13.00100040435791 ],
117
+ [ 3.721716664059388, -3.7661191611320772, 13.00100040435791 ]
118
+ ])
119
+ const polyC = poly3.create([
120
+ [ 3.721663581864801, -3.766195461144145, 13.515937566757202 ],
121
+ [ 3.721663581864801, -3.766195461144145, 13.0084828728813 ],
122
+ [ 3.7217166640593886, -3.766119161132077, 13.00100040435791 ],
123
+ [ 3.7217236444490864, -3.766109127563903, 13.00100040435791 ]
124
+ ])
125
+ const obs = reTesselateCoplanarPolygons([polyA, polyB, polyC])
126
+ t.is(obs.length, 1)
127
+
128
+ obs.forEach((polygon) => {
129
+ poly3.validate(polygon)
130
+ })
131
+ })
@@ -18,6 +18,7 @@ export const retessellate = (geometry) => {
18
18
  return geometry
19
19
  }
20
20
 
21
+ // FIXME this is creating new polygons
21
22
  const polygons = geom3.toPolygons(geometry).map((polygon, index) => ({ vertices: polygon.vertices, plane: poly3.plane(polygon), index: index }))
22
23
  const classified = classifyPolygons(polygons)
23
24
 
@@ -1,22 +1,38 @@
1
1
  import * as vec2 from '../../maths/vec2/index.js'
2
+ import * as vec3 from '../../maths/vec3/index.js'
2
3
 
3
4
  import * as geom2 from '../../geometries/geom2/index.js'
4
5
  import * as geom3 from '../../geometries/geom3/index.js'
5
6
  import * as path2 from '../../geometries/path2/index.js'
7
+ import * as path3 from '../../geometries/path3/index.js'
6
8
  import * as poly2 from '../../geometries/poly2/index.js'
7
9
 
8
10
  import { measureEpsilon } from '../../measurements/measureEpsilon.js'
9
11
 
10
12
  import { snapPolygons } from './snapPolygons.js'
11
13
 
14
+ /*
15
+ */
12
16
  const snapPath2 = (geometry) => {
13
17
  const epsilon = measureEpsilon(geometry)
14
18
  const points = path2.toPoints(geometry)
15
19
  const newPoints = points.map((point) => vec2.snap(vec2.create(), point, epsilon))
16
20
  // snap can produce duplicate points, remove those
17
- return path2.create(newPoints)
21
+ return path2.fromPoints({ closed: geometry.isClosed }, newPoints)
22
+ }
23
+
24
+ /*
25
+ */
26
+ const snapPath3 = (geometry) => {
27
+ const epsilon = measureEpsilon(geometry)
28
+ const vertices = path3.toVertices(geometry)
29
+ const newVertices = vertices.map((vertice) => vec3.snap(vec3.create(), vertice, epsilon))
30
+ // snap can produce duplicate points, remove those
31
+ return path3.fromVertices({ closed: geometry.isClosed }, newVertices)
18
32
  }
19
33
 
34
+ /*
35
+ */
20
36
  const snapGeom2 = (geometry) => {
21
37
  const epsilon = measureEpsilon(geometry)
22
38
  const outlines = geom2.toOutlines(geometry)
@@ -38,6 +54,8 @@ const snapGeom2 = (geometry) => {
38
54
  return geom2.create(newOutlines)
39
55
  }
40
56
 
57
+ /*
58
+ */
41
59
  const snapGeom3 = (geometry) => {
42
60
  const epsilon = measureEpsilon(geometry)
43
61
  const polygons = geom3.toPolygons(geometry)
@@ -54,9 +72,10 @@ const snapGeom3 = (geometry) => {
54
72
  */
55
73
  export const snap = (...geometries) => {
56
74
  const results = geometries.map((geometry) => {
57
- if (path2.isA(geometry)) return snapPath2(geometry)
58
- if (geom2.isA(geometry)) return snapGeom2(geometry)
59
75
  if (geom3.isA(geometry)) return snapGeom3(geometry)
76
+ if (geom2.isA(geometry)) return snapGeom2(geometry)
77
+ if (path2.isA(geometry)) return snapPath2(geometry)
78
+ if (path3.isA(geometry)) return snapPath3(geometry)
60
79
  if (Array.isArray(geometry)) return snap(...geometry)
61
80
  return geometry
62
81
  })
@@ -15,9 +15,10 @@ test('snap: snap of a path2 produces an expected path2', (t) => {
15
15
  const geometry2 = arc({ radius: 1 / 2, segments: 8 })
16
16
  const geometry3 = arc({ radius: 1.3333333333333333 / 2, segments: 8 })
17
17
  const geometry4 = arc({ radius: TAU / 4 * 1000, segments: 8 })
18
+ const geometry5 = path2.fromPoints({ closed: true }, [[0, 0], [2, 2], [4, 0]])
18
19
 
19
- const results = snap(geometry1, geometry2, geometry3, geometry4)
20
- t.is(results.length, 4)
20
+ const results = snap(geometry1, geometry2, geometry3, geometry4, geometry5)
21
+ t.is(results.length, 5)
21
22
 
22
23
  let pts = path2.toPoints(results[0])
23
24
  let exp = []
@@ -61,6 +62,15 @@ test('snap: snap of a path2 produces an expected path2', (t) => {
61
62
  [1110.7100826766714, -1110.7100826766714]
62
63
  ]
63
64
  t.true(comparePoints(pts, exp))
65
+
66
+ t.true(results[4].isClosed)
67
+ pts = path2.toPoints(results[4])
68
+ exp = [
69
+ [0, 0],
70
+ [2.00001, 2.00001],
71
+ [3.9999900000000004, 0]
72
+ ]
73
+ t.true(comparePoints(pts, exp))
64
74
  })
65
75
 
66
76
  test('snap: snap of a geom2 produces an expected geom2', (t) => {
@@ -91,6 +91,6 @@ test('offsetGeom3: offset completes properly, issue 876', (t) => {
91
91
 
92
92
  const obs = offset({ delta: 1.3, corners: 'round', segments: 12 }, sub)
93
93
  t.notThrows.skip(() => geom3.validate(obs))
94
- t.is(measureArea(obs), 524.9674760547548)
95
- t.is(measureVolume(obs), 604.0599465573156)
94
+ t.is(measureArea(obs), 524.9674756919702)
95
+ t.is(measureVolume(obs), 604.0599468042326)
96
96
  })
@@ -76,12 +76,13 @@ export const align = (options, ...geometries) => {
76
76
  options = validateOptions(options)
77
77
  let { modes, relativeTo, grouped } = options
78
78
 
79
+ geometries = coalesce(geometries)
80
+
79
81
  if (relativeTo.filter((val) => val == null).length) {
80
82
  const bounds = measureAggregateBoundingBox(geometries)
81
83
  relativeTo = populateRelativeToFromBounds(relativeTo, modes, bounds)
82
84
  }
83
85
  if (grouped) {
84
- geometries = coalesce(geometries)
85
86
  geometries = alignGeometries(geometries, modes, relativeTo)
86
87
  } else {
87
88
  geometries = geometries.map((geometry) => alignGeometries(geometry, modes, relativeTo))
@@ -79,7 +79,7 @@ test('align: multiple objects ungrouped, relativeTo is nulls, returns geometry a
79
79
  cube({ size: 2, center: [4, 4, 4] }),
80
80
  cube({ size: 4, center: [10, 10, 10] })
81
81
  ]
82
- const aligned = align({ modes: ['center', 'min', 'max'], relativeTo: [null, null, null], grouped: false }, ...original)
82
+ const aligned = align({ modes: ['center', 'min', 'max'], relativeTo: [null, null, null], grouped: false }, original)
83
83
  const bounds = measureAggregateBoundingBox(aligned)
84
84
  const expectedBounds = [[5.5, 3, 8], [9.5, 7, 12]]
85
85
  t.notThrows(() => geom3.validate(aligned[0]))
@@ -4,6 +4,8 @@ import * as plane from '../../maths/plane/index.js'
4
4
  import * as geom2 from '../../geometries/geom2/index.js'
5
5
  import * as geom3 from '../../geometries/geom3/index.js'
6
6
  import * as path2 from '../../geometries/path2/index.js'
7
+ import * as path3 from '../../geometries/path3/index.js'
8
+ import * as slice from '../../geometries/slice/index.js'
7
9
 
8
10
  /**
9
11
  * Mirror the given objects using the given options.
@@ -33,9 +35,11 @@ export const mirror = (options, ...objects) => {
33
35
  const matrix = mat4.mirrorByPlane(mat4.create(), planeOfMirror)
34
36
 
35
37
  const results = objects.map((object) => {
36
- if (path2.isA(object)) return path2.transform(matrix, object)
37
- if (geom2.isA(object)) return geom2.transform(matrix, object)
38
38
  if (geom3.isA(object)) return geom3.transform(matrix, object)
39
+ if (geom2.isA(object)) return geom2.transform(matrix, object)
40
+ if (path2.isA(object)) return path2.transform(matrix, object)
41
+ if (path3.isA(object)) return path3.transform(matrix, object)
42
+ if (slice.isA(object)) return slice.transform(matrix, object)
39
43
  // handle recursive arrays
40
44
  if (Array.isArray(object)) return mirror(options, ...object)
41
45
  return object
@@ -3,6 +3,8 @@ import * as mat4 from '../../maths/mat4/index.js'
3
3
  import * as geom2 from '../../geometries/geom2/index.js'
4
4
  import * as geom3 from '../../geometries/geom3/index.js'
5
5
  import * as path2 from '../../geometries/path2/index.js'
6
+ import * as path3 from '../../geometries/path3/index.js'
7
+ import * as slice from '../../geometries/slice/index.js'
6
8
 
7
9
  /**
8
10
  * Rotate the given objects using the given options.
@@ -28,9 +30,11 @@ export const rotate = (angles, ...objects) => {
28
30
  const matrix = mat4.fromTaitBryanRotation(mat4.create(), yaw, pitch, roll)
29
31
 
30
32
  const results = objects.map((object) => {
31
- if (path2.isA(object)) return path2.transform(matrix, object)
32
- if (geom2.isA(object)) return geom2.transform(matrix, object)
33
33
  if (geom3.isA(object)) return geom3.transform(matrix, object)
34
+ if (geom2.isA(object)) return geom2.transform(matrix, object)
35
+ if (path2.isA(object)) return path2.transform(matrix, object)
36
+ if (path3.isA(object)) return path3.transform(matrix, object)
37
+ if (slice.isA(object)) return slice.transform(matrix, object)
34
38
  // handle recursive arrays
35
39
  if (Array.isArray(object)) return rotate(angles, ...object)
36
40
  return object
@@ -3,6 +3,8 @@ import * as mat4 from '../../maths/mat4/index.js'
3
3
  import * as geom2 from '../../geometries/geom2/index.js'
4
4
  import * as geom3 from '../../geometries/geom3/index.js'
5
5
  import * as path2 from '../../geometries/path2/index.js'
6
+ import * as path3 from '../../geometries/path3/index.js'
7
+ import * as slice from '../../geometries/slice/index.js'
6
8
 
7
9
  /**
8
10
  * Scale the given objects using the given options.
@@ -26,9 +28,11 @@ export const scale = (factors, ...objects) => {
26
28
  const matrix = mat4.fromScaling(mat4.create(), factors)
27
29
 
28
30
  const results = objects.map((object) => {
29
- if (path2.isA(object)) return path2.transform(matrix, object)
30
- if (geom2.isA(object)) return geom2.transform(matrix, object)
31
31
  if (geom3.isA(object)) return geom3.transform(matrix, object)
32
+ if (geom2.isA(object)) return geom2.transform(matrix, object)
33
+ if (path2.isA(object)) return path2.transform(matrix, object)
34
+ if (path3.isA(object)) return path3.transform(matrix, object)
35
+ if (slice.isA(object)) return slice.transform(matrix, object)
32
36
  // handle recursive arrays
33
37
  if (Array.isArray(object)) return scale(factors, ...object)
34
38
  return object
@@ -1,6 +1,8 @@
1
1
  import * as geom2 from '../../geometries/geom2/index.js'
2
2
  import * as geom3 from '../../geometries/geom3/index.js'
3
3
  import * as path2 from '../../geometries/path2/index.js'
4
+ import * as path3 from '../../geometries/path3/index.js'
5
+ import * as slice from '../../geometries/slice/index.js'
4
6
 
5
7
  /**
6
8
  * Transform the given objects using the given matrix.
@@ -16,9 +18,11 @@ export const transform = (matrix, ...objects) => {
16
18
  // TODO how to check that the matrix is REAL?
17
19
 
18
20
  const results = objects.map((object) => {
19
- if (path2.isA(object)) return path2.transform(matrix, object)
20
- if (geom2.isA(object)) return geom2.transform(matrix, object)
21
21
  if (geom3.isA(object)) return geom3.transform(matrix, object)
22
+ if (geom2.isA(object)) return geom2.transform(matrix, object)
23
+ if (path2.isA(object)) return path2.transform(matrix, object)
24
+ if (path3.isA(object)) return path3.transform(matrix, object)
25
+ if (slice.isA(object)) return slice.transform(matrix, object)
22
26
  // handle recursive arrays
23
27
  if (Array.isArray(object)) return transform(matrix, ...object)
24
28
  return object
@@ -4,11 +4,11 @@ import { comparePoints, comparePolygonsAsPoints } from '../../../test/helpers/in
4
4
 
5
5
  import { mat4 } from '../../maths/index.js'
6
6
 
7
- import { geom2, geom3, path2 } from '../../geometries/index.js'
7
+ import { geom2, geom3, path2, path3 } from '../../geometries/index.js'
8
8
 
9
9
  import { transform } from './index.js'
10
10
 
11
- test('transform: transforming of a path2 produces expected changes to points', (t) => {
11
+ test('transform: (path2)', (t) => {
12
12
  const matrix = mat4.fromTranslation(mat4.create(), [2, 2, 0])
13
13
  let geometry = path2.fromPoints({}, [[0, 0], [1, 0]])
14
14
 
@@ -19,7 +19,18 @@ test('transform: transforming of a path2 produces expected changes to points', (
19
19
  t.true(comparePoints(obs, exp))
20
20
  })
21
21
 
22
- test('transform: transforming of a geom2 produces expected changes to sides', (t) => {
22
+ test('transform: (path3)', (t) => {
23
+ const matrix = mat4.fromTranslation(mat4.create(), [2, 2, 2])
24
+ let geometry = path3.fromVertices({ closed: true }, [[0, 0, 0], [1, 0, 1], [3, 2, 1]])
25
+
26
+ geometry = transform(matrix, geometry)
27
+ const obs = path3.toVertices(geometry)
28
+ const exp = [[2, 2, 2], [3, 2, 3], [5, 4, 3]]
29
+ t.notThrows(() => path3.validate(geometry))
30
+ t.true(comparePoints(obs, exp))
31
+ })
32
+
33
+ test('transform: (geom2)', (t) => {
23
34
  const matrix = mat4.fromScaling(mat4.create(), [5, 5, 5])
24
35
  let geometry = geom2.create([[[0, 0], [1, 0], [0, 1]]])
25
36
 
@@ -30,7 +41,7 @@ test('transform: transforming of a geom2 produces expected changes to sides', (t
30
41
  t.true(comparePoints(obs, exp))
31
42
  })
32
43
 
33
- test('transform: transforming of a geom3 produces expected changes to polygons', (t) => {
44
+ test('transform: (geom3)', (t) => {
34
45
  const matrix = mat4.fromTranslation(mat4.create(), [-3, -3, -3])
35
46
  const points = [
36
47
  [[-2, -7, -12], [-2, -7, 18], [-2, 13, 18], [-2, 13, -12]],
@@ -55,7 +66,7 @@ test('transform: transforming of a geom3 produces expected changes to polygons',
55
66
  t.true(comparePolygonsAsPoints(obs, exp))
56
67
  })
57
68
 
58
- test('transform: transforming of multiple objects produces expected changes', (t) => {
69
+ test('transform: (multiple objects)', (t) => {
59
70
  const junk = 'hello'
60
71
  const geometry1 = path2.fromPoints({}, [[-5, 5], [5, 5], [-5, -5], [10, -5]])
61
72
  const geometry2 = geom2.create([[[-5, -5], [0, 5], [10, -5]]])