@jscad/modeling 2.7.1 → 2.9.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 +46 -0
- package/dist/jscad-modeling.min.js +174 -144
- package/package.json +2 -2
- package/src/colors/hslToRgb.js +1 -1
- package/src/colors/hueToColorComponent.js +1 -0
- package/src/curves/bezier/tangentAt.js +2 -2
- package/src/curves/bezier/tangentAt.test.js +1 -1
- package/src/curves/bezier/valueAt.test.js +1 -1
- package/src/geometries/geom2/index.js +10 -0
- package/src/geometries/geom2/isA.js +2 -2
- package/src/geometries/geom2/toCompactBinary.js +4 -4
- package/src/geometries/geom2/toOutlines.js +6 -11
- package/src/geometries/geom2/toString.js +1 -1
- package/src/geometries/geom2/transform.test.js +1 -1
- package/src/geometries/geom3/fromCompactBinary.js +1 -1
- package/src/geometries/geom3/index.js +17 -0
- package/src/geometries/geom3/invert.js +2 -2
- package/src/geometries/geom3/isA.js +2 -2
- package/src/geometries/geom3/toCompactBinary.js +4 -4
- package/src/geometries/geom3/toPoints.js +1 -0
- package/src/geometries/geom3/toString.js +1 -1
- package/src/geometries/geom3/transform.test.js +1 -1
- package/src/geometries/index.js +8 -1
- package/src/geometries/path2/eachPoint.js +3 -3
- package/src/geometries/path2/index.js +11 -0
- package/src/geometries/path2/isA.js +2 -2
- package/src/geometries/path2/reverse.js +4 -4
- package/src/geometries/path2/toCompactBinary.js +6 -6
- package/src/geometries/path2/toString.js +1 -1
- package/src/geometries/path2/transform.test.js +1 -1
- package/src/geometries/poly2/arePointsInside.test.js +1 -1
- package/src/geometries/poly2/index.js +6 -0
- package/src/geometries/poly3/index.js +7 -1
- package/src/geometries/poly3/invert.js +7 -1
- package/src/geometries/poly3/isA.js +2 -2
- package/src/geometries/poly3/isConvex.js +2 -2
- package/src/geometries/poly3/measureArea.js +4 -4
- package/src/geometries/poly3/measureBoundingBox.js +2 -2
- package/src/geometries/poly3/measureBoundingSphere.js +2 -2
- package/src/geometries/poly3/measureSignedVolume.js +4 -4
- package/src/geometries/poly3/toPoints.js +2 -2
- package/src/geometries/poly3/toString.js +2 -2
- package/src/geometries/poly3/transform.js +2 -2
- package/src/maths/index.js +1 -1
- package/src/maths/line2/equals.js +2 -2
- package/src/maths/line2/fromValues.js +2 -2
- package/src/maths/line2/intersectPointOfLines.js +1 -1
- package/src/maths/line2/intersectPointOfLines.test.js +1 -1
- package/src/maths/line2/reverse.test.js +1 -1
- package/src/maths/line2/transform.test.js +1 -1
- package/src/maths/line3/equals.js +2 -2
- package/src/maths/line3/reverse.test.js +1 -1
- package/src/maths/line3/transform.test.js +1 -1
- package/src/maths/mat4/fromRotation.js +1 -1
- package/src/maths/mat4/fromVectorRotation.js +1 -1
- package/src/maths/mat4/fromVectorRotation.test.js +1 -1
- package/src/maths/mat4/identity.test.js +1 -1
- package/src/maths/mat4/invert.js +18 -18
- package/src/maths/mat4/isIdentity.js +1 -1
- package/src/maths/mat4/isIdentity.test.js +0 -2
- package/src/maths/mat4/isMirroring.js +4 -4
- package/src/maths/mat4/isMirroring.test.js +1 -1
- package/src/maths/mat4/leftMultiplyVec3.js +2 -2
- package/src/maths/mat4/rotate.js +1 -1
- package/src/maths/mat4/toString.js +2 -2
- package/src/maths/mat4/translate.test.js +1 -1
- package/src/maths/plane/flip.test.js +1 -1
- package/src/maths/plane/fromPoints.d.ts +1 -1
- package/src/maths/plane/fromPoints.js +1 -3
- package/src/maths/plane/signedDistanceToPoint.js +1 -1
- package/src/maths/plane/transform.test.js +1 -1
- package/src/maths/utils/aboutEqualNormals.js +2 -2
- package/src/maths/vec2/abs.d.ts +1 -1
- package/src/maths/vec2/add.test.js +1 -1
- package/src/maths/vec2/angleDegrees.d.ts +1 -1
- package/src/maths/vec2/angleRadians.d.ts +1 -1
- package/src/maths/vec2/create.js +1 -1
- package/src/maths/vec2/cross.test.js +1 -1
- package/src/maths/vec2/divide.test.js +1 -1
- package/src/maths/vec2/fromAngleDegrees.js +1 -1
- package/src/maths/vec2/fromScalar.js +1 -1
- package/src/maths/vec2/length.d.ts +1 -1
- package/src/maths/vec2/length.js +1 -1
- package/src/maths/vec2/length.test.js +10 -0
- package/src/maths/vec2/lerp.test.js +1 -1
- package/src/maths/vec2/multiply.test.js +1 -1
- package/src/maths/vec2/negate.test.js +1 -1
- package/src/maths/vec2/normal.js +1 -1
- package/src/maths/vec2/normalize.d.ts +1 -1
- package/src/maths/vec2/normalize.test.js +1 -1
- package/src/maths/vec2/rotate.test.js +1 -1
- package/src/maths/vec2/squaredLength.d.ts +1 -1
- package/src/maths/vec2/squaredLength.js +3 -3
- package/src/maths/vec2/subtract.test.js +1 -1
- package/src/maths/vec2/toString.js +1 -1
- package/src/maths/vec2/transform.test.js +1 -1
- package/src/maths/vec3/abs.d.ts +1 -1
- package/src/maths/vec3/add.test.js +1 -1
- package/src/maths/vec3/angle.js +2 -2
- package/src/maths/vec3/angle.test.js +17 -0
- package/src/maths/vec3/cross.test.js +1 -1
- package/src/maths/vec3/divide.test.js +1 -1
- package/src/maths/vec3/fromScalar.js +1 -1
- package/src/maths/vec3/fromVec2.d.ts +1 -1
- package/src/maths/vec3/fromVec2.js +3 -3
- package/src/maths/vec3/length.d.ts +1 -1
- package/src/maths/vec3/length.js +4 -4
- package/src/maths/vec3/length.test.js +10 -0
- package/src/maths/vec3/lerp.test.js +1 -1
- package/src/maths/vec3/multiply.test.js +1 -1
- package/src/maths/vec3/negate.d.ts +1 -1
- package/src/maths/vec3/negate.test.js +1 -1
- package/src/maths/vec3/normalize.d.ts +1 -1
- package/src/maths/vec3/normalize.test.js +1 -1
- package/src/maths/vec3/rotateX.test.js +1 -1
- package/src/maths/vec3/rotateY.test.js +1 -1
- package/src/maths/vec3/rotateZ.test.js +1 -1
- package/src/maths/vec3/scale.test.js +1 -1
- package/src/maths/vec3/squaredLength.d.ts +1 -1
- package/src/maths/vec3/squaredLength.js +4 -4
- package/src/maths/vec3/subtract.test.js +1 -1
- package/src/maths/vec3/toString.js +1 -1
- package/src/maths/vec3/transform.test.js +1 -1
- package/src/maths/vec4/toString.js +1 -1
- package/src/maths/vec4/transform.test.js +1 -1
- package/src/measurements/measureBoundingSphere.js +4 -4
- package/src/measurements/measureCenterOfMass.js +1 -1
- package/src/operations/booleans/mayOverlap.js +3 -3
- package/src/operations/booleans/retessellate.js +3 -5
- package/src/operations/booleans/scission.js +1 -1
- package/src/operations/booleans/subtract.js +1 -1
- package/src/operations/booleans/union.test.js +1 -1
- package/src/operations/booleans/unionGeom3Sub.js +1 -1
- package/src/operations/expansions/expand.js +2 -2
- package/src/operations/expansions/expand.test.js +3 -35
- package/src/operations/expansions/expandShell.js +24 -18
- package/src/operations/expansions/offset.js +1 -1
- package/src/operations/expansions/offset.test.js +25 -89
- package/src/operations/expansions/offsetFromPoints.js +11 -6
- package/src/operations/extrusions/earcut/assignHoles.js +87 -0
- package/src/operations/extrusions/earcut/assignHoles.test.js +28 -0
- package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
- package/src/operations/extrusions/earcut/index.js +252 -0
- package/src/operations/extrusions/earcut/linkedList.js +58 -0
- package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
- package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
- package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
- package/src/operations/extrusions/earcut/triangle.js +16 -0
- package/src/operations/extrusions/extrudeFromSlices.js +10 -3
- package/src/operations/extrusions/extrudeFromSlices.test.js +33 -23
- package/src/operations/extrusions/extrudeLinear.js +11 -6
- package/src/operations/extrusions/extrudeLinear.test.js +77 -27
- package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
- package/src/operations/extrusions/extrudeLinearPath2.js +24 -0
- package/src/operations/extrusions/extrudeRectangular.js +1 -1
- package/src/operations/extrusions/extrudeRectangular.test.js +7 -7
- package/src/operations/extrusions/extrudeRotate.test.js +19 -27
- package/src/operations/extrusions/project.js +1 -1
- package/src/operations/extrusions/slice/calculatePlane.js +7 -4
- package/src/operations/extrusions/slice/isA.js +2 -2
- package/src/operations/extrusions/slice/repairSlice.js +47 -0
- package/src/operations/extrusions/slice/toPolygons.js +24 -60
- package/src/operations/hulls/hull.test.js +1 -1
- package/src/operations/hulls/hullChain.js +1 -1
- package/src/operations/hulls/hullGeom2.js +1 -1
- package/src/operations/hulls/hullPath2.js +6 -4
- package/src/operations/hulls/hullPath2.test.js +16 -0
- package/src/operations/hulls/hullPoints2.test.js +1 -1
- package/src/operations/hulls/quickhull/QuickHull.js +2 -2
- package/src/operations/modifiers/edges.js +2 -4
- package/src/operations/modifiers/generalize.js +4 -7
- package/src/operations/modifiers/snap.test.js +3 -3
- package/src/operations/transforms/align.d.ts +1 -1
- package/src/operations/transforms/center.js +17 -17
- package/src/operations/transforms/mirror.js +11 -11
- package/src/operations/transforms/rotate.js +12 -12
- package/src/operations/transforms/scale.js +19 -19
- package/src/operations/transforms/transform.js +3 -3
- package/src/operations/transforms/translate.js +14 -14
- package/src/primitives/arc.js +1 -1
- package/src/primitives/cylinderElliptic.test.js +0 -2
- package/src/primitives/ellipsoid.js +1 -1
- package/src/primitives/ellipsoid.test.js +0 -2
- package/src/primitives/geodesicSphere.d.ts +0 -1
- package/src/primitives/geodesicSphere.js +2 -2
- package/src/primitives/polyhedron.js +1 -1
- package/src/primitives/roundedCylinder.js +1 -1
- package/src/primitives/torus.d.ts +0 -1
- package/src/primitives/torus.test.js +1 -1
- package/src/primitives/triangle.js +1 -1
- package/src/text/vectorText.js +2 -2
- package/src/utils/padArrayToLength.js +1 -1
- package/test/helpers/comparePolygons.js +1 -3
- package/test/helpers/nearlyEqual.js +2 -6
|
@@ -46,7 +46,7 @@ test('offset: offsetting a bent line produces expected geometry', (t) => {
|
|
|
46
46
|
// offset it by -2.
|
|
47
47
|
offsetLinePath2 = offset({ delta: -2, corners: 'edge', segments: 8 }, linePath2)
|
|
48
48
|
offsetPoints = path2.toPoints(offsetLinePath2)
|
|
49
|
-
t.is(offsetPoints.length,
|
|
49
|
+
t.is(offsetPoints.length, 5)
|
|
50
50
|
boundingBox = measureBoundingBox(offsetLinePath2)
|
|
51
51
|
t.true(comparePoints(boundingBox, [[-2, 0, 0], [10, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
|
|
52
52
|
})
|
|
@@ -124,20 +124,12 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
124
124
|
let pts = path2.toPoints(obs)
|
|
125
125
|
let exp = [
|
|
126
126
|
[-5, -6],
|
|
127
|
-
[5, -6],
|
|
128
127
|
[6, -6],
|
|
129
|
-
[6, -5],
|
|
130
|
-
[6, 5],
|
|
131
128
|
[6, 6],
|
|
132
|
-
[5, 6],
|
|
133
|
-
[3, 6],
|
|
134
129
|
[2, 6],
|
|
135
|
-
[2, 5],
|
|
136
130
|
[2, 1],
|
|
137
131
|
[-2, 1],
|
|
138
|
-
[-2, 5],
|
|
139
132
|
[-1.9999999999999996, 6],
|
|
140
|
-
[-3, 6],
|
|
141
133
|
[-5, 6]
|
|
142
134
|
]
|
|
143
135
|
t.true(comparePoints(pts, exp))
|
|
@@ -145,26 +137,14 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
145
137
|
obs = offset({ delta: 1, corners: 'edge' }, closeline)
|
|
146
138
|
pts = path2.toPoints(obs)
|
|
147
139
|
exp = [
|
|
148
|
-
[-6, -6],
|
|
149
|
-
[-5, -6],
|
|
150
|
-
[5, -6],
|
|
151
140
|
[6, -6],
|
|
152
|
-
[6, -5],
|
|
153
|
-
[6, 5],
|
|
154
141
|
[6, 6],
|
|
155
|
-
[5, 6],
|
|
156
|
-
[3, 6],
|
|
157
142
|
[2, 6],
|
|
158
|
-
[2, 5],
|
|
159
143
|
[2, 1],
|
|
160
144
|
[-2, 1],
|
|
161
|
-
[-2, 5],
|
|
162
145
|
[-1.9999999999999996, 6],
|
|
163
|
-
[-3, 6],
|
|
164
|
-
[-5, 6],
|
|
165
146
|
[-6, 6],
|
|
166
|
-
[-6,
|
|
167
|
-
[-6, -5]
|
|
147
|
+
[-6, -6]
|
|
168
148
|
]
|
|
169
149
|
t.true(comparePoints(pts, exp))
|
|
170
150
|
|
|
@@ -175,12 +155,8 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
175
155
|
[4.5, -4.5],
|
|
176
156
|
[4.5, 4.5],
|
|
177
157
|
[3.5, 4.5],
|
|
178
|
-
[3.5, -3.061616997868383e-17],
|
|
179
158
|
[3.4999999999999996, -0.5],
|
|
180
|
-
[3, -0.5],
|
|
181
|
-
[-3, -0.5],
|
|
182
159
|
[-3.5, -0.4999999999999996],
|
|
183
|
-
[-3.5, 3.061616997868383e-17],
|
|
184
160
|
[-3.5, 4.5],
|
|
185
161
|
[-5, 4.5]
|
|
186
162
|
]
|
|
@@ -193,12 +169,8 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
193
169
|
[4.5, -4.5],
|
|
194
170
|
[4.5, 4.5],
|
|
195
171
|
[3.5, 4.5],
|
|
196
|
-
[3.5, -3.061616997868383e-17],
|
|
197
172
|
[3.4999999999999996, -0.5],
|
|
198
|
-
[3, -0.5],
|
|
199
|
-
[-3, -0.5],
|
|
200
173
|
[-3.5, -0.4999999999999996],
|
|
201
|
-
[-3.5, 3.061616997868383e-17],
|
|
202
174
|
[-3.5, 4.5],
|
|
203
175
|
[-4.5, 4.5]
|
|
204
176
|
]
|
|
@@ -373,26 +345,14 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
|
|
|
373
345
|
obs = offset({ delta: 1, corners: 'edge' }, geometry)
|
|
374
346
|
pts = geom2.toPoints(obs)
|
|
375
347
|
exp = [
|
|
376
|
-
[-6, -6],
|
|
377
|
-
[-5, -6],
|
|
378
|
-
[5, -6],
|
|
379
348
|
[6, -6],
|
|
380
|
-
[6, -5],
|
|
381
|
-
[6, 5],
|
|
382
349
|
[6, 6],
|
|
383
|
-
[5, 6],
|
|
384
|
-
[3, 6],
|
|
385
350
|
[2, 6],
|
|
386
|
-
[2, 5],
|
|
387
351
|
[2, 1],
|
|
388
352
|
[-2, 1],
|
|
389
|
-
[-2, 5],
|
|
390
353
|
[-1.9999999999999996, 6],
|
|
391
|
-
[-3, 6],
|
|
392
|
-
[-5, 6],
|
|
393
354
|
[-6, 6],
|
|
394
|
-
[-6,
|
|
395
|
-
[-6, -5]
|
|
355
|
+
[-6, -6]
|
|
396
356
|
]
|
|
397
357
|
t.true(comparePoints(pts, exp))
|
|
398
358
|
|
|
@@ -422,78 +382,54 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
|
|
|
422
382
|
|
|
423
383
|
test('offset (options): offsetting of a complex geom2 produces expected offset geom2', (t) => {
|
|
424
384
|
const geometry = geom2.create([
|
|
425
|
-
[[-75
|
|
426
|
-
[[-75
|
|
427
|
-
[[75
|
|
428
|
-
[[-40
|
|
429
|
-
[[75
|
|
430
|
-
[[40
|
|
431
|
-
[[40
|
|
432
|
-
[[-40
|
|
433
|
-
[[15
|
|
434
|
-
[[-15
|
|
435
|
-
[[-15
|
|
436
|
-
[[-8
|
|
437
|
-
[[15
|
|
438
|
-
[[-8
|
|
439
|
-
[[8
|
|
440
|
-
[[8
|
|
441
|
-
[[-2
|
|
442
|
-
[[-2
|
|
443
|
-
[[2
|
|
444
|
-
[[2
|
|
385
|
+
[[-75, 75], [-75, -75]],
|
|
386
|
+
[[-75, -75], [75, -75]],
|
|
387
|
+
[[75, -75], [75, 75]],
|
|
388
|
+
[[-40, 75], [-75, 75]],
|
|
389
|
+
[[75, 75], [40, 75]],
|
|
390
|
+
[[40, 75], [40, 0]],
|
|
391
|
+
[[40, 0], [-40, 0]],
|
|
392
|
+
[[-40, 0], [-40, 75]],
|
|
393
|
+
[[15, -10], [15, -40]],
|
|
394
|
+
[[-15, -10], [15, -10]],
|
|
395
|
+
[[-15, -40], [-15, -10]],
|
|
396
|
+
[[-8, -40], [-15, -40]],
|
|
397
|
+
[[15, -40], [8, -40]],
|
|
398
|
+
[[-8, -25], [-8, -40]],
|
|
399
|
+
[[8, -25], [-8, -25]],
|
|
400
|
+
[[8, -40], [8, -25]],
|
|
401
|
+
[[-2, -15], [-2, -19]],
|
|
402
|
+
[[-2, -19], [2, -19]],
|
|
403
|
+
[[2, -19], [2, -15]],
|
|
404
|
+
[[2, -15], [-2, -15]]
|
|
445
405
|
])
|
|
446
406
|
|
|
447
407
|
// expand +
|
|
448
408
|
const obs = offset({ delta: 2, corners: 'edge' }, geometry)
|
|
449
409
|
const pts = geom2.toPoints(obs)
|
|
450
410
|
const exp = [
|
|
451
|
-
[-77, -77],
|
|
452
|
-
[-75, -77],
|
|
453
|
-
[75, -77],
|
|
454
411
|
[77, -77],
|
|
455
|
-
[77, -75],
|
|
456
|
-
[77, 75],
|
|
457
412
|
[77, 77],
|
|
458
|
-
[75, 77],
|
|
459
|
-
[40, 77],
|
|
460
413
|
[38, 77],
|
|
461
|
-
[38, 75],
|
|
462
414
|
[38, 2],
|
|
463
415
|
[-38, 2],
|
|
464
|
-
[-38, 75],
|
|
465
416
|
[-37.99999999999999, 77],
|
|
466
|
-
[-40, 77],
|
|
467
|
-
[-75, 77],
|
|
468
417
|
[-77, 77],
|
|
469
|
-
[-77, 75],
|
|
470
418
|
[13, -12],
|
|
471
419
|
[13, -38],
|
|
472
420
|
[10, -38],
|
|
473
|
-
[10, -25],
|
|
474
421
|
[10, -23],
|
|
475
|
-
[8, -23],
|
|
476
|
-
[-8, -23],
|
|
477
422
|
[-10, -23],
|
|
478
|
-
[-10, -25],
|
|
479
423
|
[-10, -38],
|
|
480
424
|
[-13, -38],
|
|
481
425
|
[-13, -12],
|
|
482
|
-
[-4, -19],
|
|
483
426
|
[-4, -21],
|
|
484
|
-
[-2, -21],
|
|
485
|
-
[1.9999999999999998, -21],
|
|
486
427
|
[3.9999999999999996, -21],
|
|
487
|
-
[4, -19],
|
|
488
|
-
[4, -15],
|
|
489
428
|
[4, -13],
|
|
490
|
-
[2, -13],
|
|
491
|
-
[-1.9999999999999998, -13],
|
|
492
429
|
[-4, -13],
|
|
493
|
-
[-
|
|
494
|
-
[-77, -75]
|
|
430
|
+
[-77, -77]
|
|
495
431
|
]
|
|
496
|
-
t.is(pts.length,
|
|
432
|
+
t.is(pts.length, 20)
|
|
497
433
|
t.true(comparePoints(pts, exp))
|
|
498
434
|
})
|
|
499
435
|
|
|
@@ -34,7 +34,7 @@ const offsetFromPoints = (options, points) => {
|
|
|
34
34
|
delta = Math.abs(delta) // sign is no longer required
|
|
35
35
|
|
|
36
36
|
let previousSegment = null
|
|
37
|
-
|
|
37
|
+
let newPoints = []
|
|
38
38
|
const newCorners = []
|
|
39
39
|
const of = vec2.create()
|
|
40
40
|
const n = points.length
|
|
@@ -94,6 +94,10 @@ const offsetFromPoints = (options, points) => {
|
|
|
94
94
|
// generate corners if necessary
|
|
95
95
|
|
|
96
96
|
if (corners === 'edge') {
|
|
97
|
+
// map for fast point index lookup
|
|
98
|
+
const pointIndex = new Map() // {point: index}
|
|
99
|
+
newPoints.forEach((point, index) => pointIndex.set(point, index))
|
|
100
|
+
|
|
97
101
|
// create edge corners
|
|
98
102
|
const line0 = line2.create()
|
|
99
103
|
const line1 = line2.create()
|
|
@@ -103,16 +107,17 @@ const offsetFromPoints = (options, points) => {
|
|
|
103
107
|
const ip = line2.intersectPointOfLines(line0, line1)
|
|
104
108
|
if (Number.isFinite(ip[0]) && Number.isFinite(ip[1])) {
|
|
105
109
|
const p0 = corner.s0[1]
|
|
106
|
-
|
|
107
|
-
i =
|
|
108
|
-
newPoints
|
|
110
|
+
const i = pointIndex.get(p0)
|
|
111
|
+
newPoints[i] = ip
|
|
112
|
+
newPoints[(i + 1) % newPoints.length] = undefined
|
|
109
113
|
} else {
|
|
110
114
|
// paralell segments, drop one
|
|
111
115
|
const p0 = corner.s1[0]
|
|
112
|
-
const i =
|
|
113
|
-
newPoints
|
|
116
|
+
const i = pointIndex.get(p0)
|
|
117
|
+
newPoints[i] = undefined
|
|
114
118
|
}
|
|
115
119
|
})
|
|
120
|
+
newPoints = newPoints.filter((p) => p !== undefined)
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
if (corners === 'round') {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { area } = require('../../../maths/utils')
|
|
2
|
+
const { toOutlines } = require('../../../geometries/geom2')
|
|
3
|
+
const { arePointsInside } = require('../../../geometries/poly2')
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Constructs a polygon hierarchy of solids and holes.
|
|
7
|
+
* The hierarchy is represented as a forest of trees. All trees shall be depth at most 2.
|
|
8
|
+
* If a solid exists inside the hole of another solid, it will be split out as its own root.
|
|
9
|
+
*
|
|
10
|
+
* @param {geom2} geometry
|
|
11
|
+
* @returns {Array} an array of polygons with associated holes
|
|
12
|
+
* @alias module:modeling/geometries/geom2.toTree
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const geometry = subtract(rectangle({size: [5, 5]}), rectangle({size: [3, 3]}))
|
|
16
|
+
* console.log(assignHoles(geometry))
|
|
17
|
+
* [{
|
|
18
|
+
* "solid": [[-2.5,-2.5],[2.5,-2.5],[2.5,2.5],[-2.5,2.5]],
|
|
19
|
+
* "holes": [[[-1.5,1.5],[1.5,1.5],[1.5,-1.5],[-1.5,-1.5]]]
|
|
20
|
+
* }]
|
|
21
|
+
*/
|
|
22
|
+
const assignHoles = (geometry) => {
|
|
23
|
+
const outlines = toOutlines(geometry)
|
|
24
|
+
const solids = [] // solid indices
|
|
25
|
+
const holes = [] // hole indices
|
|
26
|
+
outlines.forEach((outline, i) => {
|
|
27
|
+
const a = area(outline)
|
|
28
|
+
if (a < 0) {
|
|
29
|
+
holes.push(i)
|
|
30
|
+
} else if (a > 0) {
|
|
31
|
+
solids.push(i)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
// for each hole, determine what solids it is inside of
|
|
35
|
+
const children = [] // child holes of solid[i]
|
|
36
|
+
const parents = [] // parent solids of hole[i]
|
|
37
|
+
solids.forEach((s, i) => {
|
|
38
|
+
const solid = outlines[s]
|
|
39
|
+
children[i] = []
|
|
40
|
+
holes.forEach((h, j) => {
|
|
41
|
+
const hole = outlines[h]
|
|
42
|
+
// check if a point of hole j is inside solid i
|
|
43
|
+
if (arePointsInside([hole[0]], { vertices: solid })) {
|
|
44
|
+
children[i].push(h)
|
|
45
|
+
if (!parents[j]) parents[j] = []
|
|
46
|
+
parents[j].push(i)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
// check if holes have multiple parents and choose one with fewest children
|
|
51
|
+
holes.forEach((h, j) => {
|
|
52
|
+
// ensure at least one parent exists
|
|
53
|
+
if (parents[j] && parents[j].length > 1) {
|
|
54
|
+
const parent = minIndex(parents[j], (p) => p.length)
|
|
55
|
+
parents[j].forEach((p, i) => {
|
|
56
|
+
if (i !== parent) {
|
|
57
|
+
// Remove hole from skip level parents
|
|
58
|
+
children[p] = children[p].filter((c) => c !== j)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
// map indices back to points
|
|
64
|
+
return children.map((holes, i) => ({
|
|
65
|
+
solid: outlines[solids[i]],
|
|
66
|
+
holes: holes.map((h) => outlines[h])
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* Find the item in the list with smallest score(item).
|
|
72
|
+
* If the list is empty, return undefined.
|
|
73
|
+
*/
|
|
74
|
+
const minIndex = (list, score) => {
|
|
75
|
+
let bestIndex
|
|
76
|
+
let best
|
|
77
|
+
list.forEach((item, index) => {
|
|
78
|
+
const value = score(item)
|
|
79
|
+
if (best === undefined || value < best) {
|
|
80
|
+
bestIndex = index
|
|
81
|
+
best = value
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
return bestIndex
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = assignHoles
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const test = require('ava')
|
|
2
|
+
|
|
3
|
+
const { subtract } = require('../../../operations/booleans')
|
|
4
|
+
const rectangle = require('../../../primitives/rectangle')
|
|
5
|
+
const assignHoles = require('./assignHoles')
|
|
6
|
+
|
|
7
|
+
test('slice: assignHoles() should return a polygon hierarchy', (t) => {
|
|
8
|
+
const exp1 = [{
|
|
9
|
+
solid: [
|
|
10
|
+
[-3.000013333333334, -3.000013333333334],
|
|
11
|
+
[3.000013333333334, -3.000013333333334],
|
|
12
|
+
[3.000013333333334, 3.000013333333334],
|
|
13
|
+
[-3.000013333333334, 3.000013333333334]
|
|
14
|
+
],
|
|
15
|
+
holes: [[
|
|
16
|
+
[-1.9999933333333335, 1.9999933333333335],
|
|
17
|
+
[1.9999933333333335, 1.9999933333333335],
|
|
18
|
+
[1.9999933333333335, -1.9999933333333335],
|
|
19
|
+
[-1.9999933333333335, -1.9999933333333335]
|
|
20
|
+
]]
|
|
21
|
+
}]
|
|
22
|
+
const geometry = subtract(
|
|
23
|
+
rectangle({ size: [6, 6] }),
|
|
24
|
+
rectangle({ size: [4, 4] })
|
|
25
|
+
)
|
|
26
|
+
const obs1 = assignHoles(geometry)
|
|
27
|
+
t.deepEqual(obs1, exp1)
|
|
28
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const { filterPoints, linkedPolygon, locallyInside, splitPolygon } = require('./linkedPolygon')
|
|
2
|
+
const { area, pointInTriangle } = require('./triangle')
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* link every hole into the outer loop, producing a single-ring polygon without holes
|
|
6
|
+
*
|
|
7
|
+
* Original source from https://github.com/mapbox/earcut
|
|
8
|
+
* Copyright (c) 2016 Mapbox
|
|
9
|
+
*/
|
|
10
|
+
const eliminateHoles = (data, holeIndices, outerNode, dim) => {
|
|
11
|
+
const queue = []
|
|
12
|
+
|
|
13
|
+
for (let i = 0, len = holeIndices.length; i < len; i++) {
|
|
14
|
+
const start = holeIndices[i] * dim
|
|
15
|
+
const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length
|
|
16
|
+
const list = linkedPolygon(data, start, end, dim, false)
|
|
17
|
+
if (list === list.next) list.steiner = true
|
|
18
|
+
queue.push(getLeftmost(list))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
queue.sort((a, b) => a.x - b.x) // compare X
|
|
22
|
+
|
|
23
|
+
// process holes from left to right
|
|
24
|
+
for (let i = 0; i < queue.length; i++) {
|
|
25
|
+
outerNode = eliminateHole(queue[i], outerNode)
|
|
26
|
+
outerNode = filterPoints(outerNode, outerNode.next)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return outerNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* find a bridge between vertices that connects hole with an outer ring and link it
|
|
34
|
+
*/
|
|
35
|
+
const eliminateHole = (hole, outerNode) => {
|
|
36
|
+
const bridge = findHoleBridge(hole, outerNode)
|
|
37
|
+
if (!bridge) {
|
|
38
|
+
return outerNode
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bridgeReverse = splitPolygon(bridge, hole)
|
|
42
|
+
|
|
43
|
+
// filter colinear points around the cuts
|
|
44
|
+
const filteredBridge = filterPoints(bridge, bridge.next)
|
|
45
|
+
filterPoints(bridgeReverse, bridgeReverse.next)
|
|
46
|
+
|
|
47
|
+
// Check if input node was removed by the filtering
|
|
48
|
+
return outerNode === bridge ? filteredBridge : outerNode
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* David Eberly's algorithm for finding a bridge between hole and outer polygon
|
|
53
|
+
*/
|
|
54
|
+
const findHoleBridge = (hole, outerNode) => {
|
|
55
|
+
let p = outerNode
|
|
56
|
+
const hx = hole.x
|
|
57
|
+
const hy = hole.y
|
|
58
|
+
let qx = -Infinity
|
|
59
|
+
let m
|
|
60
|
+
|
|
61
|
+
// find a segment intersected by a ray from the hole's leftmost point to the left
|
|
62
|
+
// segment's endpoint with lesser x will be potential connection point
|
|
63
|
+
do {
|
|
64
|
+
if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
|
|
65
|
+
const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y)
|
|
66
|
+
if (x <= hx && x > qx) {
|
|
67
|
+
qx = x
|
|
68
|
+
if (x === hx) {
|
|
69
|
+
if (hy === p.y) return p
|
|
70
|
+
if (hy === p.next.y) return p.next
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
m = p.x < p.next.x ? p : p.next
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
p = p.next
|
|
78
|
+
} while (p !== outerNode)
|
|
79
|
+
|
|
80
|
+
if (!m) return null
|
|
81
|
+
|
|
82
|
+
if (hx === qx) return m // hole touches outer segment; pick leftmost endpoint
|
|
83
|
+
|
|
84
|
+
// look for points inside the triangle of hole point, segment intersection and endpoint
|
|
85
|
+
// if there are no points found, we have a valid connection
|
|
86
|
+
// otherwise choose the point of the minimum angle with the ray as connection point
|
|
87
|
+
|
|
88
|
+
const stop = m
|
|
89
|
+
const mx = m.x
|
|
90
|
+
const my = m.y
|
|
91
|
+
let tanMin = Infinity
|
|
92
|
+
|
|
93
|
+
p = m
|
|
94
|
+
|
|
95
|
+
do {
|
|
96
|
+
if (hx >= p.x && p.x >= mx && hx !== p.x &&
|
|
97
|
+
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
|
|
98
|
+
const tan = Math.abs(hy - p.y) / (hx - p.x) // tangential
|
|
99
|
+
|
|
100
|
+
if (locallyInside(p, hole) && (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
|
|
101
|
+
m = p
|
|
102
|
+
tanMin = tan
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
p = p.next
|
|
107
|
+
} while (p !== stop)
|
|
108
|
+
|
|
109
|
+
return m
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* whether sector in vertex m contains sector in vertex p in the same coordinates
|
|
114
|
+
*/
|
|
115
|
+
const sectorContainsSector = (m, p) => area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* find the leftmost node of a polygon ring
|
|
119
|
+
*/
|
|
120
|
+
const getLeftmost = (start) => {
|
|
121
|
+
let p = start
|
|
122
|
+
let leftmost = start
|
|
123
|
+
do {
|
|
124
|
+
if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p
|
|
125
|
+
p = p.next
|
|
126
|
+
} while (p !== start)
|
|
127
|
+
|
|
128
|
+
return leftmost
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = eliminateHoles
|