@itwin/core-geometry 5.12.0-dev.13 → 5.12.0-dev.14

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 (61) hide show
  1. package/lib/cjs/bspline/AkimaCurve3d.d.ts +5 -0
  2. package/lib/cjs/bspline/AkimaCurve3d.d.ts.map +1 -1
  3. package/lib/cjs/bspline/AkimaCurve3d.js +8 -0
  4. package/lib/cjs/bspline/AkimaCurve3d.js.map +1 -1
  5. package/lib/cjs/bspline/InterpolationCurve3d.d.ts +5 -0
  6. package/lib/cjs/bspline/InterpolationCurve3d.d.ts.map +1 -1
  7. package/lib/cjs/bspline/InterpolationCurve3d.js +8 -0
  8. package/lib/cjs/bspline/InterpolationCurve3d.js.map +1 -1
  9. package/lib/cjs/curve/CurveCollection.d.ts +2 -0
  10. package/lib/cjs/curve/CurveCollection.d.ts.map +1 -1
  11. package/lib/cjs/curve/CurveCollection.js +5 -0
  12. package/lib/cjs/curve/CurveCollection.js.map +1 -1
  13. package/lib/cjs/curve/CurvePrimitive.d.ts +2 -0
  14. package/lib/cjs/curve/CurvePrimitive.d.ts.map +1 -1
  15. package/lib/cjs/curve/CurvePrimitive.js +5 -0
  16. package/lib/cjs/curve/CurvePrimitive.js.map +1 -1
  17. package/lib/cjs/curve/RegionOps.d.ts +60 -6
  18. package/lib/cjs/curve/RegionOps.d.ts.map +1 -1
  19. package/lib/cjs/curve/RegionOps.js +241 -79
  20. package/lib/cjs/curve/RegionOps.js.map +1 -1
  21. package/lib/cjs/curve/RegionOpsClassificationSweeps.d.ts +2 -3
  22. package/lib/cjs/curve/RegionOpsClassificationSweeps.d.ts.map +1 -1
  23. package/lib/cjs/curve/RegionOpsClassificationSweeps.js +4 -5
  24. package/lib/cjs/curve/RegionOpsClassificationSweeps.js.map +1 -1
  25. package/lib/cjs/curve/internalContexts/CurveCurveIntersectXY.js +1 -1
  26. package/lib/cjs/curve/internalContexts/CurveCurveIntersectXY.js.map +1 -1
  27. package/lib/cjs/topology/Graph.d.ts +5 -2
  28. package/lib/cjs/topology/Graph.d.ts.map +1 -1
  29. package/lib/cjs/topology/Graph.js +5 -2
  30. package/lib/cjs/topology/Graph.js.map +1 -1
  31. package/lib/esm/bspline/AkimaCurve3d.d.ts +5 -0
  32. package/lib/esm/bspline/AkimaCurve3d.d.ts.map +1 -1
  33. package/lib/esm/bspline/AkimaCurve3d.js +8 -0
  34. package/lib/esm/bspline/AkimaCurve3d.js.map +1 -1
  35. package/lib/esm/bspline/InterpolationCurve3d.d.ts +5 -0
  36. package/lib/esm/bspline/InterpolationCurve3d.d.ts.map +1 -1
  37. package/lib/esm/bspline/InterpolationCurve3d.js +8 -0
  38. package/lib/esm/bspline/InterpolationCurve3d.js.map +1 -1
  39. package/lib/esm/curve/CurveCollection.d.ts +2 -0
  40. package/lib/esm/curve/CurveCollection.d.ts.map +1 -1
  41. package/lib/esm/curve/CurveCollection.js +5 -0
  42. package/lib/esm/curve/CurveCollection.js.map +1 -1
  43. package/lib/esm/curve/CurvePrimitive.d.ts +2 -0
  44. package/lib/esm/curve/CurvePrimitive.d.ts.map +1 -1
  45. package/lib/esm/curve/CurvePrimitive.js +5 -0
  46. package/lib/esm/curve/CurvePrimitive.js.map +1 -1
  47. package/lib/esm/curve/RegionOps.d.ts +60 -6
  48. package/lib/esm/curve/RegionOps.d.ts.map +1 -1
  49. package/lib/esm/curve/RegionOps.js +241 -79
  50. package/lib/esm/curve/RegionOps.js.map +1 -1
  51. package/lib/esm/curve/RegionOpsClassificationSweeps.d.ts +2 -3
  52. package/lib/esm/curve/RegionOpsClassificationSweeps.d.ts.map +1 -1
  53. package/lib/esm/curve/RegionOpsClassificationSweeps.js +4 -5
  54. package/lib/esm/curve/RegionOpsClassificationSweeps.js.map +1 -1
  55. package/lib/esm/curve/internalContexts/CurveCurveIntersectXY.js +1 -1
  56. package/lib/esm/curve/internalContexts/CurveCurveIntersectXY.js.map +1 -1
  57. package/lib/esm/topology/Graph.d.ts +5 -2
  58. package/lib/esm/topology/Graph.d.ts.map +1 -1
  59. package/lib/esm/topology/Graph.js +5 -2
  60. package/lib/esm/topology/Graph.js.map +1 -1
  61. package/package.json +5 -5
