Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
31b752d
implement PI point construction
dassaf4 Oct 9, 2025
4334166
doc
dassaf4 Oct 9, 2025
b874f67
Merge branch 'master' into da4/arc3d-pi-point
dassaf4 Oct 9, 2025
6dc6fd0
Merge branch 'da4/arc3d-pi-point' of https://github.com/iTwin/itwinjs…
dassaf4 Oct 9, 2025
2936ee4
added tests for Arc3d.computeTangentIntersection
saeeedtorabi Oct 10, 2025
2932bab
Merge branch 'master' into da4/arc3d-pi-point
saeeedtorabi Oct 10, 2025
4bfef74
filter more almost-parallel tangents than SmallSystem
dassaf4 Oct 10, 2025
cb960eb
more tests
saeeedtorabi Oct 10, 2025
c5ddfa5
Merge branch 'da4/arc3d-pi-point' of https://github.com/iTwin/itwinjs…
saeeedtorabi Oct 10, 2025
26d6980
clean up the tests
saeeedtorabi Oct 10, 2025
64f4159
filter even more almost-parallel tangents than SmallSystem
dassaf4 Oct 10, 2025
fc6f23b
Merge branch 'da4/arc3d-pi-point' of https://github.com/iTwin/itwinjs…
dassaf4 Oct 10, 2025
29b3d8a
add near semi-ellipse test
dassaf4 Oct 10, 2025
bf503cd
Merge branch 'master' into da4/arc3d-pi-point
dassaf4 Oct 10, 2025
65a9144
Merge branch 'master' into da4/arc3d-pi-point
dassaf4 Oct 10, 2025
d0af0d5
Merge branch 'da4/arc3d-pi-point' of https://github.com/iTwin/itwinjs…
dassaf4 Oct 10, 2025
eaa36c5
Merge branch 'master' into da4/arc3d-pi-point
dassaf4 Oct 11, 2025
c4da70d
Merge branch 'master' into da4/arc3d-pi-point
saeeedtorabi Oct 14, 2025
d4cf2e3
Merge branch 'master' into da4/arc3d-pi-point
dassaf4 Oct 14, 2025
4ddbe5a
Merge branch 'master' into da4/arc3d-pi-point
dassaf4 Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/api/core-geometry.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions {
cloneTransformed(transform: Transform): Arc3d;
closestPoint(spacePoint: Point3d, extend: VariantCurveExtendParameter, result?: CurveLocationDetail): CurveLocationDetail;
computeStrokeCountForOptions(options?: StrokeOptions): number;
computeTangentIntersection(f0?: number, f1?: number, result?: Point3d): Point3d | undefined;
constructCircularArcChainApproximation(options?: EllipticalArcApproximationOptions): CurveChain | Arc3d | undefined;
constructOffsetXY(offsetDistanceOrOptions: number | OffsetOptions): CurvePrimitive | CurvePrimitive[] | undefined;
static create(center: Point3d | undefined, vector0: Vector3d, vector90: Vector3d, sweep?: AngleSweep, result?: Arc3d): Arc3d;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-geometry",
"comment": "",
"type": "none"
}
],
"packageName": "@itwin/core-geometry"
}
40 changes: 36 additions & 4 deletions core/geometry/src/curve/Arc3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions {
private static _workVectorU = Vector3d.create();
private static _workVectorV = Vector3d.create();
private static _workVectorW = Vector3d.create();
private static _workRay0 = Ray3d.createZero();
private static _workRay1 = Ray3d.createZero();
private static _workRay2 = Ray3d.createZero();
/** Read/write the center. Getter returns clone. */
public get center(): Point3d {
return this._center.clone();
Expand Down Expand Up @@ -1414,7 +1417,6 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions {
* @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.
*
*/
public static createFilletArc(point0: Point3d, point1: Point3d, point2: Point3d, radius: number): ArcBlendData {
const vector10 = Vector3d.createStartEnd(point1, point0);
Expand All @@ -1428,9 +1430,9 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions {
// vector10, vector12, and bisector are UNIT vectors
// bisector splits the angle between vector10 and vector12
const perpendicular = vector12.minus(vector10);
const perpendicularMagnitude = perpendicular.magnitude(); // == 2 * sin(theta)
const perpendicularMagnitude = perpendicular.magnitude(); // == 2 * sin(theta)
const sinTheta = 0.5 * perpendicularMagnitude;
if (!Geometry.isSmallAngleRadians(sinTheta)) { // (for small theta, sinTheta is almost equal to theta)
if (!Geometry.isSmallAngleRadians(sinTheta)) { // for small theta, sinTheta is almost equal to theta
const cosTheta = Math.sqrt(1 - sinTheta * sinTheta);
const tanTheta = sinTheta / cosTheta;
const alphaRadians = Math.acos(sinTheta);
Expand All @@ -1441,7 +1443,9 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions {
const center = point1.plusScaled(bisector, distanceToCenter);
bisector.scaleInPlace(-radius);
perpendicular.scaleInPlace(radius / perpendicularMagnitude);
const arc02 = Arc3d.create(center, bisector, perpendicular, AngleSweep.createStartEndRadians(-alphaRadians, alphaRadians));
const arc02 = Arc3d.create(
center, bisector, perpendicular, AngleSweep.createStartEndRadians(-alphaRadians, alphaRadians),
);
return { arc: arc02, fraction10: f10, fraction12: f12, point: point1.clone() };
}
}
Expand Down Expand Up @@ -1523,4 +1527,32 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions {
return (this.sweep.isFullCircle && options.forcePath) ? Path.create(this) : this;
return result;
}
/**
* Compute the intersection of the tangent vectors at two fractional parameters along the arc.
* * In the civil design context of filleting a line string, the default values yield a fillet arc's "point of
* intersection", aka _PI_. This point is the line string vertex that was rounded by the fillet arc placed
* between the vertex's adjacent segments. In other words, the original line string vertices can be recovered
* from the fillets with this method.
* @param f0 fractional parameter of one tangent. Default is 0 (the arc's start tangent).
* @param f1 fractional parameter of the other tangent. Default is 1 (the arc's end tangent).
* @param result optional point to populate and return.
* @returns intersection point, or undefined if tangents are zero or parallel.
*/
public computeTangentIntersection(f0: number = 0, f1: number = 1, result?: Point3d): Point3d | undefined {
const localRay0 = this.fractionToPointAndDerivative(f0, Arc3d._workRay0);
const localRay1 = this.fractionToPointAndDerivative(f1, Arc3d._workRay1);
if (localRay0.direction.isParallelTo(localRay1.direction, true, true))
return undefined;
const worldRay0 = localRay0.clone(Arc3d._workRay2);
if (this.matrixRef.multiplyInverseXYZAsPoint3d(localRay0.origin.x, localRay0.origin.y, localRay0.origin.z, localRay0.origin)
&& this.matrixRef.multiplyInverseXYZAsPoint3d(localRay1.origin.x, localRay1.origin.y, localRay1.origin.z, localRay1.origin)
&& this.matrixRef.multiplyInverseXYZAsVector3d(localRay0.direction.x, localRay0.direction.y, localRay0.direction.z, localRay0.direction)
&& this.matrixRef.multiplyInverseXYZAsVector3d(localRay1.direction.x, localRay1.direction.y, localRay1.direction.z, localRay1.direction)
) { // conversion to local coordinates allows us to intersect without z
const intersection = SmallSystem.lineXYUVTransverseIntersection(localRay0.origin, localRay0.direction, localRay1.origin, localRay1.direction);
if (intersection)
return worldRay0.fractionToPoint(intersection.x, result); // intersection parameter is an affine invariant
}
return undefined;
}
}
123 changes: 123 additions & 0 deletions core/geometry/src/test/curve/Arc3d.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1759,4 +1759,127 @@ describe("Arc3dTangents", () => {
GeometryCoreTestIO.saveGeometry(allGeometry, "Arc3dTangents", "LineTangentPointCircle");
expect(ck.getNumErrors()).toBe(0);
});

it("TangentIntersection", () => {
const ck = new Checker();
const allGeometry: GeometryQuery[] = [];

const testTangentIntersection = (arc0: Arc3d, expectedIntersection0: Point3d, dx: number, scale0: number) => {
GeometryCoreTestIO.captureCloneGeometry(allGeometry, arc0, dx);
const intersection = arc0.computeTangentIntersection();
ck.testDefined(intersection, "intersection is defined");
ck.testPoint3d(
intersection!, expectedIntersection0, `intersection at (${JSON.stringify(expectedIntersection0.toJSON())})`,
);
GeometryCoreTestIO.createAndCaptureXYCircle(allGeometry, intersection!, 0.1, dx);
const ray0 = arc0.fractionToPointAndDerivative(0);
const ls0 = LineSegment3d.create(
ray0.origin.plusScaled(ray0.direction.scaleToLength(1)!, -scale0),
ray0.origin.plusScaled(ray0.direction.scaleToLength(1)!, scale0),
);
GeometryCoreTestIO.captureCloneGeometry(allGeometry, ls0, dx);
const ray1 = arc0.fractionToPointAndDerivative(1);
const ls1 = LineSegment3d.create(
ray1.origin.plusScaled(ray1.direction.scaleToLength(1)!, -scale0),
ray1.origin.plusScaled(ray1.direction.scaleToLength(1)!, scale0),
);
GeometryCoreTestIO.captureCloneGeometry(allGeometry, ls1, dx);
}

// unit circle
let x0 = 0;
let scale = 1;
let arc = Arc3d.createXY(Point3d.createZero(), 1, AngleSweep.createStartEndDegrees(0, 90));
let expectedIntersection = Point3d.create(1, 1);
testTangentIntersection(arc, expectedIntersection, x0, scale);
arc.sweep = AngleSweep.createStartEndDegrees(0, 180);
ck.testUndefined(arc.computeTangentIntersection(), "no intersection on semi-circle");
x0 += 10;
arc.sweep = AngleSweep.createStartEndDegrees(0, 270);
expectedIntersection = Point3d.create(1, -1);
testTangentIntersection(arc, expectedIntersection, x0, scale);

// normal ellipse
x0 += 10;
scale = 4;
arc = Arc3d.create(
Point3d.create(0, 0), Vector3d.create(2, 0), Vector3d.create(0, 3), AngleSweep.createStartEndDegrees(0, 90),
);
expectedIntersection = Point3d.create(2, 3);
testTangentIntersection(arc, expectedIntersection, x0, scale);
arc.sweep = AngleSweep.createStartEndDegrees(0, 180);
ck.testUndefined(arc.computeTangentIntersection(), "no intersection on semi-ellipse");
x0 += 10;
arc.sweep = AngleSweep.createStartEndDegrees(0, 270);
expectedIntersection = Point3d.create(2, -3);
testTangentIntersection(arc, expectedIntersection, x0, scale);

// tilted ellipse
x0 += 10;
scale = 2;
arc = Arc3d.create(
Point3d.create(0, 0), Vector3d.create(1, 0), Vector3d.create(1, 1), AngleSweep.createStartEndDegrees(0, 90),
);
expectedIntersection = Point3d.create(2, 1);
testTangentIntersection(arc, expectedIntersection, x0, scale);
arc.sweep = AngleSweep.createStartEndDegrees(0, 180);
ck.testUndefined(arc.computeTangentIntersection(), "no intersection on semi-ellipse");
x0 += 10;
arc.sweep = AngleSweep.createStartEndDegrees(179.99, 0);
expectedIntersection = Point3d.create(11460.15587355, 11459.15587355);
testTangentIntersection(arc, expectedIntersection, x0, scale);
x0 += 10;
arc.sweep = AngleSweep.createStartEndDegrees(0, 270);
expectedIntersection = Point3d.create(0, -1);
testTangentIntersection(arc, expectedIntersection, x0, scale);

// non-xy ellipse
x0 += 10;
scale = 2;
arc = Arc3d.create(
Point3d.create(0, 0), Vector3d.create(1, 0), Vector3d.create(0, 0, 1), AngleSweep.createStartEndDegrees(0, 90),
);
expectedIntersection = Point3d.create(1, 0, 1);
testTangentIntersection(arc, expectedIntersection, x0, scale);
arc.sweep = AngleSweep.createStartEndDegrees(0, 180);
ck.testUndefined(arc.computeTangentIntersection(), "no intersection on semi-ellipse");
x0 += 10;
arc.sweep = AngleSweep.createStartEndDegrees(0, 270);
expectedIntersection = Point3d.create(1, 0, -1);
testTangentIntersection(arc, expectedIntersection, x0, scale);

// special cases
ck.testUndefined(arc.computeTangentIntersection(0, 0), "no intersection for equal fractions");
ck.testUndefined(arc.computeTangentIntersection(0.5, 0.5), "no intersection for equal fractions");
arc.sweep = AngleSweep.createStartEndDegrees(0, 0);
ck.testUndefined(arc.computeTangentIntersection(), "no intersection for zero sweep");
arc.sweep = AngleSweep.createStartEndDegrees(0, 360);
ck.testUndefined(arc.computeTangentIntersection(), "no intersection for full sweep at default fractions");
ck.testUndefined(arc.computeTangentIntersection(0, 1.5), "no intersection for full sweep at fractions 0, 1.5");
arc.sweep = AngleSweep.createStartEndDegrees(0, 1e-15);
ck.testUndefined(arc.computeTangentIntersection(), "no intersection for close fractions");

// fillet
x0 += 10;
const lineString = LineString3d.create([0, 0, -1], [2, 0], [5, 3], [5, 5, 1], [3, 5]);
GeometryCoreTestIO.captureCloneGeometry(allGeometry, lineString, x0);
for (const p of lineString.points)
GeometryCoreTestIO.createAndCaptureXYCircle(allGeometry, p, 0.1, x0);
const radius = 1.0;
const chain = CurveFactory.createFilletsInLineString(lineString, radius)!;
GeometryCoreTestIO.captureCloneGeometry(allGeometry, chain, x0);
const filletIndicesInChain = [1, 3, 5];
const filletedIndicesInLineString = [1, 2, 3];
for (let i = 0; i < filletIndicesInChain.length; i++) {
const fillet = chain.children[filletIndicesInChain[i]] as Arc3d;
ck.testPoint3d(
lineString.points[filletedIndicesInLineString[i]],
fillet.computeTangentIntersection()!,
`fillet tangents at lineString point[${filletedIndicesInLineString[i]}]`,
);
}

GeometryCoreTestIO.saveGeometry(allGeometry, "Arc3dTangents", "TangentIntersection");
expect(ck.getNumErrors()).toBe(0);
});
});