/
LineString3d.ts
1553 lines (1544 loc) · 65.4 KB
/
LineString3d.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Curve
*/
import { Clipper } from "../clipping/ClipUtils";
import { AxisOrder, BeJSONFunctions, Geometry, PlaneAltitudeEvaluator } from "../Geometry";
import { Angle } from "../geometry3d/Angle";
import { GeometryHandler, IStrokeHandler } from "../geometry3d/GeometryHandler";
import { GrowableFloat64Array } from "../geometry3d/GrowableFloat64Array";
import { GrowableXYArray } from "../geometry3d/GrowableXYArray";
import { GrowableXYZArray } from "../geometry3d/GrowableXYZArray";
import { MultiLineStringDataVariant } from "../geometry3d/IndexedXYZCollection";
import { Matrix3d } from "../geometry3d/Matrix3d";
import { Plane3dByOriginAndUnitNormal } from "../geometry3d/Plane3dByOriginAndUnitNormal";
import { Plane3dByOriginAndVectors } from "../geometry3d/Plane3dByOriginAndVectors";
import { Point3d, Vector3d } from "../geometry3d/Point3dVector3d";
import { PointStreamGrowableXYZArrayCollector, VariantPointDataStream } from "../geometry3d/PointStreaming";
import { Range1d, Range3d } from "../geometry3d/Range";
import { Ray3d } from "../geometry3d/Ray3d";
import { Transform } from "../geometry3d/Transform";
import { XAndY, XYZProps } from "../geometry3d/XYZProps";
import { CurveExtendOptions, VariantCurveExtendParameter } from "./CurveExtendMode";
import { CurveIntervalRole, CurveLocationDetail, CurveSearchStatus } from "./CurveLocationDetail";
import { AnnounceNumberNumberCurvePrimitive, CurvePrimitive } from "./CurvePrimitive";
import { GeometryQuery } from "./GeometryQuery";
import { PlaneAltitudeRangeContext } from "./internalContexts/PlaneAltitudeRangeContext";
import { LineSegment3d } from "./LineSegment3d";
import { OffsetOptions } from "./OffsetOptions";
import { StrokeCountMap } from "./Query/StrokeCountMap";
import { StrokeOptions } from "./StrokeOptions";
/**
* Starting with baseIndex and moving index by stepDirection:
* If the vector from baseIndex to baseIndex +1 crossed with vectorA can be normalized, accumulate it (scaled) to normal.
* Return when successful.
* (Do nothing if everything is parallel through limits of the array)
*/
function accumulateGoodUnitPerpendicular(
points: GrowableXYZArray,
vectorA: Vector3d,
baseIndex: number,
stepDirection: number,
weight: number,
normal: Vector3d,
workVector: Vector3d,
): boolean {
const n = points.length;
if (stepDirection > 0) {
for (let i = baseIndex; i + 1 < n; i++) {
points.vectorIndexIndex(i, i + 1, workVector);
vectorA.crossProduct(workVector, workVector);
if (workVector.normalizeInPlace()) {
normal.addScaledInPlace(workVector, weight);
return true;
}
}
} else {
if (baseIndex + 1 >= n)
baseIndex = n - 2;
for (let i = baseIndex; i >= 0; i--) {
points.vectorIndexIndex(i, i + 1, workVector);
workVector.crossProduct(vectorA, workVector);
if (workVector.normalizeInPlace()) {
normal.addScaledInPlace(workVector, weight);
return true;
}
}
}
return false;
}
/**
* * A LineString3d (sometimes called a PolyLine) is a sequence of xyz coordinates that are to be joined by line
* segments.
* * The point coordinates are stored in a GrowableXYZArray, not as full point objects.
* * The parameterization of "fraction along" is
* * In a linestring with `N` segments (i.e. `N+1` points), each segment (regardless of physical length) occupies
* the same fraction (1/N) of the 0-to-1 fraction space.
* * Within segment `i`, the fraction interval `i/N` to `(i+1)/N` is mapped proportionally to the segment
* * Note that this `fraction` is therefore NOT fraction of true distance along.
* * Use `moveSignedDistanceFromFraction` to do true-length evaluations.
* @public
*/
export class LineString3d extends CurvePrimitive implements BeJSONFunctions {
/** String name for schema properties */
public readonly curvePrimitiveType = "lineString";
private static _workPointA = Point3d.create();
private static _workPointB = Point3d.create();
private static _workPointC = Point3d.create();
private static _workRay = Ray3d.createXAxis();
/** test if `other` is an instance of `LineString3d` */
public isSameGeometryClass(other: GeometryQuery): boolean {
return other instanceof LineString3d;
}
/** A LineString3d extends along its first and final segments. */
public override get isExtensibleFractionSpace(): boolean {
return true;
}
private _points: GrowableXYZArray;
private _fractions?: GrowableFloat64Array;
private _uvParams?: GrowableXYArray;
private _derivatives?: GrowableXYZArray;
private _surfaceNormals?: GrowableXYZArray;
private _pointIndices?: GrowableFloat64Array;
private _uvIndices?: GrowableFloat64Array;
private _normalIndices?: GrowableFloat64Array;
/** Return the points array (cloned). */
public get points(): Point3d[] {
return this._points.getPoint3dArray();
}
/** Return (reference to) point data in packed GrowableXYZArray. */
public get packedPoints(): GrowableXYZArray {
return this._points;
}
/**
* Return array of fraction parameters.
* * These Are only present during certain constructions such as faceting.
* * When present, these fractions are fractions of some other curve being stroked, and are NOT related to the
* linestring fraction parameters.
*/
public get fractions(): GrowableFloat64Array | undefined {
return this._fractions;
}
/** Return the (optional) array of derivatives. These Are only present during certain constructions such as faceting. */
public get packedDerivatives(): GrowableXYZArray | undefined {
return this._derivatives;
}
/** Return the (optional) array of uv params. These Are only present during certain constructions such as faceting. */
public get packedUVParams(): GrowableXYArray | undefined {
return this._uvParams;
}
/** Return the (optional) array of surface normals. These Are only present during certain constructions such as faceting. */
public get packedSurfaceNormals(): GrowableXYZArray | undefined {
return this._surfaceNormals;
}
/** Return the (optional) array of normal indices. These Are only present during certain constructions such as faceting. */
public get normalIndices(): GrowableFloat64Array | undefined {
return this._normalIndices;
}
/** Return the (optional) array of param indices. These Are only present during certain constructions such as faceting. */
public get paramIndices(): GrowableFloat64Array | undefined {
return this._uvIndices;
}
/** Return the (optional) array of point indices. These Are only present during certain constructions such as faceting. */
public get pointIndices(): GrowableFloat64Array | undefined {
return this._pointIndices;
}
private constructor(points?: GrowableXYZArray) {
super();
if (points)
this._points = points;
else
this._points = new GrowableXYZArray();
}
/** Clone this linestring and apply the transform to the clone points. */
public cloneTransformed(transform: Transform): LineString3d { // we know tryTransformInPlace succeeds.
const c = this.clone();
c.tryTransformInPlace(transform);
return c;
}
/**
* Create a linestring, using flex length arg list and any typical combination of points such as
* Point3d, Point2d, `[1,2,3]', array of any of those, or GrowableXYZArray
*/
public static create(...points: any[]): LineString3d {
const result = new LineString3d();
result.addPoints(points);
return result;
}
/** Create a linestring, capturing the given GrowableXYZArray as the points. */
public static createCapture(points: GrowableXYZArray): LineString3d {
return new LineString3d(points);
}
/** Create a linestring from `XAndY` points, with a specified z applied to all. */
public static createXY(points: XAndY[], z: number, enforceClosure: boolean = false): LineString3d {
const result = new LineString3d();
const xyz = result._points;
for (const xy of points) {
xyz.pushXYZ(xy.x, xy.y, z);
}
if (enforceClosure && points.length > 1) {
const distance = xyz.distanceIndexIndex(0, xyz.length - 1);
if (distance !== undefined && distance !== 0.0) {
if (Geometry.isSameCoordinate(0, distance)) {
xyz.pop(); // nonzero but small distance -- to be replaced by point 0 exactly.
const xyzA = xyz.front();
xyz.push(xyzA!);
}
}
}
return result;
}
/**
* Add copies of points to the linestring.
* Valid inputs are:
* * a Point2d
* * a point3d
* * An array of 2 doubles
* * An array of 3 doubles
* * A GrowableXYZArray
* * An array of any of the above
*/
public addPoints(...points: any[]) {
this._points.pushFrom(points);
}
/** Add points accessed by index in a GrowableXYZArray, with a specified index step. */
public addSteppedPoints(source: GrowableXYZArray, pointIndex0: number, step: number, numAdd: number) {
this._points.addSteppedPoints(source, pointIndex0, step, numAdd);
}
/**
* Add a point to the linestring.
* @param point
*/
public addPoint(point: Point3d) {
this._points.push(point);
}
/**
* Add a point to the linestring.
* @param point
*/
public addPointXYZ(x: number, y: number, z: number = 0) {
this._points.pushXYZ(x, y, z);
}
/**
* Append a fraction to the fractions array.
* @param fraction
*/
public addFraction(fraction: number) {
if (!this._fractions)
this._fractions = new GrowableFloat64Array();
this._fractions.push(fraction);
}
/** Ensure that the fraction array exists with no fractions but at least the capacity of the point array. */
public ensureEmptyFractions(): GrowableFloat64Array {
const n = this.numPoints();
if (!this._fractions) {
this._fractions = new GrowableFloat64Array(n);
return this._fractions;
}
this._fractions.clear();
this._fractions.ensureCapacity(n);
return this._fractions;
}
/** Ensure that the parameter array exists with no points but at least the capacity of the point array. */
public ensureEmptyUVParams(): GrowableXYArray {
const n = this.numPoints();
if (!this._uvParams) {
this._uvParams = new GrowableXYArray(n);
return this._uvParams;
}
this._uvParams.clear();
this._uvParams.ensureCapacity(n);
return this._uvParams;
}
/** Ensure that the surfaceNormals array exists with no points but at least the capacity of the point array. */
public ensureEmptySurfaceNormals(): GrowableXYZArray {
const n = this.numPoints();
if (!this._surfaceNormals) {
this._surfaceNormals = new GrowableXYZArray(n);
return this._surfaceNormals;
}
this._surfaceNormals.clear();
this._surfaceNormals.ensureCapacity(n);
return this._surfaceNormals;
}
/** Ensure that the surfaceNormals array exists with no points but at least the capacity of the point array. */
public ensureEmptyDerivatives(): GrowableXYZArray {
const n = this.numPoints();
if (!this._derivatives) {
this._derivatives = new GrowableXYZArray(n);
return this._derivatives;
}
this._derivatives.clear();
this._derivatives.ensureCapacity(n);
return this._derivatives;
}
/** Ensure that the surfaceNormals array exists with no points but at least the capacity of the point array. */
public ensureEmptyNormalIndices(): GrowableFloat64Array {
const n = this.numPoints();
if (!this._normalIndices) {
this._normalIndices = new GrowableFloat64Array(n);
return this._normalIndices;
}
this._normalIndices.clear();
this._normalIndices.ensureCapacity(n);
return this._normalIndices;
}
/** Ensure that the surfaceNormals array exists with no points but at least the capacity of the point array. */
public ensureEmptyUVIndices(): GrowableFloat64Array {
const n = this.numPoints();
if (!this._uvIndices) {
this._uvIndices = new GrowableFloat64Array(n);
return this._uvIndices;
}
this._uvIndices.clear();
this._uvIndices.ensureCapacity(n);
return this._uvIndices;
}
/** Ensure that the surfaceNormals array exists with no points but at least the capacity of the point array. */
public ensureEmptyPointIndices(): GrowableFloat64Array {
const n = this.numPoints();
if (!this._pointIndices) {
this._pointIndices = new GrowableFloat64Array(n);
return this._pointIndices;
}
this._pointIndices.clear();
this._pointIndices.ensureCapacity(n);
return this._pointIndices;
}
/**
* Append a uv coordinate to the uvParams array
* @param uv
*/
public addUVParam(uvParam: XAndY) {
if (!this._uvParams)
this._uvParams = new GrowableXYArray();
this._uvParams.pushXY(uvParam.x, uvParam.y);
}
/**
* Append a uv coordinate to the uvParams array
* @param uv
*/
public addUVParamAsUV(u: number, v: number) {
if (!this._uvParams)
this._uvParams = new GrowableXYArray();
this._uvParams.pushXY(u, v);
}
/**
* Append a derivative to the derivative array
* @param vector
*/
public addDerivative(vector: Vector3d) {
if (!this._derivatives)
this._derivatives = new GrowableXYZArray();
this._derivatives.push(vector);
}
/**
* Append a surface normal to the surface normal array.
* @param vector
*/
public addSurfaceNormal(vector: Vector3d) {
if (!this._surfaceNormals)
this._surfaceNormals = new GrowableXYZArray();
this._surfaceNormals.push(vector);
}
/** If the linestring is not already closed, add a closure point. */
public addClosurePoint() {
const distance = this._points.distanceIndexIndex(0, this._points.length - 1);
if (distance !== undefined && !Geometry.isSameCoordinate(distance, 0))
this._points.pushWrap(1);
}
/** Eliminate (but do not return!!) the final point of the linestring */
public popPoint() {
this._points.pop();
}
/** Compute `uvParams` array as (xy parts of) a linear transform of the xyz coordinates */
public computeUVFromXYZTransform(transform: Transform) {
this._uvParams = GrowableXYArray.createFromGrowableXYZArray(this._points, transform);
}
/**
* Create the linestring for a rectangle parallel to the xy plane.
* * The z coordinate from `point0` is used for all points.
* * `ax` and `ay` are signed.
* * The point sequence is:
* * Start at `point0`
* * move by (signed !) `ax` in the x direction.
* * move by (signed !) `ay` in the y direction.
* * move by (signed !) negative `ax` in the x direction.
* * move by (signed !) negative `ay` in the y direction.
* * (this returns to `point0`)
*/
public static createRectangleXY(point0: Point3d, ax: number, ay: number, closed: boolean = true): LineString3d {
const ls = LineString3d.create();
const x0 = point0.x;
const x1 = point0.x + ax;
const y0 = point0.y;
const y1 = point0.y + ay;
const z = point0.z;
ls.addPointXYZ(x0, y0, z);
ls.addPointXYZ(x1, y0, z);
ls.addPointXYZ(x1, y1, z);
ls.addPointXYZ(x0, y1, z);
if (closed)
ls.addClosurePoint();
return ls;
}
/**
* Create a regular polygon centered
* @param center center of the polygon.
* @param edgeCount number of edges.
* @param radius distance to vertex or edge (see `radiusToVertices`)
* @param radiusToVertices true if polygon is inscribed in circle (radius measured to vertices); false if polygon
* is outside circle (radius to edges)
*/
public static createRegularPolygonXY(
center: Point3d, edgeCount: number, radius: number, radiusToVertices: boolean = true,
): LineString3d {
if (edgeCount < 3)
edgeCount = 3;
const ls = LineString3d.create();
const i0 = radiusToVertices ? 0 : -1; // offset to make first vector (radius,0,0)
const radiansStep = Math.PI / edgeCount;
let c;
let s;
let radians;
if (!radiusToVertices)
radius = radius / Math.cos(radiansStep);
for (let i = 0; i < edgeCount; i++) {
radians = (i0 + 2 * i) * radiansStep;
c = Angle.cleanupTrigValue(Math.cos(radians));
s = Angle.cleanupTrigValue(Math.sin(radians));
ls.addPointXYZ(center.x + radius * c, center.y + radius * s, center.z);
}
ls.addClosurePoint();
return ls;
}
/**
* Copy coordinate data from another linestring.
* * The copied content is:
* * points
* * derivatives (if present)
* * fractions (if present)
* * surfaceNormals (if present)
* * uvParams (if present)
* @param other
*/
public setFrom(other: LineString3d) {
// ugly -- "clone" methods are inconsistent about 'reuse' and 'result' parameter . . .
this._points = other._points.clone(this._points);
if (other._derivatives)
this._derivatives = other._derivatives.clone(this._derivatives);
else
this._derivatives = undefined;
if (other._fractions)
this._fractions = other._fractions.clone(false);
else this._fractions = undefined;
if (other._surfaceNormals)
this._surfaceNormals = other._surfaceNormals.clone(this._surfaceNormals);
else
this._surfaceNormals = undefined;
if (other._uvParams)
this._uvParams = other._uvParams.clone();
else
this._uvParams = undefined;
}
/** Create a linestring from an array of points. */
public static createPoints(points: Point3d[]): LineString3d {
const ls = new LineString3d();
let point;
for (point of points)
ls._points.push(point);
return ls;
}
/** Create a linestring, taking points at specified indices from an array of points. */
public static createIndexedPoints(points: Point3d[], index: number[], addClosure: boolean = false): LineString3d {
const ls = new LineString3d();
for (const i of index)
ls._points.push(points[i]); // no clone needed -- we know this reformats to packed array.
if (addClosure && index.length > 1)
ls._points.push(points[index[0]]);
return ls;
}
/** Create a LineString3d from xyz coordinates packed in a Float64Array */
public static createFloat64Array(xyzData: Float64Array): LineString3d {
const ls = new LineString3d();
for (let i = 0; i + 3 <= xyzData.length; i += 3)
ls._points.push(Point3d.create(xyzData[i], xyzData[i + 1], xyzData[i + 2]));
return ls;
}
/** Return a clone of this linestring. */
public clone(): LineString3d {
const retVal = new LineString3d();
retVal.setFrom(this);
return retVal;
}
/**
* Set point coordinates from a json array, e.g. `[[1,2,3],[4,5,6] . . .]`
* * The `json` parameter must be an array.
* * Each member `i` of the array is converted to a point with `Point3d.fromJSON(json[i]`)
*/
public setFromJSON(json?: any) {
this._points.clear();
if (Array.isArray(json)) {
let xyz;
for (xyz of json)
this._points.push(Point3d.fromJSON(xyz));
}
}
/**
* Convert an LineString3d to a JSON object.
* * The returned object is an array of arrays of x,y,z coordinates, `[[x,y,z],...[x,y,z]]`
*/
public toJSON(): XYZProps[] {
const value = [];
let i = 0;
while (this._points.isIndexValid(i)) {
value.push(this._points.getPoint3dAtUncheckedPointIndex(i).toJSON());
i++;
}
return value;
}
/**
* Construct a new linestring.
* * See `LineString3d.setFromJSON ()` for remarks on `json` structure.
*/
public static fromJSON(json?: any): LineString3d {
const ls = new LineString3d(); ls.setFromJSON(json); return ls;
}
/**
* Evaluate a point a fractional position along this linestring.
* * See `LineString3d` class comments for description of how fraction relates to the linestring points.
* @param fraction fractional position
* @param result optional result
*/
public fractionToPoint(fraction: number, result?: Point3d): Point3d {
const n = this._points.length;
if (n === 0)
return Point3d.createZero();
if (n === 1)
return Point3d.createFrom(this._points.getPoint3dAtUncheckedPointIndex(0), result);
const df = 1.0 / (n - 1);
if (fraction <= df)
return this._points.interpolate(0, fraction / df, 1, result)!;
if (fraction + df >= 1.0)
return this._points.interpolate(n - 1, (1.0 - fraction) / df, n - 2, result)!;
const index0 = Math.floor(fraction / df);
return this._points.interpolate(index0, (fraction - index0 * df) / df, index0 + 1, result)!;
}
/**
* Evaluate a point a fractional position and derivative with respect to fraction along this linestring.
* * See `LineString3d` class comments for description of how fraction relates to the linestring points.
* * At interior corners and the end point, the left derivative is returned; at the start point, the right derivative is returned.
* @param fraction fractional position
* @param result optional result
*/
public fractionToPointAndDerivative(fraction: number, result?: Ray3d): Ray3d {
result = result ? result : Ray3d.createZero();
const n = this._points.length;
if (n <= 1) {
result.direction.setZero();
if (n === 1)
result.origin.setFrom(this._points.getPoint3dAtUncheckedPointIndex(0));
else result.origin.setZero();
return result;
}
const numSegment = n - 1;
const df = 1.0 / numSegment;
if (fraction <= df) {
result = result ? result : Ray3d.createZero();
this._points.interpolate(0, fraction / df, 1, result.origin);
this._points.vectorIndexIndex(0, 1, result.direction);
result.direction.scaleInPlace(1.0 / df);
return result;
}
if (fraction + df >= 1.0) {
result = result ? result : Ray3d.createZero();
this._points.interpolate(n - 2, 1.0 - (1.0 - fraction) / df, n - 1, result.origin);
this._points.vectorIndexIndex(n - 2, n - 1, result.direction);
result.direction.scaleInPlace(1.0 / df);
return result;
}
/* true interior point */
result = result ? result : Ray3d.createZero();
const index0 = Math.floor(fraction / df);
const localFraction = (fraction - index0 * df) / df;
this._points.interpolate(index0, localFraction, index0 + 1, result.origin);
this._points.vectorIndexIndex(index0, index0 + 1, result.direction);
result.direction.scaleInPlace(1.0 / df);
return result;
}
/** Return point and derivative at fraction, with 000 second derivative. */
public fractionToPointAnd2Derivatives(fraction: number, result?: Plane3dByOriginAndVectors): Plane3dByOriginAndVectors {
const ray = this.fractionToPointAndDerivative(fraction);
result = Plane3dByOriginAndVectors.createCapture(ray.origin, ray.direction, Vector3d.createZero(), result);
return result;
}
/**
* Convert a segment index and local fraction to a global linestring fraction.
* @param index index of segment being evaluated
* @param localFraction local fraction in [0,1] within the segment
* @param numSegment number N of segments in the linestring
* @return global fraction f in [0,1] such that the segment is parameterized by index/N <= f <= (index+1)/N.
*/
public static mapLocalToGlobalFraction(index: number, localFraction: number, numSegment: number): number {
if (numSegment < 1)
return 0.0;
return (index + localFraction) / numSegment;
}
/**
* Convert a segment index and local fraction to a global linestring fraction.
* @param index index of segment being evaluated
* @param localFraction local fraction in [0,1] within the segment
* @return global fraction f in [0,1] such that the segment is parameterized by index/N <= f <= (index+1)/N.
*/
public segmentIndexAndLocalFractionToGlobalFraction(index: number, localFraction: number): number {
return LineString3d.mapLocalToGlobalFraction(index, localFraction, this._points.length - 1);
}
/**
* Convert a global fraction to a segment index and local fraction.
* @param globalFraction a fraction f in [0,1] in the linestring parameterization, where the i_th segment
* (0 <= i < N) is parameterized by i/N <= f <= (i+1)/N.
* @returns segment index and local fraction
*/
public static mapGlobalToLocalFraction(globalFraction: number, numSegment: number): { index: number, fraction: number } {
if (numSegment < 1)
return { index: 0, fraction: 0.0 };
const scaledGlobalFraction = globalFraction * numSegment;
let segmentIndex: number;
if (globalFraction < 0)
segmentIndex = 0;
else if (globalFraction > 1)
segmentIndex = numSegment - 1;
else // globalFraction in [0,1]
segmentIndex = Math.floor(scaledGlobalFraction);
return { index: segmentIndex, fraction: scaledGlobalFraction - segmentIndex };
}
/**
* Convert a global linestring fraction to a segment index and local fraction.
* @param globalFraction a fraction f in [0,1] in the linestring parameterization, where the i_th segment
* (0 <= i < N) is parameterized by i/N <= f <= (i+1)/N.
* @param numSegment number N of segments in the linestring
* @returns segment index and local fraction
*/
public globalFractionToSegmentIndexAndLocalFraction(globalFraction: number): { index: number, fraction: number } {
return LineString3d.mapGlobalToLocalFraction(globalFraction, this._points.length - 1);
}
/** Return a frenet frame, using nearby points to estimate a plane. */
public override fractionToFrenetFrame(fraction: number, result?: Transform): Transform {
const n = this._points.length;
if (n <= 1) {
if (n === 1)
return Transform.createTranslation(this._points.getPoint3dAtUncheckedPointIndex(0), result);
return Transform.createIdentity(result);
}
if (n === 2)
return Transform.createRefs(
this._points.interpolate(0, fraction, 1),
Matrix3d.createRigidHeadsUp(this._points.vectorIndexIndex(0, 1)!, AxisOrder.XYZ));
/** 3 or more points. */
const numSegment = n - 1;
const df = 1.0 / numSegment;
let baseIndex = 0;
let localFraction = 0;
if (fraction <= df) {
localFraction = fraction / df;
baseIndex = 0;
} else if (fraction + df >= 1.0) {
baseIndex = n - 2;
localFraction = 1.0 - (1.0 - fraction) / df;
} else {
baseIndex = Math.floor(fraction / df);
localFraction = fraction * numSegment - baseIndex;
}
const origin = this._points.interpolate(baseIndex, localFraction, baseIndex + 1)!;
const vectorA = this._points.vectorIndexIndex(baseIndex, baseIndex + 1)!;
// tricky stuff to handle colinear points. But if vectorA is zero it is still a mess . ..
const normal = Vector3d.create();
const workVector = Vector3d.create();
if (baseIndex === 0) { // only look forward
accumulateGoodUnitPerpendicular(this._points, vectorA, baseIndex + 1, 1, 1.0, normal, workVector);
} else if (baseIndex + 2 >= n) { // only look back
accumulateGoodUnitPerpendicular(this._points, vectorA, baseIndex - 1, -1, 1.0, normal, workVector);
} else {
accumulateGoodUnitPerpendicular(this._points, vectorA, baseIndex - 1, -1, (1.0 - localFraction), normal, workVector);
accumulateGoodUnitPerpendicular(this._points, vectorA, baseIndex + 1, 1, (localFraction), normal, workVector);
}
const matrix = Matrix3d.createRigidFromColumns(normal, vectorA, AxisOrder.ZXY);
if (matrix)
return Transform.createOriginAndMatrix(origin, matrix, result);
return Transform.createTranslation(origin, result);
}
/** Evaluate the start point of the linestring. */
public override startPoint() {
if (this._points.length === 0)
return Point3d.createZero();
return this._points.getPoint3dAtUncheckedPointIndex(0);
}
/** If i is a valid index, return that point. */
public pointAt(i: number, result?: Point3d): Point3d | undefined {
if (this._points.isIndexValid(i))
return this._points.getPoint3dAtUncheckedPointIndex(i, result);
return undefined;
}
/** If i and j are both valid indices, return the vector from point i to point j */
public vectorBetween(i: number, j: number, result?: Vector3d): Vector3d | undefined {
return this._points.vectorIndexIndex(i, j, result);
}
/** If i is a valid index, return that stored derivative vector. */
public derivativeAt(i: number, result?: Vector3d): Vector3d | undefined {
if (this._derivatives && this._derivatives.isIndexValid(i))
return this._derivatives.getVector3dAtCheckedVectorIndex(i, result);
return undefined;
}
/** If i is a valid index, return that stored surfaceNormal vector. */
public surfaceNormalAt(i: number, result?: Vector3d): Vector3d | undefined {
if (this._surfaceNormals && this._surfaceNormals.isIndexValid(i))
return this._surfaceNormals.getVector3dAtCheckedVectorIndex(i, result);
return undefined;
}
/** Return the number of points in this linestring. */
public numPoints(): number {
return this._points.length;
}
/** Return the number of edges in this linestring. */
public numEdges(): number {
return this._points.length > 0 ? this._points.length - 1 : 0;
}
/** Evaluate the end point of the linestring. */
public override endPoint() {
if (this._points.length === 0)
return Point3d.createZero();
return this._points.getPoint3dAtUncheckedPointIndex(this._points.length - 1);
}
/** Reverse the points within the linestring. */
public reverseInPlace(): void {
if (this._points.length >= 2) {
this._points.reverseInPlace();
if (this._uvParams)
this._uvParams.reverseInPlace();
}
}
/** Apply `transform` to each point of this linestring. */
public tryTransformInPlace(transform: Transform): boolean {
this._points.multiplyTransformInPlace(transform);
if (this._derivatives)
this._derivatives.multiplyMatrix3dInPlace(transform.matrix);
if (this._surfaceNormals)
this._surfaceNormals.multiplyAndRenormalizeMatrix3dInverseTransposeInPlace(transform.matrix);
return true;
}
/** Sum the lengths of segments within the linestring */
public override curveLength(): number {
return this._points.sumLengths();
}
/** Sum the lengths of segments between fractional positions on a linestring. */
public override curveLengthBetweenFractions(fraction0: number, fraction1: number): number {
const numSegments = this._points.length - 1;
if (fraction1 === fraction0 || numSegments < 1)
return 0.0;
if (fraction1 < fraction0)
return this.curveLengthBetweenFractions(fraction1, fraction0);
const scaledFraction0 = fraction0 * numSegments;
const scaledFraction1 = fraction1 * numSegments;
const index0 = Math.max(1, Math.ceil(scaledFraction0));
const index1 = Math.min(Math.floor(scaledFraction1), numSegments - 1);
const localFraction0 = index0 - scaledFraction0;
const localFraction1 = scaledFraction1 - index1;
if (index0 > index1) {
// the interval is entirely within a single segment
return Math.abs(scaledFraction1 - scaledFraction0) * this._points.distanceIndexIndex(index0 - 1, index0)!;
} else {
// there is leading partial interval, 0 or more complete segments, and a trailing partial interval.
// (either or both partial may be zero length)
let sum = localFraction0 * this._points.distanceIndexIndex(index0 - 1, index0)!
+ localFraction1 * (this._points.distanceIndexIndex(index1, index1 + 1))!;
for (let i = index0; i < index1; i++)
sum += this._points.distanceIndexIndex(i, i + 1)!;
return sum;
}
}
/** Compute the range of points between fractional positions on the linestring. */
public override rangeBetweenFractions(fraction0: number, fraction1: number, transform?: Transform): Range3d {
const range = Range3d.create();
if (this.points.length < 1)
return range;
if (fraction1 < fraction0)
return this.rangeBetweenFractions(fraction1, fraction0, transform);
const numSegments = this._points.length - 1;
const scaledFraction0 = fraction0 * numSegments;
const index0 = Math.max(0, Math.floor(scaledFraction0));
const localFraction0 = scaledFraction0 - index0;
const workPoint = Point3d.create();
this._points.interpolate(index0, localFraction0, index0 + 1, workPoint);
range.extendPoint(workPoint, transform);
if (fraction1 === fraction0)
return range; // 1-point range
const scaledFraction1 = fraction1 * numSegments;
const index1 = Math.min(Math.floor(scaledFraction1), numSegments - 1);
const localFraction1 = scaledFraction1 - index1;
this._points.interpolate(index1, localFraction1, index1 + 1, workPoint);
range.extendPoint(workPoint, transform);
for (let i = index0 + 1; i <= index1; i++) {
this._points.getPoint3dAtUncheckedPointIndex(i, workPoint);
range.extendPoint(workPoint, transform);
}
return range;
}
/**
* * Implementation of `CurvePrimitive.moveSignedDistanceFromFraction`. (see comments there!)
* * Find the segment that contains the start fraction
* * Move point-by-point from that position to the start or end (respectively for negative or positive signedDistance)
* * Optionally extrapolate
* @param startFraction
* @param signedDistance
* @param allowExtension
* @param result
*/
public override moveSignedDistanceFromFraction(
startFraction: number, signedDistance: number, allowExtension: false, result?: CurveLocationDetail,
): CurveLocationDetail {
const numSegments = this._points.length - 1;
const scaledFraction = startFraction * numSegments;
let leftPointIndex = Geometry.restrictToInterval(Math.floor(scaledFraction), 0, numSegments - 1); // lower point index on active segment.
const localFraction = scaledFraction - leftPointIndex;
const point0 = this._points.interpolate(leftPointIndex, localFraction, leftPointIndex + 1, LineString3d._workPointA)!;
const point1 = LineString3d._workPointB;
const context = new MoveByDistanceContext(point0, startFraction, signedDistance);
if (signedDistance > 0.0) {
for (; leftPointIndex <= numSegments;) {
leftPointIndex++;
this._points.getPoint3dAtCheckedPointIndex(leftPointIndex, point1);
if (context.announcePoint(point1, leftPointIndex / numSegments))
return CurveLocationDetail.createCurveFractionPointDistanceCurveSearchStatus(
this, context.fraction0, context.point0, signedDistance, CurveSearchStatus.success, result,
);
}
// fall through for extrapolation from final segment
if (allowExtension)
context.announceExtrapolation(this._points, numSegments - 1, numSegments,
(numSegments - 1) / numSegments, 1.0);
return CurveLocationDetail.createCurveFractionPointDistanceCurveSearchStatus(
this, context.fraction0, context.point0, signedDistance, context.distanceStatus(), result,
);
} else { // (moving backwards)
if (localFraction <= 0.0)
leftPointIndex--;
for (; leftPointIndex >= 0; leftPointIndex--) {
this._points.getPoint3dAtCheckedPointIndex(leftPointIndex, point1);
if (context.announcePoint(point1, leftPointIndex / numSegments))
return CurveLocationDetail.createCurveFractionPointDistanceCurveSearchStatus(
this, context.fraction0, context.point0, signedDistance, CurveSearchStatus.success, result,
);
}
// fall through for backward extrapolation from initial segment
if (allowExtension)
context.announceExtrapolation(this._points, 1, 0, 1.0 / numSegments, 0.0);
return CurveLocationDetail.createCurveFractionPointDistanceCurveSearchStatus(
this, context.fraction0, context.point0, -context.distance0, context.distanceStatus(), result,
);
}
}
/** Sum lengths of segments in the linestring. (This is a true length.) */
public quickLength(): number { return this.curveLength(); }
/**
* Compute and normalize cross product among 3 points on the linestring.
* * "any" 3 points are acceptable -- no test for positive overall sense.
* * This is appropriate for polygon known to be convex.
* * use points spread at index step n/3, hopefully avoiding colinear points.
* * If that fails, try points 012
* @param result computed normal.
*/
public quickUnitNormal(result?: Vector3d): Vector3d | undefined {
let step = Math.floor(this._points.length / 3);
if (step < 1)
step = 1;
result = this._points.crossProductIndexIndexIndex(0, step, step + step);
if (result && result.normalizeInPlace())
return result;
return undefined;
}
/** Find the point on the linestring (including its segment interiors) that is closest to spacePoint. */
public override closestPoint(
spacePoint: Point3d, extend: VariantCurveExtendParameter, result?: CurveLocationDetail,
): CurveLocationDetail {
result = CurveLocationDetail.create(this, result);
const extend0 = CurveExtendOptions.resolveVariantCurveExtendParameterToCurveExtendMode(extend, 0);
const extend1 = CurveExtendOptions.resolveVariantCurveExtendParameterToCurveExtendMode(extend, 1);
const numPoints = this._points.length;
if (numPoints > 0) {
const lastIndex = numPoints - 1;
result.setFP(1.0, this._points.getPoint3dAtUncheckedPointIndex(lastIndex), undefined);
result.setDistanceTo(spacePoint);
if (numPoints > 1) {
let segmentFraction = 0;
let d = 0;
for (let i = 1; i < numPoints; i++) {
segmentFraction = spacePoint.fractionOfProjectionToLine(
this._points.getPoint3dAtUncheckedPointIndex(i - 1), this._points.getPoint3dAtUncheckedPointIndex(i),
);
if (segmentFraction < 0) {
if (!extend0 || i > 1)
segmentFraction = 0.0;
} else if (segmentFraction > 1.0) {
if (!extend1 || i < lastIndex)
segmentFraction = 1.0;
}
this._points.getPoint3dAtUncheckedPointIndex(i - 1)
.interpolate(segmentFraction, this._points.getPoint3dAtUncheckedPointIndex(i), result.pointQ);
d = result.pointQ.distance(spacePoint);
if (d < result.a) {
result.setFP(
this.segmentIndexAndLocalFractionToGlobalFraction(i - 1, segmentFraction), result.pointQ, undefined, d,
);
}
}
}
}
return result;
}
/** Test if all points of the linestring are in a plane. */
public isInPlane(plane: Plane3dByOriginAndUnitNormal): boolean {
return this._points.isCloseToPlane(plane, Geometry.smallMetricDistance);
}
/** Push a hit, fixing up the prior entry if needed. */
private static pushVertexHit(
result: CurveLocationDetail[], counter: number, cp: CurvePrimitive, fraction: number, point: Point3d,
): void {
const detail = CurveLocationDetail.createCurveFractionPoint(cp, fraction, point);
result.push(detail);
if (counter === 0) {
detail.setIntervalRole(CurveIntervalRole.isolatedAtVertex);
} else if (counter === 1) { // last entry must be isolatedAtVertex !!!
result[result.length - 2].setIntervalRole(CurveIntervalRole.intervalStart);
detail.setIntervalRole(CurveIntervalRole.intervalEnd);
} else {
result[result.length - 2].setIntervalRole(CurveIntervalRole.intervalInterior);
detail.setIntervalRole(CurveIntervalRole.intervalEnd);
}
}
/**
* Find intersections with a plane.
* Intersections within segments are recorded as CurveIntervalRole.isolated
* Intersections at isolated "on" vertex are recoded as CurveIntervalRole.isolatedAtVertex.
*/
public override appendPlaneIntersectionPoints(plane: PlaneAltitudeEvaluator, result: CurveLocationDetail[]): number {
if (this._points.length < 1) return 0;
const initialLength = result.length;
const n = this._points.length;
const divisor = n === 1 ? 1.0 : n - 1;
const pointA = LineString3d._workPointA;
const pointB = LineString3d._workPointB;
const pointC = LineString3d._workPointC;
this._points.getPoint3dAtUncheckedPointIndex(0, pointA);
let hB = 0;
let numConsecutiveZero = 0;
let hA = 0;
let segmentFraction = 0;
for (let i = 0; i < this._points.length; i++, pointA.setFrom(pointB), hA = hB) {
this._points.getPoint3dAtUncheckedPointIndex(i, pointB);
hB = Geometry.correctSmallMetricDistance(plane.altitude(pointB));
if (hB === 0.0)
LineString3d.pushVertexHit(result, numConsecutiveZero++, this, i / divisor, pointB);
else {
if (hA * hB < 0.0) { // at point0, hA=0 will keep us out of here . ..
segmentFraction = hA / (hA - hB); // this division is safe because the signs are different.
pointA.interpolate(segmentFraction, pointB, pointC);
const detail = CurveLocationDetail.createCurveFractionPoint(this, (i - 1 + segmentFraction) / divisor, pointC);
detail.setIntervalRole(CurveIntervalRole.isolated);
result.push(detail);
numConsecutiveZero = 0;
}
}
}
return result.length - initialLength;
}
/** Extend `rangeToExtend` to include all points of this linestring. */
public extendRange(rangeToExtend: Range3d, transform?: Transform): void {
this._points.extendRange(rangeToExtend, transform);
}
/** Test if each point of this linestring isAlmostEqual with corresponding point in `other`. */
public override isAlmostEqual(other: GeometryQuery): boolean {
if (!(other instanceof LineString3d))
return false;
if (!GrowableXYZArray.isAlmostEqual(this._points, other._points))
return false;
return true;
}
/**
* Append (clone of) one point.
* * BUT ... skip if duplicates the tail of prior points.
* * if fraction is given, "duplicate" considers both point and fraction.
*/
public appendStrokePoint(point: Point3d, fraction?: number): void {
const n = this._points.length;
let add = true;
const addFraction = fraction !== undefined && this._fractions !== undefined;
if (n > 0) {
if (addFraction && Geometry.isSameCoordinate(fraction, this._fractions!.back()))
add = false;
if (point.isAlmostEqual(this._points.getPoint3dAtUncheckedPointIndex(n - 1)))
add = false;
}
if (add) {
this._points.push(point);
if (addFraction)
this.addFraction(fraction);
}
}
/** Compress out duplicate points (according to point.isAlmostEqual) */
public removeDuplicatePoints(tolerance: number = Geometry.smallMetricDistance) {
const n = this._points.length;
if (n < 2)
return;
let n1 = 1;
for (let i = 1; i < n; i++) {
const q = this._points.distanceIndexIndex(i, n1 - 1);