@@ -374,13 +374,23 @@ export class RegionOps {
374
374
  const localToWorld = FrameBuilder.createRightHandedFrame(undefined, xyRegion);
375
375
  return localToWorld ? localToWorld.origin.z : 0;
376
376
  }
377
+ /** Convert overlapping enums, from public to internal. */
378
+ static convertToRegionGroupOpType(op) {
379
+ if (RegionBinaryOpType.Parity === op)
380
+ return RegionGroupOpType.Parity;
381
+ if (RegionBinaryOpType.Intersection === op)
382
+ return RegionGroupOpType.Intersection;
383
+ return RegionGroupOpType.Union;
384
+ }
377
385
  /**
378
386
  * Return areas defined by a boolean operation.
379
387
  * @note For best results, input regions should have correctly oriented loops. See [[sortOuterAndHoleLoopsXY]].
380
388
  * @note A common use case of this method is to split (a region with) overlapping loops into a `UnionRegion` with
381
389
  * adjacent `Loop`s: `regionOut = RegionOps.regionBooleanXY(regionIn, undefined, RegionBinaryOpType.Union)`.
382
- * @note The Union operation does not currently attempt to return a region with minimal loops. This may change with
383
- * future development.
390
+ * Such a union can be further simplified by passing the option `RegionBooleanXYOptions.simplifyUnion = true`.
391
+ * @note By default, this method does not discover holes implicitly formed by input loops. For example, the union of
392
+ * the four trapezoids that comprise a picture frame is not returned as a [[ParityRegion]] unless the option
393
+ * `simplifyUnion = true`.
384
394
  * @param loopsA first set of loops (treated as a union)
385
395
  * @param loopsB second set of loops (treated as a union)
386
396
  * @param operation indicates Union, Intersection, Parity, AMinusB, or BMinusA
@@ -388,48 +398,70 @@ export class RegionOps {
388
398
  * @returns a region resulting from merging input loops and the boolean operation.
389
399
  */
