diff --git a/core/geometry/src/curve/internalContexts/CurveCurveIntersectXY.ts b/core/geometry/src/curve/internalContexts/CurveCurveIntersectXY.ts index e6c7007e8bc..c2b2c2e03e2 100644 --- a/core/geometry/src/curve/internalContexts/CurveCurveIntersectXY.ts +++ b/core/geometry/src/curve/internalContexts/CurveCurveIntersectXY.ts @@ -40,6 +40,7 @@ import { Loop } from "../Loop"; import { Path } from "../Path"; import { ProxyCurve } from "../ProxyCurve"; import { TransitionSpiral3d } from "../spiral/TransitionSpiral3d"; +import { CurveCurve } from "../CurveCurve"; // cspell:word XYRR /** @@ -1057,7 +1058,9 @@ export class CurveCurveIntersectXY extends RecurseToCurvesGeometryHandler { const spiralApproximation = LineString3d.create(); this._geometryB.emitStrokes(spiralApproximation); const numPreviousResults = this._results.length; - this.computeSegmentLineString(segmentA, this._extendA0, this._extendA1, spiralApproximation, false, false, false); + this._results.push(...CurveCurve.closeApproachProjectedXYPairs(segmentA, spiralApproximation, Geometry.smallMetricDistance)); + if (this._extendA0 || this._extendA1) + this.computeSegmentLineString(segmentA, this._extendA0, this._extendA1, spiralApproximation, false, false, false); const numberOfNewResults = this._results.length - numPreviousResults; this.refineSpiralResultsByNewton(segmentA, this._geometryB, numberOfNewResults); } else if (this._geometryB instanceof CurveCollection) { @@ -1088,7 +1091,9 @@ export class CurveCurveIntersectXY extends RecurseToCurvesGeometryHandler { const spiralApproximation = LineString3d.create(); this._geometryB.emitStrokes(spiralApproximation); const numPreviousResults = this._results.length; - this.computeLineStringLineString(lsA, this._extendA0, this._extendA1, spiralApproximation, false, false, false); + this._results.push(...CurveCurve.closeApproachProjectedXYPairs(lsA, spiralApproximation, Geometry.smallMetricDistance)); + if (this._extendA0 || this._extendA1) + this.computeLineStringLineString(lsA, this._extendA0, this._extendA1, spiralApproximation, false, false, false); const numberOfNewResults = this._results.length - numPreviousResults; this.refineSpiralResultsByNewton(lsA, this._geometryB, numberOfNewResults); } else if (this._geometryB instanceof CurveCollection) { @@ -1119,7 +1124,9 @@ export class CurveCurveIntersectXY extends RecurseToCurvesGeometryHandler { const spiralApproximation = LineString3d.create(); this._geometryB.emitStrokes(spiralApproximation); const numPreviousResults = this._results.length; - this.computeArcLineString(arc0, this._extendA0, this._extendA1, spiralApproximation, false, false, false); + this._results.push(...CurveCurve.closeApproachProjectedXYPairs(arc0, spiralApproximation, Geometry.smallMetricDistance)); + if (this._extendA0 || this._extendA1) + this.computeArcLineString(arc0, this._extendA0, this._extendA1, spiralApproximation, false, false, false); const numberOfNewResults = this._results.length - numPreviousResults; this.refineSpiralResultsByNewton(arc0, this._geometryB, numberOfNewResults); } else if (this._geometryB instanceof CurveCollection) { @@ -1150,7 +1157,9 @@ export class CurveCurveIntersectXY extends RecurseToCurvesGeometryHandler { const spiralApproximation = LineString3d.create(); this._geometryB.emitStrokes(spiralApproximation); const numPreviousResults = this._results.length; - this.dispatchLineStringBSplineCurve(spiralApproximation, false, false, curve, this._extendA0, this._extendA1, true); + this._results.push(...CurveCurve.closeApproachProjectedXYPairs(curve, spiralApproximation, Geometry.smallMetricDistance)); + if (this._extendA0 || this._extendA1) + this.dispatchLineStringBSplineCurve(spiralApproximation, false, false, curve, this._extendA0, this._extendA1, true); const numberOfNewResults = this._results.length - numPreviousResults; this.refineSpiralResultsByNewton(curve, this._geometryB, numberOfNewResults); } else if (this._geometryB instanceof CurveCollection) { @@ -1177,24 +1186,40 @@ export class CurveCurveIntersectXY extends RecurseToCurvesGeometryHandler { for (const detail of resultsToBeRefined) { let spiralFraction = reversed ? detail.detailA.fraction : detail.detailB.fraction; let otherFraction = reversed ? detail.detailB.fraction : detail.detailA.fraction; + const otherExtend0 = reversed ? this._extendB0 : this._extendA0; + const otherExtend1 = reversed ? this._extendB1 : this._extendA1; + if (spiral.fractionToPoint(spiralFraction).isAlmostEqualXY(otherCurve.fractionToPoint(otherFraction))) { // already an accurate intersection + this.recordPointWithLocalFractions(otherFraction, otherCurve, 0, 1, spiralFraction, spiral, 0, 1, reversed); + continue; + } const xyMatchingFunction = new CurveCurveIntersectionXYRRToRRD(spiral, otherCurve); const newtonSearcher = new Newton2dUnboundedWithDerivative(xyMatchingFunction); newtonSearcher.setUV(spiralFraction, otherFraction); if (newtonSearcher.runIterations()) { spiralFraction = newtonSearcher.getU(); otherFraction = newtonSearcher.getV(); - if (this.acceptFraction(false, spiralFraction, false) && this.acceptFraction(false, otherFraction, false)) + if (this.acceptFraction(false, spiralFraction, false) && this.acceptFraction(otherExtend0, otherFraction, otherExtend1)) this.recordPointWithLocalFractions(otherFraction, otherCurve, 0, 1, spiralFraction, spiral, 0, 1, reversed); } } } /** Double dispatch handler for strongly typed spiral curve. */ public override handleTransitionSpiral(spiral: TransitionSpiral3d): any { - if (this._geometryB instanceof CurvePrimitive) { // this also handles CurveChainWithDistanceIndex + if (this._geometryB instanceof CurveChainWithDistanceIndex) { + this.dispatchCurveChainWithDistanceIndex(spiral, this.handleTransitionSpiral.bind(this)); + } else if (this._geometryB instanceof CurvePrimitive) { const spiralApproximation = LineString3d.create(); spiral.emitStrokes(spiralApproximation); + const geomBApproximation = LineString3d.create(); + if (this._geometryB instanceof TransitionSpiral3d) + this._geometryB.emitStrokes(geomBApproximation); const numPreviousResults = this._results.length; - this.handleLineString3d(spiralApproximation); + if (this._geometryB instanceof TransitionSpiral3d) + this._results.push(...CurveCurve.closeApproachProjectedXYPairs(spiralApproximation, geomBApproximation, Geometry.smallMetricDistance)); + else + this._results.push(...CurveCurve.closeApproachProjectedXYPairs(spiralApproximation, this._geometryB, Geometry.smallMetricDistance)); + if (this._extendB0 || this._extendB1) + this.handleLineString3d(spiralApproximation); const numberOfNewResults = this._results.length - numPreviousResults; this.refineSpiralResultsByNewton(this._geometryB, spiral, numberOfNewResults, true); } else if (this._geometryB instanceof CurveCollection) { diff --git a/core/geometry/src/test/curve/CurveCurveIntersectXY.test.ts b/core/geometry/src/test/curve/CurveCurveIntersectXY.test.ts index d671d45bf93..d8e7bf11beb 100644 --- a/core/geometry/src/test/curve/CurveCurveIntersectXY.test.ts +++ b/core/geometry/src/test/curve/CurveCurveIntersectXY.test.ts @@ -2373,6 +2373,7 @@ describe("CurveCurveIntersectXY", () => { ck: Checker, allGeometry: GeometryQuery[], curve0: AnyCurve, curve1: AnyCurve, numExpected: number, dx: number, dy: number, + extend0 = false, extend1 = false, ) { GeometryCoreTestIO.captureCloneGeometry(allGeometry, curve0, dx, dy); GeometryCoreTestIO.captureCloneGeometry(allGeometry, curve1, dx, dy); @@ -2427,9 +2428,9 @@ describe("CurveCurveIntersectXY", () => { } } // test both paths - const intersectionsAB = CurveCurve.intersectionXYPairs(curve0, false, curve1, false); + const intersectionsAB = CurveCurve.intersectionXYPairs(curve0, extend0, curve1, extend1); testSpiralIntersection(intersectionsAB, false); - const intersectionsBA = CurveCurve.intersectionXYPairs(curve1, false, curve0, false); + const intersectionsBA = CurveCurve.intersectionXYPairs(curve1, extend1, curve0, extend0); testSpiralIntersection(intersectionsBA, true); }; @@ -2503,11 +2504,13 @@ describe("CurveCurveIntersectXY", () => { } // curve primitives const lineSegment0 = LineSegment3d.create(Point3d.create(70, 30), Point3d.create(70, -30)); + const lineSegment1 = LineSegment3d.create(Point3d.create(-20, 0), Point3d.create(100, 0)); const lineString0 = LineString3d.create( Point3d.create(20, 20), Point3d.create(40, 20), Point3d.create(70, -40), Point3d.create(90, -40), Point3d.create(70, 20), Point3d.create(130, 20), ); const arc0 = Arc3d.createXY(Point3d.create(50, 10), 30); + const arc1 = Arc3d.createXY(Point3d.create(0, -30), 30); const bspline0 = BSplineCurve3d.createUniformKnots( [ Point3d.create(70, 50, 0), @@ -2520,28 +2523,30 @@ describe("CurveCurveIntersectXY", () => { )!; // curve collection (path-loop), curve chain, and bag of curves const lineString1 = LineString3d.create(Point3d.create(-36.33, 64.45), Point3d.create(8.34, 120.78), Point3d.create(76.58, -7)); - const arc1 = Arc3d.create( + const arc2 = Arc3d.create( Point3d.create(80, 40), Vector3d.create(10, 0), Vector3d.create(0, 50), AngleSweep.createStartEndDegrees(250, 90), ); - const arc2 = Arc3d.create( + const arc3 = Arc3d.create( Point3d.create(70, -40), Vector3d.create(20, 0), Vector3d.create(0, 20), AngleSweep.createStartEndDegrees(0, -180), ); const lineString2 = LineString3d.create(Point3d.create(80, 90), Point3d.create(90, -100), Point3d.create(40, -100)); const lineString3 = LineString3d.create(Point3d.create(50, -40), Point3d.create(0, -40), Point3d.create(0, 0)); - const lineSegment1 = LineSegment3d.create(Point3d.create(40, -100), Point3d.create(40, 30)); - const lineSegment2 = LineSegment3d.create(Point3d.create(40, 30), Point3d.create(70, -70)); - const lineSegment3 = LineSegment3d.create(Point3d.create(60, -50), Point3d.create(90, -40)); - const path0 = Path.create(arc1, lineString2, lineSegment1, lineSegment2, integratedSpirals[1]); - const path1 = Path.create(lineSegment3, arc2, lineString3, integratedSpirals[0]); - const loop = Path.create(lineString1, arc1, lineString2, lineSegment1, lineSegment2, integratedSpirals[1]); + const lineSegment2 = LineSegment3d.create(Point3d.create(40, -100), Point3d.create(40, 30)); + const lineSegment3 = LineSegment3d.create(Point3d.create(40, 30), Point3d.create(70, -70)); + const lineSegment4 = LineSegment3d.create(Point3d.create(60, -50), Point3d.create(90, -40)); + const path0 = Path.create(arc2, lineString2, lineSegment2, lineSegment3, integratedSpirals[1]); + const path1 = Path.create(lineSegment4, arc3, lineString3, integratedSpirals[0]); + const loop = Path.create(lineString1, arc2, lineString2, lineSegment2, lineSegment3, integratedSpirals[1]); const curveChain0 = CurveChainWithDistanceIndex.createCapture(path0); const curveChain1 = CurveChainWithDistanceIndex.createCapture(path1); const bagOfCurves = BagOfCurves.create(path0, arc0, lineString0); - const curves: AnyCurve[] = [ + let curves: AnyCurve[] = [ lineSegment0, + lineSegment1, lineString0, arc0, + arc1, bspline0, // add rotated and non-planar spirals integratedSpirals[1], @@ -2580,35 +2585,35 @@ describe("CurveCurveIntersectXY", () => { curveChain0, bagOfCurves, ]; - const numExpectedIntersections = [ - 1, 3, 2, 1, // curve primitives other than spirals + let numExpectedIntersections = [ + 1, 1, 3, 2, 1, 1, // curve primitives other than spirals 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // rotated and non-planar integrated spirals 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1, 1, 1, 1, 1, 1, 1, 1 // rotated and non-planar direct spirals 5, 6, 5, 10, // path, loop, curve chain, and bag of curves ]; ck.testCoordinate(curves.length, numExpectedIntersections.length, "matching arrays"); // spiral vs all curves - const test = (spiral: TransitionSpiral3d) => { + const test0 = (spiral: TransitionSpiral3d, ddy = 0, extend = false) => { for (let j = 0; j < curves.length; j++) { const curve = curves[j]; const numExpectedIntersection = numExpectedIntersections[j]; - visualizeAndTestSpiralIntersection(ck, allGeometry, spiral, curve, numExpectedIntersection, dx, dy); + visualizeAndTestSpiralIntersection(ck, allGeometry, spiral, curve, numExpectedIntersection, dx, dy, false, extend); dy += 200; } - dy = 0; + dy = ddy; dx += 200; } for (let i = 0; i < integratedSpirals.length; i++) // skip rotated and non-planar integrated spirals if (i % 3 === 0) - test(integratedSpirals[i]); + test0(integratedSpirals[i]); dx += 250; for (let i = 0; i < directSpirals.length; i++) // skip rotated and non-planar direct spirals if (i % 3 === 0) - test(directSpirals[i]); - // curve chain/collection vs curve chain/collection + test0(directSpirals[i]); dx = 0; - dy = 6000; - const numExpected = 12; + dy = 6400; + let numExpected = 12; + // curve chain/collection vs curve chain/collection visualizeAndTestSpiralIntersection(ck, allGeometry, curveChain0, curveChain1, numExpected, dx, dy); dy += 200; visualizeAndTestSpiralIntersection(ck, allGeometry, path0, path1, numExpected, dx, dy); @@ -2616,6 +2621,48 @@ describe("CurveCurveIntersectXY", () => { visualizeAndTestSpiralIntersection(ck, allGeometry, curveChain0, path1, numExpected, dx, dy); dy += 200; visualizeAndTestSpiralIntersection(ck, allGeometry, curveChain1, path0, numExpected, dx, dy); + // tangency at the interior of the spiral + dy += 200; + numExpected = 1; + const test1 = (spiral: TransitionSpiral3d) => { + const ray = spiral.fractionToPointAndDerivative(0.5); + const ls = LineString3d.create( + ray.origin.plusScaled(ray.direction.normalize()!, 50), ray.origin.plusScaled(ray.direction.normalize()!, -50) + ); + visualizeAndTestSpiralIntersection(ck, allGeometry, spiral, ls, numExpected, dx, dy); + dx += 200; + } + for (let i = 0; i < integratedSpirals.length; i++) // skip rotated and non-planar integrated spirals + if (i % 3 === 0) + test1(integratedSpirals[i]); + dx += 250; + for (let i = 0; i < directSpirals.length; i++) // skip rotated and non-planar direct spirals + if (i % 3 === 0) + test1(directSpirals[i]); + // extend curve primitive + dx = 0; + dy += 200; + const lineSegment5 = LineSegment3d.create(Point3d.create(50, 100), Point3d.create(60, 50)); + const arc4 = Arc3d.createXY(Point3d.create(50, 10), 30, AngleSweep.createStartEndDegrees(90, 180)); + const lineString4 = LineString3d.create( + Point3d.create(20, 20), Point3d.create(20, 40), Point3d.create(40, 40), Point3d.create(40, 20), Point3d.create(70, 20), + ); + const lineString5 = LineString3d.create( + Point3d.create(10, 40), Point3d.create(10, 80), Point3d.create(30, 80), + ); + const arc5 = Arc3d.createXY(Point3d.create(30, 60), 20, AngleSweep.createStartEndDegrees(90, -90)); + const lineSegment6 = LineSegment3d.create(Point3d.create(30, 40), Point3d.create(50, 30)); + const path2 = Path.create(lineString5, arc5, lineSegment6); + curves = [lineSegment5, arc4, lineString4, path2]; + numExpectedIntersections = [1, 2, 2, 2]; + ck.testCoordinate(curves.length, numExpectedIntersections.length, "matching arrays"); + for (let i = 0; i < integratedSpirals.length; i++) // skip rotated and non-planar integrated spirals + if (i % 3 === 0) + test0(integratedSpirals[i], 7400, true); + dx += 250; + for (let i = 0; i < directSpirals.length; i++) // skip rotated and non-planar direct spirals + if (i % 3 === 0) + test0(directSpirals[i], 7400, true); GeometryCoreTestIO.saveGeometry(allGeometry, "CurveCurveIntersectXY", "SpiralIntersection"); expect(ck.getNumErrors()).toBe(0);