Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow LineString3d.clonePartialCurve() to extend the instance #3315

Merged
merged 7 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 5 additions & 3 deletions common/api/core-geometry.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions {
clone(): Arc3d;
cloneAtZ(z?: number): Arc3d;
cloneInRotatedBasis(theta: Angle): Arc3d;
clonePartialCurve(fractionA: number, fractionB: number): CurvePrimitive | undefined;
clonePartialCurve(fractionA: number, fractionB: number): Arc3d;
cloneTransformed(transform: Transform): Arc3d;
closestPoint(spacePoint: Point3d, extend: VariantCurveExtendParameter, result?: CurveLocationDetail): CurveLocationDetail;
computeStrokeCountForOptions(options?: StrokeOptions): number;
Expand Down Expand Up @@ -2543,6 +2543,8 @@ export namespace IModelJson {
topY?: number;
}
export interface BSplineSurfaceProps {
closedU?: boolean;
closedV?: boolean;
orderU: number;
orderV: number;
points: [[[number]]];
Expand Down Expand Up @@ -3114,7 +3116,7 @@ export class LineSegment3d extends CurvePrimitive implements BeJSONFunctions {
announceClipIntervals(clipper: Clipper, announce?: AnnounceNumberNumberCurvePrimitive): boolean;
appendPlaneIntersectionPoints(plane: PlaneAltitudeEvaluator, result: CurveLocationDetail[]): number;
clone(): LineSegment3d;
clonePartialCurve(fractionA: number, fractionB: number): CurvePrimitive | undefined;
clonePartialCurve(fractionA: number, fractionB: number): LineSegment3d;
cloneTransformed(transform: Transform): LineSegment3d;
closestPoint(spacePoint: Point3d, extend: VariantCurveExtendParameter, result?: CurveLocationDetail): CurveLocationDetail;
computeStrokeCountForOptions(options?: StrokeOptions): number;
Expand Down Expand Up @@ -3174,7 +3176,7 @@ export class LineString3d extends CurvePrimitive implements BeJSONFunctions {
appendStrokePoint(point: Point3d, fraction?: number): void;
clear(): void;
clone(): LineString3d;
clonePartialCurve(fractionA: number, fractionB: number): CurvePrimitive | undefined;
clonePartialCurve(fractionA: number, fractionB: number): LineString3d;
cloneTransformed(transform: Transform): LineString3d;
closestPoint(spacePoint: Point3d, extend: VariantCurveExtendParameter, result?: CurveLocationDetail): CurveLocationDetail;
collectCurvePrimitivesGo(collectorArray: CurvePrimitive[], _smallestPossiblePrimitives: boolean, explodeLinestrings?: boolean): void;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-geometry",
"comment": "allow LineString3d.clonePartialCurve to extend start/end segments",
"type": "none"
}
],
"packageName": "@itwin/core-geometry"
}
2 changes: 1 addition & 1 deletion core/geometry/src/bspline/BSplineCurve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ export class BSplineCurve3d extends BSplineCurve3dBase {
}

/** Create a smoothly closed B-spline curve with uniform knots.
* Note that the curve does not start at the first pole.
* Note that the curve does not start at the first pole, and first and last poles should be distinct.
*/
public static createPeriodicUniformKnots(poles: Point3d[] | Float64Array | GrowableXYZArray, order: number): BSplineCurve3d | undefined {
const numPoles = poles instanceof Float64Array ? poles.length / 3 : poles.length;
Expand Down
6 changes: 2 additions & 4 deletions core/geometry/src/curve/Arc3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,15 +883,13 @@ export class Arc3d extends CurvePrimitive implements BeJSONFunctions {
* @param fractionA [in] start fraction
* @param fractionB [in] end fraction
*/
public override clonePartialCurve(fractionA: number, fractionB: number): CurvePrimitive | undefined {
public override clonePartialCurve(fractionA: number, fractionB: number): Arc3d {
if (fractionB < fractionA) {
const arcA = this.clonePartialCurve(fractionB, fractionA);
if (arcA)
arcA.reverseInPlace();
arcA.reverseInPlace();
return arcA;
}
const arcB = this.clone();

arcB.sweep.setStartEndRadians(
this.sweep.fractionToRadians(fractionA),
this.sweep.fractionToRadians(fractionB));
Expand Down
2 changes: 1 addition & 1 deletion core/geometry/src/curve/LineSegment3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export class LineSegment3d extends CurvePrimitive implements BeJSONFunctions {
* @param fractionA [in] start fraction
* @param fractionB [in] end fraction
*/
public override clonePartialCurve(fractionA: number, fractionB: number): CurvePrimitive | undefined {
public override clonePartialCurve(fractionA: number, fractionB: number): LineSegment3d {
return LineSegment3d.create(this.fractionToPoint(fractionA), this.fractionToPoint(fractionB));
}

Expand Down
72 changes: 49 additions & 23 deletions core/geometry/src/curve/LineString3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1141,48 +1141,74 @@ export class LineString3d extends CurvePrimitive implements BeJSONFunctions {
}
if (index < 0)
index = 0;
if (index >= n) {
index = n - 1;
if (index > n - 2) {
index = n - 2;
fraction += 1;
}
this._points.interpolate(index, fraction, index + 1, LineString3d._indexPoint);
dest.push(LineString3d._indexPoint);
}
/** Return (if possible) a LineString which is a portion of this curve.
* * This implementation does NOT extrapolate the linestring -- fractions are capped at 0 and 1.
/** Return a LineString which is a portion of this curve.
* * Fractions outside [0,1] extend the relevant end segment.
* @param fractionA [in] start fraction
* @param fractionB [in] end fraction
*/
public override clonePartialCurve(fractionA: number, fractionB: number): CurvePrimitive | undefined {
public override clonePartialCurve(fractionA: number, fractionB: number): LineString3d {
if (fractionB < fractionA) {
const linestringA = this.clonePartialCurve(fractionB, fractionA);
if (linestringA)
linestringA.reverseInPlace();
return linestringA;
}
const n = this._points.length;
if (n < 2)
return this.clone();
if (n > 2 && this.isPhysicallyClosed) {
// don't extend a closed linestring
if (fractionA < 0)
fractionA = 0;
if (fractionB > 1)
fractionB = 1;
}
const numEdge = n - 1;
if (n < 2 || fractionA >= 1.0 || fractionB <= 0.0)
return undefined;
if (fractionA < 0)
fractionA = 0;
if (fractionB > 1)
fractionB = 1;
const gA = fractionA * numEdge;
const gB = fractionB * numEdge;
const indexA = Math.floor(gA);
const indexB = Math.floor(gB);
const localFractionA = gA - indexA;
const localFractionB = gB - indexB;
let indexA: number; // left index of first extended/partial segment of clone
let indexB: number; // left index of last extended/partial segment of clone
let index0, index1: number; // range of original vertices to copy into clone
let localFractionA = fractionA;
let localFractionB = fractionB;
if (fractionA < 0) {
indexA = 0;
index0 = 1; // first original vertex is not in clone
} else if (0 <= fractionA && fractionA <= 1) {
const gA = fractionA * numEdge;
indexA = Math.floor(gA);
localFractionA = gA - indexA;
index0 = Geometry.isSmallRelative(1 - localFractionA) ? indexA + 2 : indexA + 1;
} else { // 1 < fractionA
indexA = n - 2;
index0 = n; // no original vertices in clone
}
if (fractionB < 0) {
indexB = 0;
index1 = -1; // no original vertices in clone
} else if (0 <= fractionB && fractionB <= 1) {
const gB = fractionB * numEdge;
indexB = Math.floor(gB);
localFractionB = gB - indexB;
index1 = Geometry.isSmallRelative(localFractionB) ? indexB - 1: indexB;
} else { // 1 < fractionB
indexB = n - 2;
index1 = n - 2; // last original vertex is not in clone
}
const result = LineString3d.create();
this.addResolvedPoint(indexA, localFractionA, result._points);
for (let index = indexA + 1; index <= indexB; index++) {
this._points.getPoint3dAtUncheckedPointIndex(index, LineString3d._workPointA);
result._points.push(LineString3d._workPointA);
}
if (!Geometry.isSmallRelative(localFractionB)) {
this.addResolvedPoint(indexB, localFractionB, result._points);
for (let index = index0; index <= index1; index++) {
if (this._points.isIndexValid(index)) {
this._points.getPoint3dAtUncheckedPointIndex(index, LineString3d._workPointA);
result._points.push(LineString3d._workPointA);
}
}
this.addResolvedPoint(indexB, localFractionB, result._points);
return result;
}
/** Return (if possible) a specific segment of the linestring */
Expand Down
4 changes: 4 additions & 0 deletions core/geometry/src/serialization/IModelJsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ export namespace IModelJson {
uKnots: [number];
/** Array of knots for the v direction bspline */
vKnots: [number];
/** optional flag for periodic data in the u parameter direction */
closedU?: boolean;
/** optional flag for periodic data in the v parameter direction */
closedV?: boolean;
}

/**
Expand Down
87 changes: 69 additions & 18 deletions core/geometry/src/test/curve/LineString3d.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { expect } from "chai";
import { ClipPlane } from "../../clipping/ClipPlane";
import { CurveLocationDetail } from "../../curve/CurveLocationDetail";
import { CurvePrimitive } from "../../curve/CurvePrimitive";
import { GeometryQuery } from "../../curve/GeometryQuery";
import { LineSegment3d } from "../../curve/LineSegment3d";
import { LineString3d } from "../../curve/LineString3d";
import { StrokeOptions } from "../../curve/StrokeOptions";
Expand All @@ -17,9 +18,68 @@ import { Point2d } from "../../geometry3d/Point2dVector2d";
import { Point3d, Vector3d } from "../../geometry3d/Point3dVector3d";
import { Transform } from "../../geometry3d/Transform";
import { Checker } from "../Checker";
import { GeometryCoreTestIO } from "../GeometryCoreTestIO";

/* eslint-disable no-console */

function exerciseClonePartialLineString3d(ck: Checker, allGeometry: GeometryQuery[], lsA: LineString3d, delta: Point3d) {
const expectValidResults = lsA.numPoints() > 1;
const yInc = 1.2 * lsA.range().yLength();
delta.x += 1.2 * lsA.range().xLength();
GeometryCoreTestIO.captureCloneGeometry(allGeometry, lsA, delta.x, delta.y = 0);
for (const extendFraction of [0.05, 0.721, 1.4, 3.96]) {
const cee0 = lsA.clonePartialCurve(-extendFraction, -extendFraction / 2); // does not contain [0,1]
const ce0 = lsA.clonePartialCurve(-extendFraction, 0); // contains [0]
const ce01 = lsA.clonePartialCurve(-extendFraction, 1); // contains [0,1]
const ce01e = lsA.clonePartialCurve(-extendFraction, 1 + extendFraction); // contains [0,1]
const c01e = lsA.clonePartialCurve(0, 1 + extendFraction); // contains [0,1]
const c1e = lsA.clonePartialCurve(1, 1 + extendFraction); // contains [1]
const c1ee = lsA.clonePartialCurve(1 + extendFraction / 2, 1 + extendFraction); // does not contain [0,1]
if (ck.testPointer(cee0) && expectValidResults) {
ck.testExactNumber(cee0.numPoints(), 2, "isolated pre-extension is a segment");
if (!lsA.isPhysicallyClosed)
ck.testCoordinate(cee0.curveLength(), (extendFraction - (extendFraction / 2)) * lsA.points[0].distance(lsA.points[1]), "isolated pre-extension length is fraction of first segment");
GeometryCoreTestIO.captureCloneGeometry(allGeometry, cee0, delta.x, delta.y += yInc);
}
if (ck.testPointer(ce0) && expectValidResults) {
ck.testExactNumber(ce0.numPoints(), 2, "pre-extension is a segment");
if (!lsA.isPhysicallyClosed)
ck.testCoordinate(ce0.curveLength(), extendFraction * lsA.points[0].distance(lsA.points[1]), "pre-extension length is fraction of first segment");
GeometryCoreTestIO.captureCloneGeometry(allGeometry, ce0, delta.x, delta.y += yInc);
}
if (ck.testPointer(ce01) && expectValidResults) {
ck.testExactNumber(ce01.numPoints(), lsA.numPoints(), "pre-extended linestring has same point count");
if (!lsA.isPhysicallyClosed)
ck.testCoordinate(ce01.curveLength(), lsA.curveLength() + extendFraction * lsA.points[0].distance(lsA.points[1]), "pre-extended linestring has expected length");
GeometryCoreTestIO.captureCloneGeometry(allGeometry, ce01, delta.x, delta.y += yInc);
}
if (ck.testPointer(ce01e) && expectValidResults) {
ck.testExactNumber(ce01e.numPoints(), lsA.numPoints(), "bi-extended linestring has same point count");
if (!lsA.isPhysicallyClosed)
ck.testCoordinate(ce01e.curveLength(), extendFraction * lsA.points[0].distance(lsA.points[1]) + lsA.curveLength() + extendFraction * lsA.points[lsA.numPoints() - 2].distance(lsA.points[lsA.numPoints() - 1]), "bi-extended linestring has expected length");
GeometryCoreTestIO.captureCloneGeometry(allGeometry, ce01e, delta.x, delta.y += yInc);
}
if (ck.testPointer(c01e) && expectValidResults) {
ck.testExactNumber(c01e.numPoints(), lsA.numPoints(), "post-extended linestring has same point count");
if (!lsA.isPhysicallyClosed)
ck.testCoordinate(c01e.curveLength(), lsA.curveLength() + extendFraction * lsA.points[lsA.numPoints() - 2].distance(lsA.points[lsA.numPoints() - 1]), "post-extended linestring has expected length");
GeometryCoreTestIO.captureCloneGeometry(allGeometry, c01e, delta.x, delta.y += yInc);
}
if (ck.testPointer(c1e) && expectValidResults) {
ck.testExactNumber(c1e.numPoints(), 2, "post-extension is a segment");
if (!lsA.isPhysicallyClosed)
ck.testCoordinate(c1e.curveLength(), extendFraction * lsA.points[lsA.numPoints() - 2].distance(lsA.points[lsA.numPoints() - 1]), "post-extension length is fraction of last segment");
GeometryCoreTestIO.captureCloneGeometry(allGeometry, c1e, delta.x, delta.y += yInc);
}
if (ck.testPointer(c1ee) && expectValidResults) {
ck.testExactNumber(c1ee.numPoints(), 2, "isolated post-extension is a segment");
if (!lsA.isPhysicallyClosed)
ck.testCoordinate(c1ee.curveLength(), (extendFraction - (extendFraction / 2)) * lsA.points[lsA.numPoints() - 2].distance(lsA.points[lsA.numPoints() - 1]), "isolated post-extension length is fraction of last segment");
GeometryCoreTestIO.captureCloneGeometry(allGeometry, c1ee, delta.x, delta.y += yInc);
}
}
}

function exerciseLineString3d(ck: Checker, lsA: LineString3d) {
const expectValidResults = lsA.numPoints() > 1;
const a = 4.2;
Expand All @@ -46,15 +106,14 @@ function exerciseLineString3d(ck: Checker, lsA: LineString3d) {
}
}
}

const splitFraction = 0.4203;
const partA = lsA.clonePartialCurve(0.0, splitFraction);
const partB = lsA.clonePartialCurve(1.0, splitFraction); // reversed to exercise more code. But length is absolute so it will add.
if (expectValidResults
&& ck.testPointer(partA, "forward partial") && partA
&& ck.testPointer(partA, "forward partial") && partB) {
if (ck.testPointer(partA, "forward partial") && ck.testPointer(partB, "backward partial"))
ck.testCoordinate(lsA.curveLength(), partA.curveLength() + partB.curveLength(), "Partial curves sum to length", lsA, partA, partB);
}
}

describe("LineString3d", () => {
it("HelloWorld", () => {
const ck = new Checker();
Expand All @@ -68,9 +127,7 @@ describe("LineString3d", () => {
const point150 = Point3d.create(1, 5, 0);
const lsA = LineString3d.create([point100, point420, point450, point150]);
exerciseLineString3d(ck, lsA);
const lsB = LineString3d.createRectangleXY(
Point3d.create(1, 1),
3, 2, true);
const lsB = LineString3d.createRectangleXY(Point3d.create(1, 1), 3, 2, true);
exerciseLineString3d(ck, lsB);
const lsC = LineString3d.create([point100]);
ck.testUndefined(lsC.quickUnitNormal(), "quickUnitNormal expected failure 1 point");
Expand All @@ -81,7 +138,6 @@ describe("LineString3d", () => {
const normalA = lsA.quickUnitNormal();
if (ck.testPointer(normalA, "quickUnitNormal") && normalA)
ck.testCoordinate(1.0, normalA.magnitude(), "unit normal magnitude");

ck.checkpoint("LineString3d.HelloWorld");
expect(ck.getNumErrors()).equals(0);
});
Expand Down Expand Up @@ -118,16 +174,11 @@ describe("LineString3d", () => {

it("clonePartial", () => {
const ck = new Checker();
// we know the length of this linestring is 'a'.
// make partials
const a = 5.0;
const interiorFraction = 0.24324;
const ls = LineString3d.createXY([Point2d.create(0, 1), Point2d.create(0.5 * a, 1), Point2d.create(a, 1)], 0);
ck.testExactNumber(3, ls.numPoints());
const ls1 = ls.clonePartialCurve(interiorFraction, 3.0)!;
const ls2 = ls.clonePartialCurve(-4, interiorFraction)!;
ck.testCoordinate(ls1.curveLength(), (1.0 - interiorFraction) * a, "clonePartial does not extrapolate up");
ck.testCoordinate(ls2.curveLength(), (interiorFraction) * a, "clonePartial does not extrapolate down");
const allGeometry: GeometryQuery[] = [];
const delta = Point3d.createZero();
const lsA = LineString3d.create([Point3d.create(1, 0, 0), Point3d.create(4, 2, 0), Point3d.create(4, 5, 0), Point3d.create(1, 5, 0)]);
exerciseClonePartialLineString3d(ck, allGeometry, lsA, delta);
GeometryCoreTestIO.saveGeometry(allGeometry, "LineString3d", "ClonePartial");
expect(ck.getNumErrors()).equals(0);
});

Expand Down