390
400
  static regionBooleanXY(loopsA, loopsB, operation, mergeToleranceOrOptions = Geometry.smallMetricDistance) {
391
- let mergeTolerance;
392
- let simplifyUnion;
393
- if (typeof mergeToleranceOrOptions === "number") {
394
- mergeTolerance = mergeToleranceOrOptions;
395
- simplifyUnion = false;
396
- }
397
- else {
398
- mergeTolerance = mergeToleranceOrOptions.mergeTolerance ?? Geometry.smallMetricDistance;
399
- simplifyUnion = mergeToleranceOrOptions.simplifyUnion ?? false;
400
- }
401
- let result = UnionRegion.create();
402
- const context = RegionBooleanContext.create(RegionGroupOpType.Union, RegionGroupOpType.Union);
401
+ const optionIsNumber = typeof mergeToleranceOrOptions === "number";
402
+ const mergeTolerance = optionIsNumber ? mergeToleranceOrOptions : mergeToleranceOrOptions.mergeTolerance ?? Geometry.smallMetricDistance;
403
+ const simplifyUnion = optionIsNumber ? false : mergeToleranceOrOptions.simplifyUnion ?? false;
404
+ const operationGroupA = optionIsNumber ? RegionGroupOpType.Union : RegionOps.convertToRegionGroupOpType(mergeToleranceOrOptions.operationGroupA);
405
+ const operationGroupB = optionIsNumber ? RegionGroupOpType.Union : RegionOps.convertToRegionGroupOpType(mergeToleranceOrOptions.operationGroupB);
406
+ let result;
407
+ const context = RegionBooleanContext.create(operationGroupA, operationGroupB);
403
408
  context.addMembers(loopsA, loopsB);
404
409
  context.annotateAndMergeCurvesInGraph(mergeTolerance);
405
410
  const bridgeMask = HalfEdgeMask.BRIDGE_EDGE;
406
411
  const visitMask = context.graph.grabMask(false);
412
+ // use our own outMask: we cannot use EXTERIOR as sweep clears it before processing each component
413
+ const outMask = simplifyUnion ? context.graph.grabMask(false) : HalfEdgeMask.NULL_MASK;
414
+ const outSeeds = simplifyUnion ? [] : undefined;
415
+ context.graph.clearMask(visitMask | outMask);
407
416
  const range = context.groupA.range().union(context.groupB.range());
408
417
  const areaTol = this.computeXYAreaTolerance(range, mergeTolerance);
409
418
  const z = RegionOps.getZCoordinate(operation === RegionBinaryOpType.BMinusA ? loopsB : loopsA);
410
419
  const options = { compress: true, closureTol: mergeTolerance, bridgeMask, visitMask, z };
420
+ let numFacesIn = 0;
411
421
  context.runClassificationSweep(operation, (_graph, face, faceType, area) => {
412
422
  // ignore danglers and null faces, but not 2-edge "banana" faces with nonzero area
413
423
  if (face.countEdgesAroundFace() < 2)
414
424
  return;
415
425
  if (Math.abs(area) < areaTol)
416
426
  return;
417
- if (faceType === 1) {
427
+ if (simplifyUnion) { // process the outer/negative faces later
428
+ if (faceType === 1) {
429
+ numFacesIn++;
430
+ }
431
+ else {
432
+ face.setMaskAroundFace(outMask);
433
+ outSeeds?.push(face);
434
+ }
435
+ }
436
+ else if (faceType === 1) { // collect the inner faces now
418
437
  const loopOrParityRegion = PlanarSubdivision.createLoopOrParityRegionInFace(face, options);
419
- if (loopOrParityRegion)
420
- result.tryAddChild(loopOrParityRegion);
438
+ if (loopOrParityRegion) {
439
+ if (!result)
440
+ result = UnionRegion.create();
441
+ if (result.tryAddChild(loopOrParityRegion))
442
+ numFacesIn++;
443
+ }
421
444
  }
422
445
  });
423
- context.graph.dropMask(visitMask);
424
- if (simplifyUnion && operation === RegionBinaryOpType.Union) {
425
- const signedLoops = RegionOps.constructAllXYRegionLoops(result);
426
- if (signedLoops) {
427
- const outerLoops = signedLoops.map((component) => component.negativeAreaLoops).flat();
428
- if (outerLoops.length > 0)
429
- result = UnionRegion.create(...outerLoops);
446
+ // the outer faces are marked; now we collect their boundary super face loops and sort them
447
+ if (simplifyUnion && numFacesIn > 0 && outSeeds && outSeeds.length > 0) {
448
+ context.graph.clearMask(visitMask);
449
+ const outLoops = [];
450
+ for (const outSeed of outSeeds) {
451
+ const superFace = [];
452
+ const skipEdge = (he) => he.isMaskSet(visitMask) || !he.isMaskSet(outMask) || he.edgeMate.isMaskSet(outMask);
453
+ const announceEdge = (he) => superFace.push(he);
454
+ if (outSeed.announceEdgesInSuperFace(skipEdge, announceEdge)) {
455
+ superFace.forEach((he) => he.setMask(visitMask));
456
+ const outLoop = PlanarSubdivision.createLoopInFace(superFace, options);
457
+ if (outLoop)
458
+ outLoops.push(outLoop);
459
+ }
430
460
  }
461
+ result = this.sortOuterAndHoleLoopsXY(outLoops);
431
462
  }
432
- return this.simplifyRegion(result);
463
+ context.graph.dropMask(visitMask | outMask);
464
+ return result ? this.simplifyRegion(result) : undefined;
433
465
  }
434
466
  /**
435
467
  * Return a polyface whose facets are a boolean operation between the input regions.
@@ -746,86 +778,216 @@ export class RegionOps {
746
778
  return SortablePolygon.sortAsAnyRegion(loopAndArea);
747
779
  }
748
780
  /**
749
- * Simplify the graph by removing bridge edges that do not serve to connect inner and outer loops.
750
- * * If edgeTags are `CurveLocationDetail`s, e.g., as set by `PlanarSubdivision.assembleHalfEdgeGraph`, attempt to heal edges split by removed bridge edges.
781
+ * Compare two edges' geometry from CurveLocationDetail edge tags.
782
+ * @returns an object with following properties:
783
+ * * sameCurves: whether the edges' curve definitions are equivalent.
784
+ * * reversedCurves: whether the edges' curve definitions are equivalent after reversing one of them.
785
+ * * senseEdge0/1: whether the topological edge is oriented in the same direction as the curve geometry, or the opposite direction.
786
+ */
787
+ static getEdgeGeometry(edge0, edge1) {
788
+ let sameCurves = false;
789
+ let reversedCurves = false;
790
+ let senseEdge0 = false;
791
+ let senseEdge1 = false;
792
+ if (edge0.edgeTag instanceof CurveLocationDetail && edge0.edgeTag.curve !== undefined && edge0.edgeTag.isInterval() && edge0.sortData &&
793
+ edge1.edgeTag instanceof CurveLocationDetail && edge1.edgeTag.curve !== undefined && edge1.edgeTag.isInterval() && edge1.sortData) {
794
+ sameCurves = edge0.edgeTag.curve === edge1.edgeTag.curve || edge0.edgeTag.curve.isAlmostEqual(edge1.edgeTag.curve);
795
+ reversedCurves = !sameCurves && edge0.edgeTag.curve.isAlmostEqual(edge1.edgeTag.curve.clone().reverse());
796
+ senseEdge0 = edge0.sortData > 0;
797
+ senseEdge1 = edge1.sortData > 0;
798
+ }
799
+ return { sameCurves, reversedCurves, senseEdge0, senseEdge1 };
800
+ }
801
+ /**
802
+ * Compare two edges' orientations. They do not have to be adjacent.
803
+ * * Requires CurveLocationDetail edge tags.
804
+ * @returns an object with following properties:
805
+ * * diametric: whether edges have same/reversed curves and are oriented diametrically opposite each other.
806
+ * * reversedCurves: whether the underlying curve definitions are reversed.
807
+ * * senseEdge0/1: whether the topological edge is oriented in the same direction as the curve geometry, or the opposite direction.
808
+ */
809
+ static areEdgesDiametric(edge0, edge1) {
810
+ let diametric = false;
811
+ let reversedCurves = false;
812
+ let senseEdge0 = false;
813
+ let senseEdge1 = false;
814
+ if (edge0 !== edge1) {
815
+ const data = RegionOps.getEdgeGeometry(edge0, edge1);
816
+ reversedCurves = data.reversedCurves;
817
+ senseEdge0 = data.senseEdge0;
818
+ senseEdge1 = data.senseEdge1;
819
+ diametric = (data.sameCurves && (senseEdge0 !== senseEdge1)) || (reversedCurves && (senseEdge0 === senseEdge1));
820
+ }
821
+ return { diametric, reversedCurves, senseEdge0, senseEdge1 };
822
+ }
823
+ /**
824
+ * Determine if the edges at a vertex were created by splitting an edge or null face.
825
+ * * Requires CurveLocationDetail edge tags.
826
+ * @param he an edge created by the split operation.
827
+ * @param vs another edge at the same vertex. If undefined, use `vs = he.vertexSuccessor` and check for a
828
+ * degree-2 vertex (e.g., split edge); otherwise, ignore the degree check (e.g., inputs are a null face pair).
829
+ */
830
+ static isHealableEdgeOrNullFace(he, vs) {
831
+ if (vs) {
832
+ if (!he.findAroundVertex(vs))
833
+ return false;
834
+ }
835
+ else {
836
+ vs = he.vertexSuccessor;
837
+ if (vs.vertexSuccessor !== he)
838
+ return false; // not a split edge
839
+ }
840
+ const directionData = RegionOps.areEdgesDiametric(he, vs);
841
+ if (!directionData.diametric)
842
+ return false;
843
+ const heVertexFraction = directionData.senseEdge0 ? he.edgeTag.fraction : he.edgeTag.fraction1;
844
+ let vsVertexFraction = directionData.senseEdge1 ? vs.edgeTag.fraction : vs.edgeTag.fraction1;
845
+ if (directionData.reversedCurves)
846
+ vsVertexFraction = 1.0 - vsVertexFraction;
847
+ return Geometry.isSmallRelative(heVertexFraction - vsVertexFraction); // unrelated vertex fractions are not healable
848
+ }
849
+ /**
850
+ * Determine if the vertex loop resulted from splitting a null face into two null faces (and thus can be healed).
851
+ * * Requires CurveLocationDetail edge tags.
852
+ * * If there are multiple null faces at the vertex (a null face bundle), they must be adjacent and positioned in
853
+ * diametrically opposite pairs, each of which resulted from splitting a null face. This implies there are exactly
854
+ * two non-null faces opposite each other in the vertex loop.
855
+ * @returns an object with properties:
856
+ * * isHealable: whether the input is a healable vertex loop in a null face bundle
857
+ * * nullFacePairs: if `isHealable` is true, an array of the matching null face pairs around the vertex.
858
+ */
859
+ static isHealableNullFaceBundle(vertex) {
860
+ const nonNullFaceIndex = [];
861
+ const vertexLoop = [];
862
+ // lambda: null face detector, with curve check to rule out banana faces
863
+ const isNullFace = (face) => {
864
+ const fSucc = face.faceSuccessor;
865
+ return (fSucc.faceSuccessor === face) && RegionOps.areEdgesDiametric(face, fSucc).diametric;
866
+ };
867
+ vertex.announceEdgesAroundVertex((e) => {
868
+ if (!isNullFace(e))
869
+ nonNullFaceIndex.push(vertexLoop.length);
870
+ vertexLoop.push(e);
871
+ });
872
+ const degree = vertexLoop.length;
873
+ if (degree < 4 || nonNullFaceIndex.length !== 2 || Geometry.modulo(degree, 2) > 0)
874
+ return { isHealable: false };
875
+ const iNonNull0 = nonNullFaceIndex[0];
876
+ if (nonNullFaceIndex[1] - iNonNull0 !== degree / 2)
877
+ return { isHealable: false };
878
+ const nullFacePairs = [];
879
+ for (let i = 1; i < degree / 2; i++) {
880
+ const iNullFace = Geometry.modulo(iNonNull0 + i, degree);
881
+ const iMatchingNullFace = Geometry.modulo(2 * iNonNull0 - iNullFace, degree);
882
+ if (!RegionOps.isHealableEdgeOrNullFace(vertexLoop[iNullFace], vertexLoop[iMatchingNullFace]))
883
+ return { isHealable: false };
884
+ nullFacePairs.push([vertexLoop[iNullFace], vertexLoop[iMatchingNullFace]]);
885
+ }
886
+ return { isHealable: true, nullFacePairs };
887
+ }
888
+ /** Heal a split edge by removing the redundant vertex loop and extending the surviving curve details. */
889
+ static healSplitEdgeAndCurve(v) {
890
+ const vs = v.vertexSuccessor;
891
+ const fPred = HalfEdge.healEdge(v, false);
892
+ if (!fPred)
893
+ return;
894
+ // lambda to extend the surviving node's curve interval. Inputs are on the same side of the healed edge.
895
+ const extendDetailInterval = (doomed, survivor) => {
896
+ const doomedData = RegionOps.getEdgeGeometry(doomed, survivor);
897
+ assert(doomedData.sameCurves || doomedData.reversedCurves, "expect related curves");
898
+ let newEndFraction = doomedData.senseEdge0 ? doomed.edgeTag.fraction1 : doomed.edgeTag.fraction;
899
+ if (doomedData.reversedCurves)
900
+ newEndFraction = 1.0 - newEndFraction;
901
+ const newEndPoint = doomedData.senseEdge0 ? doomed.edgeTag.point1 : doomed.edgeTag.point;
902
+ if (doomedData.senseEdge1)
903
+ survivor.edgeTag.captureFraction1Point1(newEndFraction, newEndPoint);
904
+ else
905
+ survivor.edgeTag.captureFractionPoint(newEndFraction, newEndPoint);
906
+ };
907
+ extendDetailInterval(v, fPred);
908
+ extendDetailInterval(vs, fPred.edgeMate);
909
+ }
910
+ /**
911
+ * Disconnect extraneous bridge edges, which remain in the graph as isolated edges.
751
912
  * @param graph half edges to process.
752
- * @param isBridgeEdge optional function to identify a bridge edge. Default looks for `HalfEdgeMask.BRIDGE_EDGE`.
753
- * @param faceToArea optional function to compute face area. Default is `HalfEdgeGraphSearch.signedFaceArea`.
754
- * @returns the number of extraneous bridge edges removed from the graph.
755
- * @internal
913
+ * @param isBridgeEdge optional callback to identify a bridge edge.
914
+ * @param faceToArea optional callback to compute face area.
915
+ * @returns array of surviving vertices formerly adjacent to the isolated bridge edges (e.g., for the caller to heal).
756
916
  */
757
- static removeExtraneousBridgeEdges(graph, isBridgeEdge, faceToArea) {
917
+ static isolateExtraneousBridgeEdges(graph, isBridgeEdge, faceToArea) {
758
918
  const toHeal = [];
759
919
  const interiorBridges = [];
760
- if (!faceToArea)
761
- faceToArea = (node) => HalfEdgeGraphSearch.signedFaceArea(node);
762
- if (!isBridgeEdge)
763
- isBridgeEdge = (node) => node.isMaskSet(HalfEdgeMask.BRIDGE_EDGE);
920
+ const isolateBridgeEdge = (edge) => {
921
+ if (!edge.isDangling)
922
+ toHeal.push(edge.vertexSuccessor);
923
+ if (!edge.edgeMate.isDangling)
924
+ toHeal.push(edge.edgeMate.vertexSuccessor);
925
+ edge.isolateEdge();
926
+ };
764
927
  // isolate dangling bridges, bridges separating different faces, and "exterior" bridges in the negative area face
765
928
  graph.announceEdges((_g, node) => {
766
929
  if (isBridgeEdge(node)) {
767
- if (node.isDangling || node.edgeMate.isDangling || !node.findAroundFace(node.edgeMate) || faceToArea(node) < 0.0) {
768
- toHeal.push(node.vertexSuccessor);
769
- toHeal.push(node.edgeMate.vertexSuccessor);
770
- node.isolateEdge();
771
- }
772
- else {
930
+ if (node.isDangling || node.edgeMate.isDangling || !node.findAroundFace(node.edgeMate) || faceToArea(node) < 0.0)
931
+ isolateBridgeEdge(node);
932
+ else
773
933
  interiorBridges.push(node);
774
- }
775
934
  }
776
935
  return true;
777
936
  });
778
- // Relies only on face loop orientation. Doesn't use static HalfEdgeMasks!
779
- const isBoundaryEdge = (node) => {
937
+ // lambda: check if a non-isolated interior bridge edge is extraneous, relying solely on face loop orientation (not HalfEdgeMasks!)
938
+ const isBridgeEdgeExtraneous = (node) => {
939
+ if (node.isIsolatedEdge)
940
+ return false; // already extraneous
780
941
  if (faceToArea(node) < 0.0)
781
942
  return true; // exterior face
782
943
  if (node.findAroundFace(node.edgeMate))
783
944
  return false; // interior washer face
784
945
  return faceToArea(node.edgeMate) < 0.0; // adjacent to exterior face
785
946
  };
786
- // All bridges in the negative area face were isolated, but this may have promoted other bridges to the
787
- // negative area face. Keep isolating these bridge edges until none remain.
947
+ // All bridges in the negative area face were isolated, but this may have caused surviving bridges to become extraneous
948
+ // (e.g., by swelling the negative area face around them). Keep isolating these bridge edges until none remain.
788
949
  let numIsolatedThisPass;
789
950
  do {
790
951
  numIsolatedThisPass = 0;
791
952
  for (const node of interiorBridges) {
792
- if (!node.isIsolatedEdge && isBoundaryEdge(node)) {
793
- toHeal.push(node.vertexSuccessor);
794
- toHeal.push(node.edgeMate.vertexSuccessor);
795
- node.isolateEdge();
953
+ if (isBridgeEdgeExtraneous(node)) {
954
+ isolateBridgeEdge(node);
796
955
  numIsolatedThisPass++;
797
956
  }
798
957
  }
799
958
  } while (numIsolatedThisPass > 0);
800
- // lambda to extend the detail interval on a side of a healed edge
801
- const mergeDetails = (he, newFraction, newPoint) => {
802
- if (he && he.edgeTag instanceof CurveLocationDetail && he.sortData !== undefined && newFraction !== undefined && newPoint) {
803
- if (he.sortData > 0)
804
- he.edgeTag.captureFraction1Point1(newFraction, newPoint);
805
- else
806
- he.edgeTag.captureFractionPoint(newFraction, newPoint);
959
+ return toHeal.filter((edge) => !edge.isIsolatedEdge);
960
+ }
961
+ /**
962
+ * Simplify the graph by removing bridge edges that do not serve to connect inner and outer loops.
963
+ * * If edgeTags are `CurveLocationDetail`s, e.g., as set by `PlanarSubdivision.assembleHalfEdgeGraph`, attempt to heal edges split by removed bridge edges.
964
+ * @param graph half edges to process.
965
+ * @param isBridgeEdge optional callback to identify a bridge edge. Default looks for `HalfEdgeMask.BRIDGE_EDGE`.
966
+ * @param faceToArea optional callback to compute face area. Default is `HalfEdgeGraphSearch.signedFaceArea`.
967
+ * @returns the number of extraneous bridge edges removed from the graph.
968
+ * @internal
969
+ */
970
+ static removeExtraneousBridgeEdges(graph, isBridgeEdge, faceToArea) {
971
+ if (!faceToArea)
972
+ faceToArea = (node) => HalfEdgeGraphSearch.signedFaceArea(node);
973
+ if (!isBridgeEdge)
974
+ isBridgeEdge = (node) => node.isMaskSet(HalfEdgeMask.BRIDGE_EDGE);
975
+ const toHeal = RegionOps.isolateExtraneousBridgeEdges(graph, isBridgeEdge, faceToArea);
976
+ // clean up the isolated bridge edges' original vertex loops, if possible
977
+ for (const vertex of toHeal) {
978
+ if (vertex.isDangling || vertex.isIsolatedEdge)
979
+ continue;
980
+ if (RegionOps.isHealableEdgeOrNullFace(vertex)) {
981
+ RegionOps.healSplitEdgeAndCurve(vertex);
982
+ continue;
807
983
  }
808
- };
809
- // At this point all removable bridges are isolated. Clean up their original vertex loops, if possible.
810
- for (const doomedA of toHeal) {
811
- const doomedB = doomedA.vertexSuccessor;
812
- if ( // are the geometries mergeable?
813
- doomedA !== doomedB &&
814
- doomedA.edgeTag instanceof CurveLocationDetail && doomedA.sortData !== undefined &&
815
- doomedB.edgeTag instanceof CurveLocationDetail && doomedB.sortData !== undefined &&
816
- doomedA.edgeTag.curve === doomedB.edgeTag.curve &&
817
- doomedA.edgeTag.isInterval() && doomedB.edgeTag.isInterval() &&
818
- doomedA.sortData * doomedB.sortData < 0 &&
819
- ((doomedA.sortData > 0 && Geometry.isSmallRelative(doomedA.edgeTag.fraction - doomedB.edgeTag.fraction1)) ||
820
- (doomedA.sortData < 0 && Geometry.isSmallRelative(doomedA.edgeTag.fraction1 - doomedB.edgeTag.fraction)))) {
821
- const survivorA = HalfEdge.healEdge(doomedA, false);
822
- if (survivorA) {
823
- const endFractionA = (doomedA.sortData > 0) ? doomedA.edgeTag.fraction1 : doomedA.edgeTag.fraction;
824
- const endPointA = (doomedA.sortData > 0) ? doomedA.edgeTag.point1 : doomedA.edgeTag.point;
825
- mergeDetails(survivorA, endFractionA, endPointA);
826
- const endFractionB = (doomedB.sortData > 0) ? doomedB.edgeTag.fraction1 : doomedB.edgeTag.fraction;
827
- const endPointB = (doomedB.sortData > 0) ? doomedB.edgeTag.point1 : doomedB.edgeTag.point;
828
- mergeDetails(survivorA.edgeMate, endFractionB, endPointB);
984
+ const bundleData = RegionOps.isHealableNullFaceBundle(vertex);
985
+ if (bundleData.isHealable && bundleData.nullFacePairs) {
986
+ for (const [f0, f1] of bundleData.nullFacePairs)
987
+ HalfEdge.pinch(f0, f1); // merge the null face pair into a single null face (with 2 split edges)
988
+ for (const [e0, e1] of bundleData.nullFacePairs) {
989
+ RegionOps.healSplitEdgeAndCurve(e0);
990
+ RegionOps.healSplitEdgeAndCurve(e1);
829
991
  }
830
992
  }
831
993
  }