From 3269edd8c87bc2377c60c7b490b1618437016728 Mon Sep 17 00:00:00 2001 From: Saeed Torabi <114527638+saeeedtorabi@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:10:11 -0400 Subject: [PATCH] Consolidated circular arc construction methods into Arc3d (#7239) Co-authored-by: dassaf4 <68340676+dassaf4@users.noreply.github.com> --- common/api/core-geometry.api.md | 19 ++-- ...i-circularArcMethods_2024-10-04-20-42.json | 10 ++ core/geometry/internaldocs/Arc3d.md | 22 ++++ .../Arc3d/createCircularStartTangentEnd.png | Bin 0 -> 80098 bytes core/geometry/internaldocs/quarticRoots.md | 36 +++++++ core/geometry/src/curve/Arc3d.ts | 98 +++++++++++------- core/geometry/src/curve/CurveFactory.ts | 63 ++++------- .../CurveCurveCloseApproachXY.ts | 4 +- .../internalContexts/CurveCurveIntersectXY.ts | 4 +- .../CurveCurveIntersectXYZ.ts | 2 +- .../EllipticalArcApproximationContext.ts | 25 +++-- core/geometry/src/geometry3d/AngleSweep.ts | 27 ++--- core/geometry/src/geometry3d/PolygonOps.ts | 4 +- core/geometry/src/geometry4d/Map4d.ts | 10 +- core/geometry/src/geometry4d/Matrix4d.ts | 11 +- core/geometry/src/numerics/Polynomials.ts | 44 ++++---- core/geometry/src/polyface/PolyfaceClip.ts | 8 +- core/geometry/src/test/curve/Arc3d.test.ts | 15 ++- .../src/test/numerics/Polynomial.test.ts | 64 ++++++++++++ 19 files changed, 302 insertions(+), 164 deletions(-) create mode 100644 common/changes/@itwin/core-geometry/saeed-torabi-circularArcMethods_2024-10-04-20-42.json create mode 100644 core/geometry/internaldocs/Arc3d.md create mode 100644 core/geometry/internaldocs/figs/Arc3d/createCircularStartTangentEnd.png create mode 100644 core/geometry/internaldocs/quarticRoots.md diff --git a/common/api/core-geometry.api.md b/common/api/core-geometry.api.md index f2d8b7804cbf..25dbcc1f1e8c 100644 --- a/common/api/core-geometry.api.md +++ b/common/api/core-geometry.api.md @@ -156,8 +156,8 @@ export class AngleSweep implements BeJSONFunctions { static fromJSON(json?: AngleSweepProps): AngleSweep; interpolate(fraction: number, other: AngleSweep): AngleSweep; isAlmostEqual(other: AngleSweep): boolean; - isAlmostEqualAllowPeriodShift(other: AngleSweep): boolean; - isAlmostEqualNoPeriodShift(other: AngleSweep): boolean; + isAlmostEqualAllowPeriodShift(other: AngleSweep, radianTol?: number): boolean; + isAlmostEqualNoPeriodShift(other: AngleSweep, radianTol?: number): boolean; isAngleInSweep(angle: Angle): boolean; get isCCW(): boolean; get isEmpty(): boolean; @@ -257,10 +257,11 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions { static createCircularStartEndRadius(start: Point3d, end: Point3d, radius: number, helper: Point3d | Vector3d): Arc3d | undefined; static createCircularStartMiddleEnd(pointA: XYAndZ, pointB: XYAndZ, pointC: XYAndZ, result?: Arc3d): Arc3d | LineString3d; static createCircularStartTangentEnd(start: Point3d, tangentAtStart: Vector3d, end: Point3d, result?: Arc3d): Arc3d | LineSegment3d; + static createCircularStartTangentRadius(start: Point3d, tangentAtStart: Vector3d, radius: number, upVector?: Vector3d, sweep?: Angle | AngleSweep): Arc3d | undefined; static createFilletArc(point0: Point3d, point1: Point3d, point2: Point3d, radius: number): ArcBlendData; static createRefs(center: Point3d, matrix: Matrix3d, sweep: AngleSweep, result?: Arc3d): Arc3d; static createScaledXYColumns(center: Point3d | undefined, matrix: Matrix3d, radius0: number, radius90: number, sweep?: AngleSweep, result?: Arc3d): Arc3d; - static createStartMiddleEnd(point0: XYAndZ, point1: XYAndZ, point2: XYAndZ, sweep?: AngleSweep, result?: Arc3d): Arc3d | undefined; + static createStartMiddleEnd(start: XYAndZ, middle: XYAndZ, end: XYAndZ, sweep?: AngleSweep, result?: Arc3d): Arc3d | undefined; static createUnitCircle(): Arc3d; static createXY(center: Point3d, radius: number, sweep?: AngleSweep): Arc3d; static createXYEllipse(center: Point3d, radiusA: number, radiusB: number, sweep?: AngleSweep): Arc3d; @@ -281,7 +282,7 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions { getFractionToDistanceScale(): number | undefined; // @internal getPlaneAltitudeSineCosinePolynomial(plane: PlaneAltitudeEvaluator, result?: SineCosinePolynomial): SineCosinePolynomial; - isAlmostEqual(otherGeometry: GeometryQuery): boolean; + isAlmostEqual(otherGeometry: GeometryQuery, distanceTol?: number, radianTol?: number): boolean; get isCircular(): boolean; get isExtensibleFractionSpace(): boolean; isInPlane(plane: Plane3dByOriginAndUnitNormal): boolean; @@ -1640,8 +1641,8 @@ export class CurveExtendOptions { export class CurveFactory { static appendToArcInPlace(arcA: Arc3d, arcB: Arc3d, allowReverse?: boolean): boolean; static assembleArcChainOnEllipsoid(ellipsoid: Ellipsoid, pathPoints: GeodesicPathPoint[], fractionForIntermediateNormal?: number): Path; - static createArcPointTangentPoint(pointA: Point3d, tangentA: Vector3d, pointB: Point3d): Arc3d | undefined; - static createArcPointTangentRadius(pointA: Point3d, tangentA: Vector3d, radius: number, upVector?: Vector3d, sweep?: Angle | AngleSweep): Arc3d | undefined; + static createArcPointTangentPoint(start: Point3d, tangentAtStart: Vector3d, end: Point3d): Arc3d | undefined; + static createArcPointTangentRadius(start: Point3d, tangentAtStart: Vector3d, radius: number, upVector?: Vector3d, sweep?: Angle | AngleSweep): Arc3d | undefined; static createFilletsInLineString(points: LineString3d | IndexedXYZCollection | Point3d[], radius: number | number[], allowBackupAlongEdge?: boolean): Path | undefined; static createLineSpiralArcSpiralLine(spiralType: IntegratedSpiralTypeName, pointA: Point3d, pointB: Point3d, pointC: Point3d, lengthA: number, lengthB: number, arcRadius: number): GeometryQuery[] | undefined; static createLineSpiralSpiralLine(spiralType: IntegratedSpiralTypeName, startPoint: Point3d, shoulderPoint: Point3d, targetPoint: Point3d): GeometryQuery[] | undefined; @@ -4611,11 +4612,11 @@ export class PolyfaceBuilder extends NullGeometryHandler { // @public export class PolyfaceClip { static clipPolyface(polyface: Polyface, clipper: ClipPlane | ConvexClipPlaneSet): Polyface | undefined; - static clipPolyfaceClipPlane(polyface: Polyface, clipper: ClipPlane, insideClip?: boolean, buildClosureFaces?: boolean): Polyface; + static clipPolyfaceClipPlane(polyface: Polyface, clipper: ClipPlane, insideClip?: boolean, buildClosureFaces?: boolean): IndexedPolyface; // @internal static clipPolyfaceClipPlaneToBuilders(polyface: Polyface, clipper: PlaneAltitudeEvaluator, destination: ClippedPolyfaceBuilders): void; - static clipPolyfaceClipPlaneWithClosureFace(polyface: Polyface, clipper: ClipPlane, insideClip?: boolean, buildClosureFaces?: boolean): Polyface; - static clipPolyfaceConvexClipPlaneSet(polyface: Polyface, clipper: ConvexClipPlaneSet): Polyface; + static clipPolyfaceClipPlaneWithClosureFace(polyface: Polyface, clipper: ClipPlane, insideClip?: boolean, buildClosureFaces?: boolean): IndexedPolyface; + static clipPolyfaceConvexClipPlaneSet(polyface: Polyface, clipper: ConvexClipPlaneSet): IndexedPolyface; // @internal static clipPolyfaceConvexClipPlaneSetToBuilders(polyface: Polyface, clipper: ConvexClipPlaneSet, destination: ClippedPolyfaceBuilders): void; static clipPolyfaceInsideOutside(polyface: Polyface, clipper: ClipPlane | ConvexClipPlaneSet | UnionOfConvexClipPlaneSets, destination: ClippedPolyfaceBuilders, outputSelect?: number): void; diff --git a/common/changes/@itwin/core-geometry/saeed-torabi-circularArcMethods_2024-10-04-20-42.json b/common/changes/@itwin/core-geometry/saeed-torabi-circularArcMethods_2024-10-04-20-42.json new file mode 100644 index 000000000000..9d7216f28509 --- /dev/null +++ b/common/changes/@itwin/core-geometry/saeed-torabi-circularArcMethods_2024-10-04-20-42.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-geometry", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/core-geometry" +} \ No newline at end of file diff --git a/core/geometry/internaldocs/Arc3d.md b/core/geometry/internaldocs/Arc3d.md new file mode 100644 index 000000000000..fa96deb0fda7 --- /dev/null +++ b/core/geometry/internaldocs/Arc3d.md @@ -0,0 +1,22 @@ +# Create Circular Arc using Start, TangentAtStart, and End + +Below we explain the algorithm used in `Arc3d.createCircularStartTangentEnd` to create a circular arc using start point, tangent at start, and end point. + +We first set up a frame of three perpendicular unit vectors using `Matrix3d.createRigidFromColumns`: +* `frameColX` lies along `tangentAtStart`, +* the circle normal lies along the cross product of `tangentAtStart` and the vector `startToEnd`, and +* `frameColY` lies along the cross product of the normal and `tangentAtStart`. + +We seek a formula for the radius of the circle. From the radius, we can find the circle center by moving a distance of radius from `start` along `frameColY`. From the center and the two input points, we can compute the arc sweep. Then we are done. + +The key to finding the radius is the inscribed right triangle pictured below, with one vertex at `start`, with hypotenuse along a diameter, and with leg along `startToEnd`. We know this is a right triangle from the inscribed angle theorem of classical geometry. + +Suppose `v = startToEnd`, `w = frameColY`, `r` is the radius we seek, and $\theta$ is the right triangle's angle at `start`. Then with + +$$v\cdot w = ||v|| ||w|| \cos\theta = ||v|| \frac{||v||}{2r} = \frac{||v||^2}{2r},$$ + +we have: + +$$\frac{v\cdot v}{2v\cdot w} = \frac{||v||^2}{2\frac{||v||^2}{2r}} = r.$$ + +![>](./figs/Arc3d/createCircularStartTangentEnd.png) \ No newline at end of file diff --git a/core/geometry/internaldocs/figs/Arc3d/createCircularStartTangentEnd.png b/core/geometry/internaldocs/figs/Arc3d/createCircularStartTangentEnd.png new file mode 100644 index 0000000000000000000000000000000000000000..ea49490926a23113207da1e7e30ea37a3eb83e2e GIT binary patch literal 80098 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 z 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 G=l=tA2JY$r literal 0 HcmV?d00001 diff --git a/core/geometry/internaldocs/quarticRoots.md b/core/geometry/internaldocs/quarticRoots.md new file mode 100644 index 000000000000..425c828c4dcd --- /dev/null +++ b/core/geometry/internaldocs/quarticRoots.md @@ -0,0 +1,36 @@ +# Finding Roots of a Quartic Polynomial + +In `AnalyticRoots.appendQuarticRoots` we compute the solutions of the general quartic equation: +$$a_4x^4 + a_3x^3 + a_2x^2 + a_1x + a_0 = 0.\tag{1}$$ + +Without loss of generality, assume $a_4 = 1$. We further simplify by performing the substitution $x = y - a_3/4$ to obtain the depressed quartic: +$$P(x) = x^4 + px^2 + qx + r.$$ + +Note that the solutions of $(1)$ are obtained by subtracting $a_3/4$ from each root of $P$. + +Most classical solutions to $(1)$ derive a so-called _resolvent_ cubic polynomial $R$ from the depressed quartic, and use one of the roots of $R$ to construct a pair of quadratic equations whose roots are the roots of $P$. We will show this, using a resolvent cubic of this form: +$$R(y) = y^3 - \frac{1}{2}py^2 - ry + \frac{1}{2}rp - \frac{1}{8}q^2.$$ + +Let $x$ be a root of $P$. Then, introducing a quantity $y$ to complete the square, we have the following equivalences: + +$$\begin{align} +\notag{}P(x) = 0 &\Leftrightarrow x^4 = -px^2 - qx - r \\ +\notag{}&\Leftrightarrow (x^2 + y)^2 = -px^2 - qx - r + 2yx^2 + y^2 \\ +&\Leftrightarrow (x^2 + y)^2 = (2y - p)x^2 - qx + (y^2 - r)\tag{2} \\ +&\Leftrightarrow (x^2 + y)^2 = \left(\sqrt{2y - p}x - \frac{q}{2\sqrt{2y - p}}\right)^2\tag{3} \\ +&\Leftrightarrow \frac{q^2}{4(2y - p)} = y^2 - r\tag{4} \\ +\notag{}&\Leftrightarrow q^2 = 4(y^2 - r)(2y - p) \\ +\notag{}&\Leftrightarrow 0 = 8 R(y), +\end{align}$$ + +where in $(3)$ we have rewritten the right hand side of $(2)$ as a square (since the left hand side of $(2)$ is a square), and in $(4)$ we have equated the constant terms on the right hand sides of $(2)$ and $(3)$. Thus it is seen that the quantity $y$ is actually a root of the resolvent. We can compute the roots of $R$ with `AnalyticRoots.appendCubicRoots`. + +Lastly, choose a root $z$ of $R$. We need only one, so `AnalyticRoots.mostDistantFromMean` picks one employing a numerical stability criterion. Then from the above equivalences, and taking the square root of both sides of $(3)$, we have two quadratics in $x$: + +$$\begin{align} +\notag{}R(z) = 0 &\Leftrightarrow x^2 + z = \pm\left(\sqrt{2z - p}x - \frac{q}{2\sqrt{2z - p}}\right) \\ +&\Leftrightarrow x^2 \pm\sqrt{2z - p}x + z \mp\sqrt{z^2 - r} = 0,\tag{5} +\end{align}$$ + +where we have used the equality $(4)$ to simplify the constant term. We solve these two quadratics with `AnalyticRoots.appendQuadraticRoots`, yielding 0-4 values of $x$. By the above equivalences, each of these is a root of $P$. + diff --git a/core/geometry/src/curve/Arc3d.ts b/core/geometry/src/curve/Arc3d.ts index 671a296a4175..d07ca6322568 100644 --- a/core/geometry/src/curve/Arc3d.ts +++ b/core/geometry/src/curve/Arc3d.ts @@ -384,21 +384,21 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions { } /** * Create an elliptical arc from three points on the ellipse: two points on an axis and one in between. - * @param point0 start of arc, on an axis - * @param point1 point on arc somewhere between `point0` and `point2` - * @param point2 point on arc directly opposite `point0` - * @param sweep angular sweep, measured from `point0` in the direction of `point1`. - * For a half-ellipse from `point0` to `point2` passing through `point1`, pass `AngleSweep.createStartEndDegrees(0,180)`. + * @param start start of arc, on an axis + * @param middle point on arc somewhere between `start` and `end` + * @param end point on arc directly opposite `start` + * @param sweep angular sweep, measured from `start` in the direction of `middle`. + * For a half-ellipse from `start` to `end` passing through `middle`, pass `AngleSweep.createStartEndDegrees(0,180)`. * Default value is full sweep to create the entire ellipse. * @param result optional preallocated result * @returns elliptical arc, or undefined if construction impossible. */ public static createStartMiddleEnd( - point0: XYAndZ, point1: XYAndZ, point2: XYAndZ, sweep?: AngleSweep, result?: Arc3d, + start: XYAndZ, middle: XYAndZ, end: XYAndZ, sweep?: AngleSweep, result?: Arc3d, ): Arc3d | undefined { - const center = Point3d.createAdd2Scaled(point0, 0.5, point2, 0.5); - const vector0 = Vector3d.createStartEnd(center, point0); - const vector1 = Vector3d.createStartEnd(center, point1); + const center = Point3d.createAdd2Scaled(start, 0.5, end, 0.5); + const vector0 = Vector3d.createStartEnd(center, start); + const vector1 = Vector3d.createStartEnd(center, middle); const v0DotV1 = vector0.dotProduct(vector1); const v0Len2 = vector0.magnitudeSquared(); if (Math.abs(v0DotV1) >= v0Len2) @@ -406,7 +406,7 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions { const normal = vector0.crossProduct(vector1); const vector90 = normal.unitCrossProductWithDefault(vector0, 0, 0, 0); const v1DotV90 = vector1.dotProduct(vector90); - // Solve the standard ellipse equation for the unknown axis length, given local coords of point1 (v0.v1/||v0||, v90.v1) + // solve the standard ellipse equation for the unknown axis length, given local coords of middle (v0.v1/||v0||, v90.v1) const v90Len = Geometry.safeDivideFraction(v0Len2 * v1DotV90, Math.sqrt(v0Len2 * v0Len2 - v0DotV1 * v0DotV1), 0); if (Geometry.isSmallMetricDistanceSquared(v90Len)) return undefined; @@ -415,37 +415,59 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions { } /** * Create a circular arc defined by start point, tangent at start point, and end point. - * If tangent is parallel to line segment from start to end, return the line segment. + * * The circular arc is swept from `start` to `end` in the direction of `tangentAtStart`. + * * If `tangentAtStart` is parallel to the line segment from `start` to `end`, return the line segment. */ public static createCircularStartTangentEnd( start: Point3d, tangentAtStart: Vector3d, end: Point3d, result?: Arc3d, ): Arc3d | LineSegment3d { - // To find the circle passing through start and end with tangentAtStart at start: - // - find line 1: the perpendicular bisector of the line from start to end. - // - find line 2: the perpendicular to the tangentAtStart. - // - intersection of the two lines would be the circle center. - const vector = Vector3d.createStartEnd(start, end); - const normal = tangentAtStart.crossProduct(vector).normalize(); - if (normal) { - const vectorPerp = normal.crossProduct(vector); - const tangentPerp = normal.crossProduct(tangentAtStart); - const midPoint = start.plusScaled(vector, 0.5); - - const lineSeg1 = LineSegment3d.create(start, start.plusScaled(tangentPerp, 1)); - const lineSeg2 = LineSegment3d.create(midPoint, midPoint.plusScaled(vectorPerp, 1)); - const intersection = LineSegment3d.closestApproach(lineSeg1, true, lineSeg2, true); - - if (intersection) { - const center = intersection.detailA.point; - const vector0 = Vector3d.createStartEnd(center, start); - const vector90 = normal.crossProduct(vector0); - const endVector = Vector3d.createStartEnd(center, end); - const sweep = AngleSweep.create(vector0.signedAngleTo(endVector, normal)); + // see itwinjs-core\core\geometry\internaldocs\Arc3d.md to clarify below algorithm + const startToEnd = Vector3d.createStartEnd(start, end); + const frame = Matrix3d.createRigidFromColumns(tangentAtStart, startToEnd, AxisOrder.XYZ); + if (frame !== undefined) { + const vv = startToEnd.dotProduct(startToEnd); + const vw = frame.dotColumnY(startToEnd); + const radius = Geometry.conditionalDivideCoordinate(vv, 2 * vw); + if (radius !== undefined) { + const vector0 = frame.columnY(); + vector0.scaleInPlace(-radius); // center to start + const vector90 = frame.columnX(); + vector90.scaleInPlace(radius); + const centerToEnd = vector0.plus(startToEnd); + const sweepAngle = vector0.angleTo(centerToEnd); + let sweepRadians = sweepAngle.radians; // always positive and less than PI + if (tangentAtStart.dotProduct(centerToEnd) < 0.0) // sweepRadians is the wrong way + sweepRadians = 2.0 * Math.PI - sweepRadians; + const center = start.plusScaled(vector0, -1.0); + const sweep = AngleSweep.createStartEndRadians(0.0, sweepRadians); return Arc3d.create(center, vector0, vector90, sweep, result); } } return LineSegment3d.create(start, end); } + /** + * Create a circular arc from start point, tangent at start, radius, optional plane normal, arc sweep. + * * The vector from start point to center is in the direction of upVector crossed with tangentA. + * @param start start point. + * @param tangentAtStart vector in tangent direction at the start. + * @param radius signed radius. + * @param upVector optional out-of-plane vector. Defaults to positive Z. + * @param sweep angular range. If single `Angle` is given, start angle is at 0 degrees (the start point). + */ + public static createCircularStartTangentRadius( + start: Point3d, tangentAtStart: Vector3d, radius: number, upVector?: Vector3d, sweep?: Angle | AngleSweep, + ): Arc3d | undefined { + if (upVector === undefined) + upVector = Vector3d.unitZ(); + const vector0 = upVector.unitCrossProduct(tangentAtStart); + if (vector0 === undefined) + return undefined; + const center = start.plusScaled(vector0, radius); + // reverse the A-to-center vector and bring it up to scale + vector0.scaleInPlace(-radius); + const vector90 = tangentAtStart.scaleToLength(Math.abs(radius))!; // cannot fail; prior unitCrossProduct would have failed first + return Arc3d.create(center, vector0, vector90, AngleSweep.create(sweep)); + } /** * Create a circular arc defined by start and end points and radius. * @param start start point of the arc @@ -910,7 +932,7 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions { return this.isCircular ? this._matrix.columnXMagnitude() : undefined; } - /** Return the larger of the two defining vectors. */ + /** Return the larger length of the two defining vectors. */ public maxVectorLength(): number { return Math.max(this._matrix.columnXMagnitude(), this._matrix.columnYMagnitude()); } @@ -1138,12 +1160,12 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions { }; } /** Test if this arc is almost equal to another GeometryQuery object */ - public override isAlmostEqual(otherGeometry: GeometryQuery): boolean { + public override isAlmostEqual(otherGeometry: GeometryQuery, distanceTol: number = Geometry.smallMetricDistance, radianTol: number = Geometry.smallAngleRadians): boolean { if (otherGeometry instanceof Arc3d) { const other = otherGeometry; - return this._center.isAlmostEqual(other._center) - && this._matrix.isAlmostEqual(other._matrix) - && this._sweep.isAlmostEqualAllowPeriodShift(other._sweep); + return this._center.isAlmostEqual(other._center, distanceTol) + && this._matrix.isAlmostEqual(other._matrix, distanceTol) + && this._sweep.isAlmostEqualAllowPeriodShift(other._sweep, radianTol); } return false; } @@ -1257,7 +1279,7 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions { * * `point` is the `point1` input. * * both fractions are zero * * `arc` is undefined. - * @param point0 first point of path. (the point before the point of inflection) + * @param point0 first point of path (the point before the point of inflection) * @param point1 second point of path (the point of inflection) * @param point2 third point of path (the point after the point of inflection) * @param radius arc radius diff --git a/core/geometry/src/curve/CurveFactory.ts b/core/geometry/src/curve/CurveFactory.ts index 3a3fe29fc942..e789208ced16 100644 --- a/core/geometry/src/curve/CurveFactory.ts +++ b/core/geometry/src/curve/CurveFactory.ts @@ -103,37 +103,18 @@ export class CurveFactory { path.tryAddChild(LineSegment3d.create(pointA.interpolate(fraction0, pointB), pointA.interpolate(fraction1, pointB))); } } - /** - * Create a circular arc from start point, tangent at start, and another point (endpoint) on the arc. - * @param pointA - * @param tangentA - * @param pointB + * Create a circular arc defined by start point, tangent at start point, and end point. + * * The circular arc is swept from start to end toward direction of the `tangentAtStart`. + * * If tangent is parallel to line segment from start to end, return `undefined`. */ - public static createArcPointTangentPoint(pointA: Point3d, tangentA: Vector3d, pointB: Point3d): Arc3d | undefined { - const vectorV = Vector3d.createStartEnd(pointA, pointB); - const frame = Matrix3d.createRigidFromColumns(tangentA, vectorV, AxisOrder.XYZ); - if (frame !== undefined) { - const vv = vectorV.dotProduct(vectorV); - const vw = frame.dotColumnY(vectorV); - const alpha = Geometry.conditionalDivideCoordinate(vv, 2 * vw); - if (alpha !== undefined) { - const vector0 = frame.columnY(); - vector0.scaleInPlace(-alpha); - const vector90 = frame.columnX(); - vector90.scaleInPlace(alpha); - const centerToEnd = vector0.plus(vectorV); - const sweepAngle = vector0.angleTo(centerToEnd); - let sweepRadians = sweepAngle.radians; // That's always positive and less than PI. - if (tangentA.dotProduct(centerToEnd) < 0.0) // ah, sweepRadians is the wrong way - sweepRadians = 2.0 * Math.PI - sweepRadians; - const center = pointA.plusScaled(vector0, -1.0); - return Arc3d.create(center, vector0, vector90, AngleSweep.createStartEndRadians(0.0, sweepRadians)); - } - } - return undefined; + public static createArcPointTangentPoint(start: Point3d, tangentAtStart: Vector3d, end: Point3d): Arc3d | undefined { + const ret = Arc3d.createCircularStartTangentEnd(start, tangentAtStart, end); + if (ret instanceof Arc3d) + return ret; + else + return undefined; } - /** * Construct a sequence of alternating lines and arcs with the arcs creating tangent transition between consecutive edges. * * If the radius parameter is a number, that radius is used throughout. @@ -466,26 +447,18 @@ export class CurveFactory { } /** - * Create a circular arc from start point, tangent at start, radius, optional plane normal, arc sweep + * Create a circular arc from start point, tangent at start, radius, optional plane normal, arc sweep. * * The vector from start point to center is in the direction of upVector crossed with tangentA. - * @param pointA start point - * @param tangentA vector in tangent direction at the start + * @param start start point. + * @param tangentAtStart vector in tangent direction at the start. * @param radius signed radius. - * @param upVector optional out-of-plane vector. Defaults to positive Z - * @param sweep angular range. If single `Angle` is given, start angle is at 0 degrees (the start point). - * + * @param upVector optional out-of-plane vector. Defaults to positive Z. + * @param sweep angular range. If single `Angle` is given, start angle is at 0 degrees (the start point). */ - public static createArcPointTangentRadius(pointA: Point3d, tangentA: Vector3d, radius: number, upVector?: Vector3d, sweep?: Angle | AngleSweep): Arc3d | undefined { - if (upVector === undefined) - upVector = Vector3d.unitZ(); - const vector0 = upVector.unitCrossProduct(tangentA); - if (vector0 === undefined) - return undefined; - const center = pointA.plusScaled(vector0, radius); - // reverse the A-to-center vector and bring it up to scale ... - vector0.scaleInPlace(-radius); - const vector90 = tangentA.scaleToLength(Math.abs(radius))!; // (Cannot fail -- prior unitCrossProduct would have failed first) - return Arc3d.create(center, vector0, vector90, AngleSweep.create(sweep)); + public static createArcPointTangentRadius( + start: Point3d, tangentAtStart: Vector3d, radius: number, upVector?: Vector3d, sweep?: Angle | AngleSweep, + ): Arc3d | undefined { + return Arc3d.createCircularStartTangentRadius(start, tangentAtStart, radius, upVector, sweep); } /** diff --git a/core/geometry/src/curve/internalContexts/CurveCurveCloseApproachXY.ts b/core/geometry/src/curve/internalContexts/CurveCurveCloseApproachXY.ts index 172d0234fca9..fb65db5426dc 100644 --- a/core/geometry/src/curve/internalContexts/CurveCurveCloseApproachXY.ts +++ b/core/geometry/src/curve/internalContexts/CurveCurveCloseApproachXY.ts @@ -436,7 +436,7 @@ export class CurveCurveCloseApproachXY extends RecurseToCurvesGeometryHandler { */ private getPointCurveClosestApproachXYNewton(curveP: CurvePrimitive, pointQ: Point3d): CurveLocationDetail | undefined { if (!(curveP instanceof Arc3d) && !(curveP instanceof LineSegment3d)) { - assert(!"getPointCurveClosestApproachXYNewton only supports Arc3d and LineSegment"); + assert(false, "getPointCurveClosestApproachXYNewton only supports Arc3d and LineSegment"); return undefined; } const seeds = [0.2, 0.4, 0.6, 0.8]; // HEURISTIC: arcs have up to 4 perpendiculars; lines have only 1 @@ -757,7 +757,7 @@ export class CurveCurveCloseApproachXY extends RecurseToCurvesGeometryHandler { if (!this._geometryB || !(this._geometryB instanceof CurveChainWithDistanceIndex)) return; if (geomA instanceof CurveChainWithDistanceIndex) { - assert(!"call handleCurveChainWithDistanceIndex(geomA) instead"); + assert(false, "call handleCurveChainWithDistanceIndex(geomA) instead"); return; } const index0 = this._results.length; diff --git a/core/geometry/src/curve/internalContexts/CurveCurveIntersectXY.ts b/core/geometry/src/curve/internalContexts/CurveCurveIntersectXY.ts index 966da18972ea..8cfd4f65d3c9 100644 --- a/core/geometry/src/curve/internalContexts/CurveCurveIntersectXY.ts +++ b/core/geometry/src/curve/internalContexts/CurveCurveIntersectXY.ts @@ -688,7 +688,7 @@ export class CurveCurveIntersectXY extends RecurseToCurvesGeometryHandler { } const bcurveAFraction = bezierA.fractionToParentFraction(bezierAFraction); const bcurveBFraction = bezierB.fractionToParentFraction(bezierBFraction); - if (!"verify results") { + if (false) { // verify results const xyzA0 = bezierA.fractionToPoint(bezierAFraction); const xyzA1 = bcurveA.fractionToPoint(bcurveAFraction); const xyzB0 = bezierB.fractionToPoint(bezierBFraction); @@ -966,7 +966,7 @@ export class CurveCurveIntersectXY extends RecurseToCurvesGeometryHandler { if (!this._geometryB || !(this._geometryB instanceof CurveChainWithDistanceIndex)) return; if (geomA instanceof CurveChainWithDistanceIndex) { - assert(!"call handleCurveChainWithDistanceIndex(geomA) instead"); + assert(false, "call handleCurveChainWithDistanceIndex(geomA) instead"); return; } const index0 = this._results.length; diff --git a/core/geometry/src/curve/internalContexts/CurveCurveIntersectXYZ.ts b/core/geometry/src/curve/internalContexts/CurveCurveIntersectXYZ.ts index 4a74191aaa19..33a0881c53fe 100644 --- a/core/geometry/src/curve/internalContexts/CurveCurveIntersectXYZ.ts +++ b/core/geometry/src/curve/internalContexts/CurveCurveIntersectXYZ.ts @@ -796,7 +796,7 @@ export class CurveCurveIntersectXYZ extends RecurseToCurvesGeometryHandler { if (!this._geometryB || !(this._geometryB instanceof CurveChainWithDistanceIndex)) return; if (geomA instanceof CurveChainWithDistanceIndex) { - assert(!"call handleCurveChainWithDistanceIndex(geomA) instead"); + assert(false, "call handleCurveChainWithDistanceIndex(geomA) instead"); return; } const index0 = this._results.length; diff --git a/core/geometry/src/curve/internalContexts/EllipticalArcApproximationContext.ts b/core/geometry/src/curve/internalContexts/EllipticalArcApproximationContext.ts index 4e9ec9488e28..5f618b319b03 100644 --- a/core/geometry/src/curve/internalContexts/EllipticalArcApproximationContext.ts +++ b/core/geometry/src/curve/internalContexts/EllipticalArcApproximationContext.ts @@ -342,7 +342,7 @@ class AdaptiveSubdivisionQ1IntervalErrorProcessor extends QuadrantFractionsProce /** Remember the initial value of the fraction f to be perturbed. */ public override announceQuadrantBegin(q: QuadrantFractions, reversed: boolean): boolean { assert(q.quadrant === 1); - assert(!reversed); // ASSUME bracket and q.fractions have same ordering + assert(!reversed); // ASSUME [bracket0, bracket1] and q.fractions have the same ordering // the first fraction might be an extra point for computing the first 3-pt arc. assert(q.fractions.length === 4 || (q.fractions.length === 3 && q.interpolateStartTangent)); this._error0 = this._error1 = Geometry.largeCoordinateResult; @@ -454,7 +454,7 @@ class AdaptiveSubdivisionQ1ErrorProcessor extends QuadrantFractionsProcessor { const interpolateStartTangent = Geometry.isAlmostEqualEitherNumber(f0, this._fractionRangeQ1.low, this._fractionRangeQ1.high, 0); const interpolateEndTangent = Geometry.isAlmostEqualEitherNumber(f1, this._fractionRangeQ1.low, this._fractionRangeQ1.high, 0); if (!interpolateStartTangent && undefined === fPrev) - fPrev = this.getPreviousFraction(f0); // createLastArc caller doesn't supply fPrev + fPrev = this.getPreviousFraction(f0); // createLastArc doesn't supply fPrev to announceArc const fractions = (undefined === fPrev) ? [f0, f, f1] : [fPrev, f0, f, f1]; const q1 = [QuadrantFractions.create(1, fractions, interpolateStartTangent, interpolateEndTangent)]; const processor = AdaptiveSubdivisionQ1IntervalErrorProcessor.create(this.fullEllipseXY, f0, f, f1); @@ -817,12 +817,13 @@ export class EllipticalArcApproximationContext { arc.sweep.setStartEndRadians(startAngle.radians, arc.sweep.endRadians); return arc; // returned arc starts at arcStart, ends at arcEnd }; - const createFirstArc = (f0: number, f1: number, reverse: boolean): void => { + const createFirstArc = (f0: number, f1: number): void => { // This arc starts at the first sample f0 and ends at f1. + // This arc interpolates point and tangent at f0, but only point at f1. ellipticalArc.fractionToPointAndDerivative(f0, ray); ellipticalArc.fractionToPoint(f1, pt1); - if (reverse) - ray.direction.scaleInPlace(-1); + if (f0 > f1) + ray.direction.scaleInPlace(-1); // computed arc is retrograde const arc = arcBetween2Samples(ray, pt1, false); if (arc) processor.announceArc(arc, undefined, f0, f1); @@ -838,12 +839,14 @@ export class EllipticalArcApproximationContext { if (arc) processor.announceArc(arc, fPrev, f1, f2); }; - const createLastArc = (f0: number, f1: number, reverse: boolean): void => { - // This arc starts at f0 and ends at the last sample f1. It is the only arc to use f1. + const createLastArc = (f0: number, f1: number): void => { + // This arc starts at f0 and ends at the last sample f1. + // This arc interpolates point and tangent at f1, but only point at f0. ellipticalArc.fractionToPoint(f0, pt0); ellipticalArc.fractionToPointAndDerivative(f1, ray); - if (!reverse) - ray.direction.scaleInPlace(-1); + if (f1 > f0) + ray.direction.scaleInPlace(-1); // computed arc is retrograde + // compute last arc from f1 to f0, then reverse const arc = arcBetween2Samples(ray, pt0, true); if (arc) processor.announceArc(arc, undefined, f0, f1); @@ -874,13 +877,13 @@ export class EllipticalArcApproximationContext { if (!processor.announceQuadrantBegin(q, reversed)) continue; if (q.interpolateStartTangent) - createFirstArc(q.fractions[0], q.fractions[1], reversed); + createFirstArc(q.fractions[0], q.fractions[1]); // the first inner arc approximates the ellipse over [f[1],f[2]]; the last inner arc, over [f[n-3],f[n-2]] for (let i = 0; i + 2 < n - 1; ++i) createInnerArc(q.fractions[i], q.fractions[i + 1], q.fractions[i + 2]); if (n > 2) { // the final arc approximates [f[n-2],f[n-1]] if (q.interpolateEndTangent) - createLastArc(q.fractions[n - 2], q.fractions[n - 1], reversed); + createLastArc(q.fractions[n - 2], q.fractions[n - 1]); else createInnerArc(q.fractions[n - 3], q.fractions[n - 2], q.fractions[n - 1]); } diff --git a/core/geometry/src/geometry3d/AngleSweep.ts b/core/geometry/src/geometry3d/AngleSweep.ts index 44e689c8651f..8cda7035aa02 100644 --- a/core/geometry/src/geometry3d/AngleSweep.ts +++ b/core/geometry/src/geometry3d/AngleSweep.ts @@ -523,28 +523,31 @@ export class AngleSweep implements BeJSONFunctions { } /** * Convert an AngleSweep to a JSON object. - * @return {*} {degrees: [startAngleInDegrees, endAngleInDegrees} + * @return {*} [startAngleInDegrees, endAngleInDegrees] */ public toJSON(): any { return [this.startDegrees, this.endDegrees]; } /** - * Test if this angle sweep and other angle sweep match with radians tolerance. - * * Period shifts are allowed. + * Test if two angle sweeps match within the given tolerance. + * * Period shifts are allowed, but orientations must be the same. + * @param other sweep to compare to this instance + * @param radianTol optional radian tolerance, default value `Geometry.smallAngleRadians` */ - public isAlmostEqualAllowPeriodShift(other: AngleSweep): boolean { - // We compare angle sweeps by checking if start angle and sweep match. We cannot compare start and end because for - // example (0, 90) and (360, 90) have the same start (we allow period shift) and end but are not same angle sweeps. - return Angle.isAlmostEqualRadiansAllowPeriodShift(this._radians0, other._radians0) - && Angle.isAlmostEqualRadiansAllowPeriodShift(this._radians1 - this._radians0, other._radians1 - other._radians0); + public isAlmostEqualAllowPeriodShift(other: AngleSweep, radianTol: number = Geometry.smallAngleRadians): boolean { + return this.isCCW === other.isCCW // this rules out equating opposite sweeps like [0,-100] and [0,260] + && Angle.isAlmostEqualRadiansAllowPeriodShift(this._radians0, other._radians0, radianTol) + && Angle.isAlmostEqualRadiansAllowPeriodShift(this._radians1 - this._radians0, other._radians1 - other._radians0, radianTol); } /** - * Test if this angle sweep and other angle sweep match with radians tolerance. + * Test if two angle sweeps match within the given tolerance. * * Period shifts are not allowed. + * @param other sweep to compare to this instance + * @param radianTol optional radian tolerance, default value `Geometry.smallAngleRadians` */ - public isAlmostEqualNoPeriodShift(other: AngleSweep): boolean { - return Angle.isAlmostEqualRadiansNoPeriodShift(this._radians0, other._radians0) - && Angle.isAlmostEqualRadiansNoPeriodShift(this._radians1 - this._radians0, other._radians1 - other._radians0); + public isAlmostEqualNoPeriodShift(other: AngleSweep, radianTol: number = Geometry.smallAngleRadians): boolean { + return Angle.isAlmostEqualRadiansNoPeriodShift(this._radians0, other._radians0, radianTol) + && Angle.isAlmostEqualRadiansNoPeriodShift(this._radians1 - this._radians0, other._radians1 - other._radians0, radianTol); } /** * Test if start and end angles match with radians tolerance. diff --git a/core/geometry/src/geometry3d/PolygonOps.ts b/core/geometry/src/geometry3d/PolygonOps.ts index a13988b44fe6..c8d8bacb3ed5 100644 --- a/core/geometry/src/geometry3d/PolygonOps.ts +++ b/core/geometry/src/geometry3d/PolygonOps.ts @@ -1262,7 +1262,7 @@ export class PolygonOps { const areaOfNormalParallelogram = Math.abs(outwardUnitNormalOfPrevEdge.crossProductXY(outwardUnitNormalOfEdge)); const coord = Geometry.conditionalDivideCoordinate(areaOfNormalParallelogram, projToPrevEdge.x * projToEdge.x, largestResult); if (undefined === coord) { - assert(!"unexpectedly small projection distance to an edge"); + assert(false, "unexpectedly small projection distance to an edge"); return undefined; // shouldn't happen due to chopping in computeEdgeDataXY: area/(dist*dist) <= 1/tol^2 = largestResult } coords[i] = coord; @@ -1273,7 +1273,7 @@ export class PolygonOps { } const scale = Geometry.conditionalDivideCoordinate(1.0, coordSum); if (undefined === scale) { - assert(!"unexpected zero barycentric coordinate sum"); + assert(false, "unexpected zero barycentric coordinate sum"); return undefined; } for (let i = 0; i < n; ++i) diff --git a/core/geometry/src/geometry4d/Map4d.ts b/core/geometry/src/geometry4d/Map4d.ts index ee151c8c74f0..60817c81d0ce 100644 --- a/core/geometry/src/geometry4d/Map4d.ts +++ b/core/geometry/src/geometry4d/Map4d.ts @@ -111,11 +111,11 @@ export class Map4d implements BeJSONFunctions { return this._matrix0.isAlmostEqual(other._matrix0) && this._matrix1.isAlmostEqual(other._matrix1); } /** - * Create a map between a frustum and world coordinates. - * @param origin lower left of frustum - * @param uVector Vector from lower left rear to lower right rear. - * @param vVector Vector from lower left rear to upper left rear. - * @param wVector Vector from lower left rear to lower left front, i.e. lower left rear towards eye. + * Create a world to NPC map that maps between world coordinates and the given frustum. + * @param origin lower left rear of frustum + * @param uVector Vector from origin to lower right rear. + * @param vVector Vector from origin to upper left rear. + * @param wVector Vector from origin to lower left front, i.e. origin towards eye. * @param fraction front size divided by rear size. */ public static createVectorFrustum( diff --git a/core/geometry/src/geometry4d/Matrix4d.ts b/core/geometry/src/geometry4d/Matrix4d.ts index 4a39098193ed..f5e7d3b19736 100644 --- a/core/geometry/src/geometry4d/Matrix4d.ts +++ b/core/geometry/src/geometry4d/Matrix4d.ts @@ -30,7 +30,7 @@ export type Matrix4dProps = Point4dProps[]; * * indices 8,9,10,11 are the "z row" They may be called the zx,zy,zz,zw entries * * indices 12,13,14,15 are the "w row". They may be called the wx,wy,wz,ww entries * * If "w row" contains numeric values 0,0,0,1, the Matrix4d is equivalent to a Transform with - * * The upper right 3x3 matrix (entries 0,1,2,4,5,6,8,9,10) are the 3x3 matrix part of the transform + * * The upper left 3x3 matrix (entries 0,1,2,4,5,6,8,9,10) are the 3x3 matrix part of the transform * * The far right column entries xw,yw,zw are the "origin" (sometimes called "translation") part of the transform. * @public */ @@ -190,7 +190,7 @@ export class Matrix4d implements BeJSONFunctions { return Matrix4d.createRowValues(scaleX, 0, 0, tx, 0, scaleY, 0, ty, 0, 0, scaleZ, tz, 0, 0, 0, 1, result); } /** - * Create a mapping the scales and translates (no rotation) from box A to boxB + * Create a mapping that scales and translates (no rotation) from box A to box B * @param lowA low point of box A * @param highA high point of box A * @param lowB low point of box B @@ -502,7 +502,7 @@ export class Matrix4d implements BeJSONFunctions { } /** multiply each matrix * points[i]. This produces a weighted xyzw. * Immediately renormalize back to xyz and replace the original point. - * If zero weight appears in the result (i.e. input is on eyeplane)leave the mapped xyz untouched. + * If zero weight appears in the result (i.e. input is on eyeplane) leave the mapped xyz untouched. */ public multiplyPoint3dArrayQuietNormalize(points: Point3d[]) { points.forEach((point) => this.multiplyXYZWQuietRenormalize(point.x, point.y, point.z, 1.0, point)); @@ -686,7 +686,7 @@ export class Matrix4d implements BeJSONFunctions { this._coffs[15] += a * vectorV.w; } /** - * ADD (n place) scale*A*B*AT where + * Add (in place) scale*A*B*AT where * * A is a pure translation with final column [x,y,z,1] * * B is the given `matrixB` * * AT is the transpose of A. @@ -735,12 +735,9 @@ export class Matrix4d implements BeJSONFunctions { * * A is a pure translation with final column [x,y,z,1] * * this is this matrix. * * AT is the transpose of A. - * * scale is a multiplier. - * @param matrixB the middle matrix. * @param ax x part of translation * @param ay y part of translation * @param az z part of translation - * @param scale scale factor for entire product */ public multiplyTranslationSandwichInPlace(ax: number, ay: number, az: number) { const bx = this._coffs[3]; diff --git a/core/geometry/src/numerics/Polynomials.ts b/core/geometry/src/numerics/Polynomials.ts index b8629ede19c4..aec17e6c02a2 100644 --- a/core/geometry/src/numerics/Polynomials.ts +++ b/core/geometry/src/numerics/Polynomials.ts @@ -28,7 +28,7 @@ import { Point4d } from "../geometry4d/Point4d"; * @internal */ export class Degree2PowerPolynomial { - /** The three coefficients for the quartic */ + /** The three coefficients for the quadratic */ public coffs: number[]; constructor(c0: number = 0, c1: number = 0, c2: number = 0) { @@ -913,7 +913,7 @@ export class AnalyticRoots { // EDL April 5, 2020 replace classic GraphicsGems solver by RWDNickalls. // Don't know if improveRoots is needed. // Breaks in AnalyticRoots.test.ts checkQuartic suggest it indeed converts many e-16 errors to zero. - // e-13 cases are unaffected + // e-13 cases are unaffected this.improveRoots(c, 3, results, false); } else { this.appendQuadraticRoots(c, results); @@ -923,6 +923,7 @@ export class AnalyticRoots { } /** Compute roots of quartic `c[0] + c[1] * x + c[2] * x^2 + c[3] * x^3 + c[4] * x^4` */ public static appendQuarticRoots(c: Float64Array | number[], results: GrowableFloat64Array) { + // for details, see core\geometry\internaldocs\quarticRoots.md const coffs = new Float64Array(4); let u: number; let v: number; @@ -952,7 +953,7 @@ export class AnalyticRoots { results.push(0); this.addConstant(origin, results); // apply origin return; - } else { // solve the resolvent cubic; more info: https://en.wikipedia.org/wiki/Resolvent_cubic#Second_definition + } else { // solve the resolvent cubic coffs[0] = 0.5 * r * p - 0.125 * q * q; coffs[1] = -r; coffs[2] = -0.5 * p; @@ -985,7 +986,6 @@ export class AnalyticRoots { coffs[2] = 1; this.appendQuadraticRoots(coffs, results); } - // substitute this.addConstant(origin, results); // apply origin results.sort(); this.improveRoots(c, 4, results, true); @@ -1120,8 +1120,7 @@ export class TrigPolynomial { // tolerance for small angle decision. private static readonly _smallAngle: number = 1.0e-11; - // see itwinjs-core\core\geometry\internaldocs\unitCircleEllipseIntersection.md - // on how below variables are derived. + // see core\geometry\internaldocs\unitCircleEllipseIntersection.md for derivation of these coefficients. /** Standard Basis coefficients for the numerator of the y-coordinate y(t) = S(t)/W(t) in the rational semicircle parameterization. */ public static readonly S = Float64Array.from([0.0, 2.0, -2.0]); /** Standard Basis coefficients for the numerator of the x-coordinate x(t) = C(t)/W(t) in the rational semicircle parameterization. */ @@ -1146,7 +1145,7 @@ export class TrigPolynomial { /** * Solve a polynomial created from trigonometric condition using Trig.S, Trig.C, Trig.W. * * Polynomial is of degree 4: - * `coff[0] + coff[1] * t + coff[2] * t^2 + coff[3] * t^3 + coff[4] * t^4` + * `p(t) = coff[0] + coff[1] * t + coff[2] * t^2 + coff[3] * t^3 + coff[4] * t^4` * * Solution logic includes inferring angular roots corresponding zero leading coefficients * (roots at infinity). * @param coff coefficients. @@ -1176,14 +1175,12 @@ export class TrigPolynomial { degree--; const roots = new GrowableFloat64Array(); if (degree === -1) { - // Umm. Dunno. Nothing there. + // do nothing } else { if (degree === 0) { - // p(t) is a nonzero constant - // No roots, but not degenerate. + // p(t) is a nonzero constant; no roots but not degenerate. } else if (degree === 1) { - // p(t) = coff[1] * t + coff[0] - roots.push(- coff[0] / coff[1]); + roots.push(-coff[0] / coff[1]); // p(t) = coff[0] + coff[1] * t } else if (degree === 2) { AnalyticRoots.appendQuadraticRoots(coff, roots); } else if (degree === 3) { @@ -1194,17 +1191,15 @@ export class TrigPolynomial { // TODO: WORK WITH BEZIER SOLVER } if (roots.length > 0) { - // Each solution t represents an angle with - // Math.Cos(theta) = C(t)/W(t) and sin(theta) = S(t)/W(t) + // Each solution t represents an angle with Math.Cos(theta) = C(t)/W(t) and sin(theta) = S(t)/W(t) // Division by W has no effect on atan2 calculations, so we just compute S(t),C(t) for (let i = 0; i < roots.length; i++) { const ss = PowerPolynomial.evaluate(this.S, roots.atUncheckedIndex(i)); const cc = PowerPolynomial.evaluate(this.C, roots.atUncheckedIndex(i)); radians.push(Math.atan2(ss, cc)); } - // Each leading zero at the front of the coefficients corresponds to a root at -PI/2. - // Only make one entry.... - // for (int i = degree; i < nominalDegree; i++) + // each leading zero at the front of the coefficient array corresponds to a root at -PI/2. + // only make one entry because we don't report multiplicity. if (degree < nominalDegree) radians.push(-0.5 * Math.PI); } @@ -1230,8 +1225,7 @@ export class TrigPolynomial { const coffs = new Float64Array(5); PowerPolynomial.zero(coffs); let degree; - // see itwinjs-core\core\geometry\internaldocs\unitCircleEllipseIntersection.md - // on how coffs (coefficient array) is built. + // see core\geometry\internaldocs\unitCircleEllipseIntersection.md for derivation of these coefficients if (Geometry.hypotenuseXYZ(axx, axy, ayy) > TrigPolynomial._coefficientRelTol * Geometry.hypotenuseXYZ(ax, ay, a)) { PowerPolynomial.accumulate(coffs, this.CW, ax); PowerPolynomial.accumulate(coffs, this.SW, ay); @@ -1274,9 +1268,14 @@ export class TrigPolynomial { * @param ellipseRadians solution angles in ellipse parameter space * @param circleRadians solution angles in circle parameter space */ - public static solveUnitCircleEllipseIntersection(cx: number, cy: number, ux: number, uy: number, - vx: number, vy: number, ellipseRadians: number[], circleRadians: number[]): boolean { + public static solveUnitCircleEllipseIntersection( + cx: number, cy: number, + ux: number, uy: number, + vx: number, vy: number, + ellipseRadians: number[], circleRadians: number[], + ): boolean { circleRadians.length = 0; + // see core\geometry\internaldocs\unitCircleEllipseIntersection.md for derivation of these coefficients: const acc = ux * ux + uy * uy; const acs = 2.0 * (ux * vx + uy * vy); const ass = vx * vx + vy * vy; @@ -1315,8 +1314,7 @@ export class TrigPolynomial { ellipseRadians: number[], circleRadians: number[], ): boolean { circleRadians.length = 0; - // see itwinjs-core\core\geometry\internaldocs\unitCircleEllipseIntersection.md - // on how below variables are derived. + // see core\geometry\internaldocs\unitCircleEllipseIntersection.md for derivation of these coefficients: const acc = ux * ux + uy * uy - uw * uw; const acs = 2.0 * (ux * vx + uy * vy - uw * vw); const ass = vx * vx + vy * vy - vw * vw; diff --git a/core/geometry/src/polyface/PolyfaceClip.ts b/core/geometry/src/polyface/PolyfaceClip.ts index 63ce8aa170f3..62aa49fa296d 100644 --- a/core/geometry/src/polyface/PolyfaceClip.ts +++ b/core/geometry/src/polyface/PolyfaceClip.ts @@ -99,14 +99,14 @@ export class PolyfaceClip { * * Return all surviving clip as a new mesh. * * WARNING: The new mesh is "points only" -- parameters, normals, etc are not interpolated */ - public static clipPolyfaceClipPlaneWithClosureFace(polyface: Polyface, clipper: ClipPlane, insideClip: boolean = true, buildClosureFaces: boolean = true) { + public static clipPolyfaceClipPlaneWithClosureFace(polyface: Polyface, clipper: ClipPlane, insideClip: boolean = true, buildClosureFaces: boolean = true): IndexedPolyface { return this.clipPolyfaceClipPlane(polyface, clipper, insideClip, buildClosureFaces); } /** Clip each facet of polyface to the ClipPlane. * * Return all surviving clip as a new mesh. * * WARNING: The new mesh is "points only" -- parameters, normals, etc are not interpolated */ - public static clipPolyfaceClipPlane(polyface: Polyface, clipper: ClipPlane, insideClip: boolean = true, buildClosureFaces: boolean = false): Polyface { + public static clipPolyfaceClipPlane(polyface: Polyface, clipper: ClipPlane, insideClip: boolean = true, buildClosureFaces: boolean = false): IndexedPolyface { const builders = ClippedPolyfaceBuilders.create(insideClip, !insideClip, buildClosureFaces); this.clipPolyfaceInsideOutside(polyface, clipper, builders); return builders.claimPolyface(insideClip ? 0 : 1, true)!; @@ -116,7 +116,7 @@ export class PolyfaceClip { * * Return surviving clip as a new mesh. * * WARNING: The new mesh is "points only". */ - public static clipPolyfaceConvexClipPlaneSet(polyface: Polyface, clipper: ConvexClipPlaneSet): Polyface { + public static clipPolyfaceConvexClipPlaneSet(polyface: Polyface, clipper: ConvexClipPlaneSet): IndexedPolyface { const visitor = polyface.createVisitor(0); const builder = PolyfaceBuilder.create(); const work = new GrowableXYZArray(10); @@ -215,7 +215,7 @@ export class PolyfaceClip { for (const child of region.children) this.addRegion(builder, child); } else { - assert(!"unexpected region encountered"); + assert(false, "unexpected region encountered"); } } } diff --git a/core/geometry/src/test/curve/Arc3d.test.ts b/core/geometry/src/test/curve/Arc3d.test.ts index baaf4a98d8e3..ba53972fdc38 100644 --- a/core/geometry/src/test/curve/Arc3d.test.ts +++ b/core/geometry/src/test/curve/Arc3d.test.ts @@ -6,6 +6,7 @@ import { assert, describe, expect, it } from "vitest"; import { compareNumbers, OrderedSet } from "@itwin/core-bentley"; import { Constant } from "../../Constant"; +import { CurveFactory } from "../../core-geometry"; import { Arc3d, EllipticalArcApproximationOptions, EllipticalArcSampleMethod, FractionMapper } from "../../curve/Arc3d"; import { CoordinateXYZ } from "../../curve/CoordinateXYZ"; import { CurveChainWithDistanceIndex } from "../../curve/CurveChainWithDistanceIndex"; @@ -716,6 +717,15 @@ describe("Arc3d", () => { ck.testPoint3d(circularArc4.endPoint(), end4); ck.testPoint3d(circularArc4.center, Point3d.create(0.75, 0, 0.75)); + dx += 10; + const start5 = Point3d.create(10, 0, 0); + const end5 = Point3d.create(7.0710678118654755, 2.1213203435596424, 0); + const tangent5 = Vector3d.create(-0, -18.84955592153876, -0); + const circularArc5A = Arc3d.createCircularStartTangentEnd(start5, tangent5, end5) as Arc3d; + const circularArc5B = CurveFactory.createArcPointTangentPoint(start5, tangent5, end5) as Arc3d; + ck.testTrue(circularArc5A.isAlmostEqual(circularArc5B, 1.0e-13, 1.0e-13), "methods are equivalent"); + GeometryCoreTestIO.captureCloneGeometry(allGeometry, [circularArc5A, circularArc5B], dx); + GeometryCoreTestIO.saveGeometry(allGeometry, "Arc3d", "createCircularStartTangentEnd"); expect(ck.getNumErrors()).toBe(0); }); @@ -1054,7 +1064,6 @@ describe("ApproximateArc3d", () => { } } } - y0 += yDelta(yWidth); } if ( @@ -1229,13 +1238,13 @@ describe("ApproximateArc3d", () => { x += delta; } - // Observed: subdivision wins 90.9% of comparisons to n-sample methods (95.67% with enableLongTests) + // Observed: subdivision wins 90.9% of comparisons to n-sample methods (95.7% with enableLongTests) const winPct = 100 * Geometry.safeDivideFraction(nSubdivisionComparisonWins, nComparisons, 0); GeometryCoreTestIO.consoleLog(`Subdivision wins ${nSubdivisionComparisonWins} of ${nComparisons} comparisons (${winPct}%).`); const targetWinPct = GeometryCoreTestIO.enableLongTests ? 90 : 85; ck.testLE(targetWinPct, winPct, `Subdivision is more accurate than another n-sample method over ${targetWinPct}% of the time.`); - // Observed: subdivision is most accurate method in 64% of ellipses tested (82.76% with enableLongTests) + // Observed: subdivision is most accurate method in 63.6% of ellipses tested (82.8% with enableLongTests) const winOverallPct = 100 * Geometry.safeDivideFraction(nEllipses - nSubdivisionLosses, nEllipses, 0); GeometryCoreTestIO.consoleLog(`Subdivision wins overall for ${nEllipses - nSubdivisionLosses} of ${nEllipses} ellipses (${winOverallPct}%).`); const targetNSampleWinPct = GeometryCoreTestIO.enableLongTests ? 80 : 60; diff --git a/core/geometry/src/test/numerics/Polynomial.test.ts b/core/geometry/src/test/numerics/Polynomial.test.ts index 6a2d62ca85ca..117d18f3eda0 100644 --- a/core/geometry/src/test/numerics/Polynomial.test.ts +++ b/core/geometry/src/test/numerics/Polynomial.test.ts @@ -708,3 +708,67 @@ function sphereSnakeGridPoints(center: Point3d, radius: number, thetaArray: numb } return points; } + +function captureUnitCircleEllipseIntersections( + allGeometry: any[], ck: any, + cx: number, cy: number, + ux: number, uy: number, + vx: number, vy: number, + dx: number, expectedIntersections: number, +) { + const unitCircle = Arc3d.create( + Point3d.create(0, 0), Vector3d.create(1, 0), Vector3d.create(0, 1), AngleSweep.createStartEndDegrees(), + ); + const arc = Arc3d.create( + Point3d.create(cx, cy), Vector3d.create(ux, uy), Vector3d.create(vx, vy), + AngleSweep.createStartEndDegrees(), + ); + GeometryCoreTestIO.captureCloneGeometry(allGeometry, unitCircle, dx); + GeometryCoreTestIO.captureCloneGeometry(allGeometry, arc, dx); + + const ellipseRadians: number[] = []; + const circleRadians: number[] = []; + TrigPolynomial.solveUnitCircleEllipseIntersection(cx, cy, ux, uy, vx, vy, ellipseRadians, circleRadians); + const len = ellipseRadians.length; + for (let i = 0; i < len; i++) { + const fraction = arc.sweep.radiansToSignedFraction(ellipseRadians[i]); + GeometryCoreTestIO.createAndCaptureXYCircle(allGeometry, arc.fractionToPoint(fraction), 0.2, dx); + } + ck.testExactNumber(len, expectedIntersections, `${expectedIntersections} intersection(s) expected`); +} + +it("unitCircleEllipseIntersection", () => { + const ck = new Checker(); + const allGeometry: GeometryQuery[] = []; + let dx = 0; + // intersection at lower half of the ellipse + let cx = 0, cy = 1.5; // ellipse center + let ux = 2, uy = 0; // ellipse vector0 + let vx = 0, vy = 1; // ellipse vector90 + let expectedIntersections = 2; + captureUnitCircleEllipseIntersections(allGeometry, ck, cx, cy, ux, uy, vx, vy, dx, expectedIntersections); + // intersection at t = 0 + dx += 12; + cx = 3, cy = 0; + ux = 2, uy = 0; + vx = 0, vy = 1; + expectedIntersections = 1; + captureUnitCircleEllipseIntersections(allGeometry, ck, cx, cy, ux, uy, vx, vy, dx, expectedIntersections); + // intersection at t = 1 + dx += 12; + cx = -3, cy = 0; + ux = 2, uy = 0; + vx = 0, vy = 1; + expectedIntersections = 1; + captureUnitCircleEllipseIntersections(allGeometry, ck, cx, cy, ux, uy, vx, vy, dx, expectedIntersections); + // intersection at t = infinity + dx += 12; + cx = 0, cy = 2; + ux = 2, uy = 0; + vx = 0, vy = 1; + expectedIntersections = 1; + // captureUnitCircleEllipseIntersections(allGeometry, ck, cx, cy, ux, uy, vx, vy, dx, expectedIntersections); + + GeometryCoreTestIO.saveGeometry(allGeometry, "CurveCurveIntersectXY", "unitCircleEllipseIntersection"); + expect(ck.getNumErrors()).toBe(0); +});