-
Notifications
You must be signed in to change notification settings - Fork 208
/
Range.ts
1814 lines (1796 loc) · 67.4 KB
/
Range.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 CartesianGeometry
*/
import { AxisIndex, BeJSONFunctions, Geometry } from "../Geometry";
import { MultiLineStringDataVariant } from "../geometry3d/IndexedXYZCollection";
import { GrowableXYZArray } from "./GrowableXYZArray";
import { Matrix3d } from "./Matrix3d";
import { Point2d, Vector2d } from "./Point2dVector2d";
import { Point3d, Vector3d } from "./Point3dVector3d";
import { PointStreamRangeCollector, VariantPointDataStream } from "./PointStreaming";
import { Transform } from "./Transform";
import { LowAndHighXY, LowAndHighXYZ, Range1dProps, Range2dProps, Range3dProps, XAndY, XYAndZ } from "./XYZProps";
// allow _EXTREME_POSITIVE and _EXTREME_NEGATIVE
/* eslint-disable @typescript-eslint/naming-convention */
/**
* Base class for Range1d, Range2d, Range3d.
* @public
*/
export abstract class RangeBase {
/** Number considered impossibly large possibly for a coordinate in a range. */
protected static readonly _EXTREME_POSITIVE: number = 1.0e200;
/** Number considered to be impossibly negative for a coordinate in a range. */
protected static readonly _EXTREME_NEGATIVE: number = -1.0e200;
/** Return 0 if high<= low, otherwise `1/(high-low)` for use in fractionalizing */
protected static npcScaleFactor(low: number, high: number): number {
return (high <= low) ? 0.0 : 1.0 / (high - low);
}
/** Return true if x is outside the range `[_EXTREME_NEGATIVE, _EXTREME_POSITIVE]' */
public static isExtremeValue(x: number): boolean {
return Math.abs(x) >= RangeBase._EXTREME_POSITIVE;
}
/** Return true if any x or y or z is outside the range `[_EXTREME_NEGATIVE, _EXTREME_POSITIVE]' */
public static isExtremePoint3d(xyz: Point3d) {
return RangeBase.isExtremeValue(xyz.x) || RangeBase.isExtremeValue(xyz.y) || RangeBase.isExtremeValue(xyz.z);
}
/** Return true if either of x,y is outside the range `[_EXTREME_NEGATIVE, _EXTREME_POSITIVE]' */
public static isExtremePoint2d(xy: Point2d) {
return RangeBase.isExtremeValue(xy.x) || RangeBase.isExtremeValue(xy.y);
}
/**
* Return the min absolute distance from any point of `[lowA,highA]' to any point of `[lowB,highB]'.
* * Both low,high pairs have order expectations: The condition `high < low` means null interval.
* * If there is interval overlap, the distance is zero.
* @param lowA low of interval A
* @param highA high of interval A
* @param lowB low of interval B
* @param highB high of interval B
*/
public static rangeToRangeAbsoluteDistance(lowA: number, highA: number, lowB: number, highB: number): number {
if (highA < lowA)
return RangeBase._EXTREME_POSITIVE;
if (highB < lowB)
return RangeBase._EXTREME_POSITIVE;
if (highB < lowA)
return lowA - highB;
if (highB <= highA)
return 0.0;
if (lowB <= highA)
return 0.0;
return lowB - highA;
}
/**
* Given a coordinate and pair of range limits, return the smallest distance to the range.
* * This is zero for any point inside the range
* * This is _EXTREME_POSITIVE if the range limits are inverted
* * Otherwise (i.e. x is outside a finite range) the distance to the near endpoint.
*/
public static coordinateToRangeAbsoluteDistance(x: number, low: number, high: number): number {
if (high < low)
return RangeBase._EXTREME_POSITIVE;
if (x < low)
return low - x;
if (x > high)
return x - high;
return 0.0;
}
/**
* If a > 0, return (extrapolationFactor * a); otherwise return defaultValue
* @param q
* @param factor multiplier for positive q values.
*/
public static multiplyIfPositive(q: number, factor: number, defaultValue: number = 0.0): number {
return q > 0 ? factor * q : defaultValue;
}
}
/**
* Axis aligned range in 3D.
* * member `low` contains minimum coordinate of range box
* * member `high` contains maximum coordinate of range box
* * The range is considered null (empty) if any low member is larger than its high counterpart.
* @public
*/
export class Range3d extends RangeBase implements LowAndHighXYZ, BeJSONFunctions {
// low and high are always non-null objects
// any direction of low.q > high.q is considered a null range.
// private ctor and setXYZXYZ_direct set the low and high explicitly (without further tests of low.q.<= high.q)
/** Low point coordinates */
public low: Point3d;
/** High point coordinates */
public high: Point3d;
/** Set this transform to values that indicate it has no geometric contents. */
public setNull() {
this.low.x = RangeBase._EXTREME_POSITIVE;
this.low.y = RangeBase._EXTREME_POSITIVE;
this.low.z = RangeBase._EXTREME_POSITIVE;
this.high.x = RangeBase._EXTREME_NEGATIVE;
this.high.y = RangeBase._EXTREME_NEGATIVE;
this.high.z = RangeBase._EXTREME_NEGATIVE;
}
/** Freeze this instance (and its members) so it is read-only */
public freeze(): Readonly<this> {
this.low.freeze();
this.high.freeze();
return Object.freeze(this);
}
/** Flatten the low and high coordinates of any json object with low.x .. high.z into an array of 6 doubles */
public static toFloat64Array(val: LowAndHighXYZ): Float64Array {
return Float64Array.of(val.low.x, val.low.y, val.low.z, val.high.x, val.high.y, val.high.z);
}
/** Flatten the low and high coordinates of this into an array of 6 doubles */
public toFloat64Array(): Float64Array {
return Range3d.toFloat64Array(this);
}
/**
* Construct a Range3d from an array of double-precision values
* @param f64 the array, which should contain exactly 6 values in this order: lowX, lowY, lowZ, highX, highY, highZ
* @return a new Range3d object
*/
public static fromFloat64Array<T extends Range3d>(f64: Float64Array): T {
if (f64.length !== 6)
throw new Error("invalid array");
return new this(f64[0], f64[1], f64[2], f64[3], f64[4], f64[5]) as T;
}
/**
* Construct a Range3d from an un-typed array. This mostly useful when interpreting ECSQL query results of
* the 'blob' type, where you know that that result is a Range3d.
* @param buffer untyped array
* @return a new Range3d object
*/
public static fromArrayBuffer<T extends Range3d>(buffer: ArrayBuffer): T {
return this.fromFloat64Array(new Float64Array(buffer));
}
// explicit ctor - no enforcement of value relationships
public constructor(
lowX: number = RangeBase._EXTREME_POSITIVE, lowY: number = RangeBase._EXTREME_POSITIVE, lowZ: number = RangeBase._EXTREME_POSITIVE,
highX: number = RangeBase._EXTREME_NEGATIVE, highY: number = RangeBase._EXTREME_NEGATIVE, highZ: number = RangeBase._EXTREME_NEGATIVE,
) {
super();
this.low = Point3d.create(lowX, lowY, lowZ);
this.high = Point3d.create(highX, highY, highZ);
}
/** Returns true if this and other have equal low and high parts, or both are null ranges. */
public isAlmostEqual(other: Readonly<Range3d>, tol?: number): boolean {
return (this.low.isAlmostEqual(other.low, tol) && this.high.isAlmostEqual(other.high, tol))
|| (this.isNull && other.isNull);
}
/** Copy low and high values from other. */
public setFrom(other: Range3d) {
this.low.setFrom(other.low);
this.high.setFrom(other.high);
}
/** Return a new Range3d copied from a range or derived type */
public static createFrom<T extends Range3d>(other: Range3d, result?: T): T {
if (result) {
result.setFrom(other);
return result;
}
return this.createXYZXYZOrCorrectToNull<T>(
other.low.x, other.low.y, other.low.z,
other.high.x, other.high.y, other.high.z,
result,
);
}
/**
* Set this range (in place) from json such as
* * key-value pairs: `{low:[1,2,3], high:[4,5,6]}`
* * array of points: `[[1,2,3],[9,3,4],[-2,1,3] ...]`
* * Lowest level points can be `[1,2,3]` or `{x:1,y:2,z:3}`
*/
public setFromJSON(json?: Range3dProps) {
if (!json)
return;
this.setNull();
if (Array.isArray(json)) {
const point = Point3d.create();
for (const value of json) {
point.setFromJSON(value);
this.extendPoint(point);
}
return;
}
const low = Point3d.fromJSON(json.low);
const high = Point3d.fromJSON(json.high);
if (!RangeBase.isExtremePoint3d(low) && !RangeBase.isExtremePoint3d(high)) {
this.extendPoint(low);
this.extendPoint(high);
}
}
/**
* Return a JSON object `{low: ... , high: ...}`
* with points formatted by `Point3d.toJSON()`
*/
public toJSON(): Range3dProps {
return { low: this.low.toJSON(), high: this.high.toJSON() };
}
/** Use `setFromJSON` to parse `json` into a new Range3d instance. */
public static fromJSON<T extends Range3d>(json?: Range3dProps): T {
const result = new this() as T;
result.setFromJSON(json);
return result;
}
// internal use only -- directly set all coordinates, test only if directed.
private setDirect(xA: number, yA: number, zA: number, xB: number, yB: number, zB: number, correctToNull: boolean) {
this.low.x = xA;
this.low.y = yA;
this.low.z = zA;
this.high.x = xB;
this.high.y = yB;
this.high.z = zB;
if (correctToNull) {
if (
this.low.x > this.high.x
|| this.low.y > this.high.y
|| this.low.z > this.high.z
)
this.setNull();
}
}
/** Return a copy */
public clone(result?: this): this {
result = result ? result : new (this.constructor as any)() as this;
result.setDirect(this.low.x, this.low.y, this.low.z, this.high.x, this.high.y, this.high.z, false);
return result;
}
/**
* Return a copy, translated by adding `shift` components in all directions.
* * The translate of a null range is also a null range.
*/
public cloneTranslated(shift: XYAndZ, result?: this): this {
result = result ? result : new (this.constructor as any)() as this;
if (!this.isNull)
result.setDirect(
this.low.x + shift.x, this.low.y + shift.y, this.low.z + shift.z,
this.high.x + shift.x, this.high.y + shift.y, this.high.z + shift.z,
false,
);
return result;
}
/** Return a range initialized to have no content. */
public static createNull<T extends Range3d>(result?: T): T {
result = result ? result : new this() as T;
result.setNull();
return result;
}
/** Extend (modify in place) so that the range is large enough to include the supplied points. */
public extend(...point: Point3d[]) {
let p;
for (p of point)
this.extendPoint(p);
}
/** Return a range large enough to include the supplied points. If no points are given, the range is a null range */
public static create(...point: Point3d[]) {
const result = Range3d.createNull();
let p;
for (p of point)
result.extendPoint(p);
return result;
}
/** Create a range from freely structured MultiLineStringDataVariant. */
public static createFromVariantData(data: MultiLineStringDataVariant): Range3d {
const collector = new PointStreamRangeCollector();
VariantPointDataStream.streamXYZ(data, collector);
return collector.claimResult();
}
/** Create a Range3d enclosing the transformed points. */
public static createTransformed<T extends Range3d>(transform: Transform, ...point: Point3d[]): T {
const result = this.createNull<T>();
let p;
for (p of point)
result.extendTransformedXYZ(transform, p.x, p.y, p.z);
return result;
}
/** Create a Range3d enclosing the transformed points. */
public static createTransformedArray<T extends Range3d>(
transform: Transform, points: Point3d[] | GrowableXYZArray,
): T {
const result = this.createNull<T>();
result.extendArray(points, transform);
return result;
}
/** Create a Range3d enclosing the points after inverse transform. */
public static createInverseTransformedArray<T extends Range3d>(
transform: Transform, points: Point3d[] | GrowableXYZArray,
): T {
const result = this.createNull<T>();
result.extendInverseTransformedArray(points, transform);
return result;
}
/** Set the range to be a single point supplied as x,y,z values */
public setXYZ(x: number, y: number, z: number) {
this.low.x = this.high.x = x;
this.low.y = this.high.y = y;
this.low.z = this.high.z = z;
}
/** Create a single point range */
public static createXYZ<T extends Range3d>(x: number, y: number, z: number, result?: T): T {
result = result ? result : new this() as T;
result.setDirect(x, y, z, x, y, z, false);
return result;
}
/** Create a box with 2 pairs of xyz candidates. Theses are compared and shuffled as needed for the box. */
public static createXYZXYZ<T extends Range3d>(
xA: number, yA: number, zA: number, xB: number, yB: number, zB: number, result?: T,
): T {
result = result ? result : new this() as T;
result.setDirect(
Math.min(xA, xB), Math.min(yA, yB), Math.min(zA, zB),
Math.max(xA, xB), Math.max(yA, yB), Math.max(zA, zB),
false,
);
return result;
}
/** Create a box with 2 pairs of xyz candidates. If any direction has order flip, create null. */
public static createXYZXYZOrCorrectToNull<T extends Range3d>(
xA: number, yA: number, zA: number, xB: number, yB: number, zB: number, result?: T,
): T {
result = result ? result : new this() as T;
if (xA > xB || yA > yB || zA > zB)
return this.createNull(result);
result.setDirect(
Math.min(xA, xB), Math.min(yA, yB), Math.min(zA, zB),
Math.max(xA, xB), Math.max(yA, yB), Math.max(zA, zB),
true,
);
return result;
}
/** Creates a 3d range from a 2d range's low and high members, setting the corresponding z values to the value given. */
public static createRange2d<T extends Range3d>(range: Range2d, z: number = 0, result?: T): T {
const retVal = result ? result : new this() as T;
retVal.setNull();
retVal.extendXYZ(range.low.x, range.low.y, z);
retVal.extendXYZ(range.high.x, range.high.y, z);
return retVal;
}
/** Create a range around an array of points. */
public static createArray<T extends Range3d>(points: Point3d[], result?: T): T {
result = result ? result : new this() as T;
result.setNull();
let point;
for (point of points)
result.extendPoint(point);
return result;
}
/** Extend a range around an array of points (optionally transformed) */
public extendArray(points: Point3d[] | GrowableXYZArray, transform?: Transform) {
if (Array.isArray(points))
if (transform)
for (const point of points)
this.extendTransformedXYZ(transform, point.x, point.y, point.z);
else
for (const point of points)
this.extendXYZ(point.x, point.y, point.z);
else // growable array -- this should be implemented without point extraction !!!
if (transform)
for (let i = 0; i < points.length; i++)
this.extendTransformedXYZ(
transform,
points.getXAtUncheckedPointIndex(i),
points.getYAtUncheckedPointIndex(i),
points.getZAtUncheckedPointIndex(i),
);
else
for (let i = 0; i < points.length; i++)
this.extendXYZ(
points.getXAtUncheckedPointIndex(i),
points.getYAtUncheckedPointIndex(i),
points.getZAtUncheckedPointIndex(i),
);
}
/** Extend a range around an array of points (optionally transformed) */
public extendInverseTransformedArray(points: Point3d[] | GrowableXYZArray, transform: Transform) {
if (Array.isArray(points))
for (const point of points)
this.extendInverseTransformedXYZ(transform, point.x, point.y, point.z);
else // growable array -- this should be implemented without point extraction !!!
for (let i = 0; i < points.length; i++)
this.extendInverseTransformedXYZ(
transform,
points.getXAtUncheckedPointIndex(i),
points.getYAtUncheckedPointIndex(i),
points.getZAtUncheckedPointIndex(i),
);
}
/** Multiply the point x,y,z by transform and use the coordinate to extend this range. */
public extendTransformedXYZ(transform: Transform, x: number, y: number, z: number) {
const origin = transform.origin;
const coffs = transform.matrix.coffs;
this.extendXYZ(
origin.x + coffs[0] * x + coffs[1] * y + coffs[2] * z,
origin.y + coffs[3] * x + coffs[4] * y + coffs[5] * z,
origin.z + coffs[6] * x + coffs[7] * y + coffs[8] * z,
);
}
/** Multiply the point x,y,z,w by transform and use the coordinate to extend this range. */
public extendTransformedXYZW(transform: Transform, x: number, y: number, z: number, w: number) {
const origin = transform.origin;
const coffs = transform.matrix.coffs;
this.extendXYZW(
origin.x * w + coffs[0] * x + coffs[1] * y + coffs[2] * z,
origin.y * w + coffs[3] * x + coffs[4] * y + coffs[5] * z,
origin.z * w + coffs[6] * x + coffs[7] * y + coffs[8] * z,
w,
);
}
/** Multiply the point x,y,z by the inverse of the transform and use the coordinate to extend this range. */
public extendInverseTransformedXYZ(transform: Transform, x: number, y: number, z: number): boolean {
const origin = transform.origin;
if (!transform.matrix.computeCachedInverse(true))
return false;
const coffs = transform.matrix.inverseCoffs!;
const xx = x - origin.x;
const yy = y - origin.y;
const zz = z - origin.z;
this.extendXYZ(
coffs[0] * xx + coffs[1] * yy + coffs[2] * zz,
coffs[3] * xx + coffs[4] * yy + coffs[5] * zz,
coffs[6] * xx + coffs[7] * yy + coffs[8] * zz,
);
return true;
}
/** Extend the range by the two transforms applied to xyz */
public extendTransformTransformedXYZ(
transformA: Transform, transformB: Transform, x: number, y: number, z: number,
): void {
const origin = transformB.origin;
const coffs = transformB.matrix.coffs;
this.extendTransformedXYZ(
transformA,
origin.x + coffs[0] * x + coffs[1] * y + coffs[2] * z,
origin.y + coffs[3] * x + coffs[4] * y + coffs[5] * z,
origin.z + coffs[6] * x + coffs[7] * y + coffs[8] * z,
);
}
/** Test if the box has high<low for any of x,y,z, condition. Note that a range around a single point is NOT null. */
public get isNull(): boolean {
return this.high.x < this.low.x
|| this.high.y < this.low.y
|| this.high.z < this.low.z;
}
/** Test if data has high<low for any of x,y,z, condition. Note that a range around a single point is NOT null. */
public static isNull(data: LowAndHighXYZ): boolean {
return data.high.x < data.low.x
|| data.high.y < data.low.y
|| data.high.z < data.low.z;
}
/** Test of the range contains a single point. */
public get isSinglePoint(): boolean {
return this.high.x === this.low.x
&& this.high.y === this.low.y
&& this.high.z === this.low.z;
}
/** Return the midpoint of the diagonal. No test for null range. */
public get center(): Point3d {
return this.low.interpolate(.5, this.high);
}
/** Return the low x coordinate */
public get xLow(): number {
return this.low.x;
}
/** Return the low y coordinate */
public get yLow(): number {
return this.low.y;
}
/** Return the low z coordinate */
public get zLow(): number {
return this.low.z;
}
/** Return the high x coordinate */
public get xHigh(): number {
return this.high.x;
}
/** Return the high y coordinate */
public get yHigh(): number {
return this.high.y;
}
/** Return the high z coordinate */
public get zHigh(): number {
return this.high.z;
}
/** Return the length of the box in the x direction */
public xLength(): number {
const a = this.high.x - this.low.x; return a > 0.0 ? a : 0.0;
}
/** Return the length of the box in the y direction */
public yLength(): number {
const a = this.high.y - this.low.y; return a > 0.0 ? a : 0.0;
}
/** Return the length of the box in the z direction */
public zLength(): number {
const a = this.high.z - this.low.z; return a > 0.0 ? a : 0.0;
}
/** Return the largest of the x,y, z lengths of the range. */
public maxLength(): number {
return Math.max(this.xLength(), this.yLength(), this.zLength());
}
/**
* Return the diagonal vector. There is no check for isNull -- if the range isNull(), the vector will have very
* large negative coordinates.
*/
public diagonal(result?: Vector3d): Vector3d {
return this.low.vectorTo(this.high, result);
}
/**
* Return the diagonal vector. There is no check for isNull -- if the range isNull(), the vector will have very
* large negative coordinates.
*/
public diagonalFractionToPoint(fraction: number, result?: Point3d): Point3d {
return this.low.interpolate(fraction, this.high, result);
}
/** Return a point given by fractional positions on the XYZ axes. This is done with no check for isNull !!! */
public fractionToPoint(fractionX: number, fractionY: number, fractionZ: number = 0, result?: Point3d): Point3d {
return this.low.interpolateXYZ(fractionX, fractionY, fractionZ, this.high, result);
}
/**
* Return a point given by fractional positions on the XYZ axes.
* Returns undefined if the range is null.
*/
public localXYZToWorld(
fractionX: number, fractionY: number, fractionZ: number, result?: Point3d,
): Point3d | undefined {
if (this.isNull) return undefined;
return this.low.interpolateXYZ(fractionX, fractionY, fractionZ, this.high, result);
}
/**
* Return a point given by fractional positions on the XYZ axes.
* * Returns undefined if the range is null.
*/
public localToWorld(xyz: XYAndZ, result?: Point3d): Point3d | undefined {
return this.localXYZToWorld(xyz.x, xyz.y, xyz.z, result);
}
/**
* Replace fractional coordinates by world coordinates.
* @returns false if null range.
*/
public localToWorldArrayInPlace(points: Point3d[]): boolean {
if (this.isNull) return false;
for (const p of points)
this.low.interpolateXYZ(p.x, p.y, p.z, this.high, p);
return false;
}
/**
* Return fractional coordinates of point within the range.
* * returns undefined if the range is null.
* * returns undefined if any direction (x,y,z) has zero length
*/
public worldToLocal(point: Point3d, result?: Point3d): Point3d | undefined {
const ax = RangeBase.npcScaleFactor(this.low.x, this.high.x);
const ay = RangeBase.npcScaleFactor(this.low.y, this.high.y);
const az = RangeBase.npcScaleFactor(this.low.z, this.high.z);
if (ax === 0.0 || ay === 0.0 || az === 0.0)
return undefined;
return Point3d.create(
(point.x - this.low.x) * ax,
(point.y - this.low.y) * ay,
(point.z - this.low.z) * az,
result,
);
}
/**
* Return fractional coordinates of point within the range.
* * returns undefined if the range is null.
* * returns undefined if any direction (x,y,z) has zero length
*/
public worldToLocalArrayInPlace(point: Point3d[]): boolean {
const ax = RangeBase.npcScaleFactor(this.low.x, this.high.x);
const ay = RangeBase.npcScaleFactor(this.low.y, this.high.y);
const az = RangeBase.npcScaleFactor(this.low.z, this.high.z);
if (ax === 0.0 || ay === 0.0 || az === 0.0)
return false;
for (const p of point)
Point3d.create((p.x - this.low.x) * ax, (p.y - this.low.y) * ay, (p.z - this.low.z) * az, p);
return true;
}
/**
* Return an array with the 8 corners on order wth "x varies fastest, then y, then z"
* * points preallocated in `result` are reused if result.length >= 8.
* * in reuse case, result.length is trimmed to 8
*/
public corners(result?: Point3d[]): Point3d[] {
if (result !== undefined && result.length >= 8) {
result[0].set(this.low.x, this.low.y, this.low.z);
result[1].set(this.high.x, this.low.y, this.low.z);
result[2].set(this.low.x, this.high.y, this.low.z);
result[3].set(this.high.x, this.high.y, this.low.z);
result[4].set(this.low.x, this.low.y, this.high.z);
result[5].set(this.high.x, this.low.y, this.high.z);
result[6].set(this.low.x, this.high.y, this.high.z);
result[7].set(this.high.x, this.high.y, this.high.z);
result.length = 8;
return result;
}
return [
Point3d.create(this.low.x, this.low.y, this.low.z),
Point3d.create(this.high.x, this.low.y, this.low.z),
Point3d.create(this.low.x, this.high.y, this.low.z),
Point3d.create(this.high.x, this.high.y, this.low.z),
Point3d.create(this.low.x, this.low.y, this.high.z),
Point3d.create(this.high.x, this.low.y, this.high.z),
Point3d.create(this.low.x, this.high.y, this.high.z),
Point3d.create(this.high.x, this.high.y, this.high.z),
];
}
/**
* Return an array with indices of the corners of a face
* * face 0 has negative x normal
* * face 1 has positive x normal
* * face 2 has negative y normal
* * face 3 has positive y normal
* * face 4 has negative z normal
* * face 5 has positive z normal
* * Any other value returns face 5
* * faces are CCW as viewed from outside.
*/
public static faceCornerIndices(index: number): number[] {
if (index === 0)
return [0, 4, 6, 2];
if (index === 1)
return [1, 3, 7, 5];
if (index === 2)
return [0, 1, 5, 4];
if (index === 3)
return [3, 2, 6, 7];
if (index === 4)
return [0, 2, 3, 1];
return [4, 5, 7, 6];
}
/**
* Return a rectangle that is the cross section as viewed from above (z direction) and at zFraction
* @param zFraction plane altitude within the 0..1 z fraction range
* @param upwardNormal true for CCW as viewed from above
* @param addClosure true to add closure edge back to the start
* @returns
*/
public rectangleXY(
zFraction: number = 0.0, upwardNormal: boolean = true, addClosure: boolean = true,
): Point3d[] | undefined {
if (this.isNull)
return undefined;
const points: Point3d[] = [
this.fractionToPoint(0, 0, zFraction),
this.fractionToPoint(1, 0, zFraction),
this.fractionToPoint(1, 1, zFraction),
this.fractionToPoint(0, 1, zFraction),
];
if (addClosure)
points.push(points[0].clone());
if (!upwardNormal)
points.reverse();
return points;
}
/** Return the largest absolute value among any coordinates in the box corners. */
public maxAbs(): number {
if (this.isNull)
return 0.0;
return Math.max(this.low.maxAbs(), this.high.maxAbs());
}
/** Returns true if the x direction size is nearly zero */
public get isAlmostZeroX(): boolean {
return Geometry.isSmallMetricDistance(this.xLength());
}
/** Returns true if the y direction size is nearly zero */
public get isAlmostZeroY(): boolean {
return Geometry.isSmallMetricDistance(this.yLength());
}
/** Returns true if the z direction size is nearly zero */
public get isAlmostZeroZ(): boolean {
return Geometry.isSmallMetricDistance(this.zLength());
}
/** Test if a point given as x,y,z is within the range. */
public containsXYZ(x: number, y: number, z: number): boolean {
return x >= this.low.x
&& y >= this.low.y
&& z >= this.low.z
&& x <= this.high.x
&& y <= this.high.y
&& z <= this.high.z;
}
/** Test if a point given as x,y is within the range (ignoring z of range). */
public containsXY(x: number, y: number): boolean {
return x >= this.low.x
&& y >= this.low.y
&& x <= this.high.x
&& y <= this.high.y;
}
/** Test if a point is within the range. */
public containsPoint(point: Point3d): boolean {
return this.containsXYZ(point.x, point.y, point.z);
}
/** Test if the x,y coordinates of a point are within the range. */
public containsPointXY(point: Point3d): boolean {
return point.x >= this.low.x
&& point.y >= this.low.y
&& point.x <= this.high.x
&& point.y <= this.high.y;
}
/** Test of other range is within this range */
public containsRange(other: Range3d): boolean {
return other.low.x >= this.low.x
&& other.low.y >= this.low.y
&& other.low.z >= this.low.z
&& other.high.x <= this.high.x
&& other.high.y <= this.high.y
&& other.high.z <= this.high.z;
}
/** Test if there is any intersection with other range */
public intersectsRange(other: Range3d): boolean {
return !(this.low.x > other.high.x
|| this.low.y > other.high.y
|| this.low.z > other.high.z
|| other.low.x > this.high.x
|| other.low.y > this.high.y
|| other.low.z > this.high.z);
}
/** Test if there is any intersection with other range, ignoring z. */
public intersectsRangeXY(other: Range3d): boolean {
return !(
this.low.x > other.high.x
|| this.low.y > other.high.y
|| other.low.x > this.high.x
|| other.low.y > this.high.y
);
}
/** Return 0 if the point is within the range, otherwise the distance to the closest face or corner */
public distanceToPoint(point: XYAndZ): number {
if (this.isNull)
return RangeBase._EXTREME_POSITIVE;
return Math.min(
Geometry.hypotenuseXYZ(
RangeBase.coordinateToRangeAbsoluteDistance(point.x, this.low.x, this.high.x),
RangeBase.coordinateToRangeAbsoluteDistance(point.y, this.low.y, this.high.y),
RangeBase.coordinateToRangeAbsoluteDistance(point.z, this.low.z, this.high.z),
),
RangeBase._EXTREME_POSITIVE,
);
}
/** Returns 0 if the ranges have any overlap, otherwise the shortest absolute distance from one to the other. */
public distanceToRange(other: Range3d): number {
return Math.min(
Geometry.hypotenuseXYZ(
RangeBase.rangeToRangeAbsoluteDistance(this.low.x, this.high.x, other.low.x, other.high.x),
RangeBase.rangeToRangeAbsoluteDistance(this.low.y, this.high.y, other.low.y, other.high.y),
RangeBase.rangeToRangeAbsoluteDistance(this.low.z, this.high.z, other.low.z, other.high.z),
),
RangeBase._EXTREME_POSITIVE,
);
}
/** Expand this range by distances a (possibly signed) in all directions */
public extendXYZ(x: number, y: number, z: number): void {
if (x < this.low.x)
this.low.x = x;
if (x > this.high.x)
this.high.x = x;
if (y < this.low.y)
this.low.y = y;
if (y > this.high.y)
this.high.y = y;
if (z < this.low.z)
this.low.z = z;
if (z > this.high.z)
this.high.z = z;
}
/** Expand this range by a point interpolated between given points. */
public extendInterpolated(xyz0: Point3d, fraction: number, xyz1: Point3d): void {
if (fraction < 0.5) {
this.extendXYZ(
xyz0.x + fraction * (xyz1.x - xyz0.x),
xyz0.y + fraction * (xyz1.y - xyz0.y),
xyz0.z + fraction * (xyz1.z - xyz0.z));
} else {
// use reversed formulas for best accuracy at fraction=1.0
const g = 1.0 - fraction;
this.extendXYZ(
xyz1.x + g * (xyz0.x - xyz1.x),
xyz1.y + g * (xyz0.y - xyz1.y),
xyz1.z + g * (xyz0.z - xyz1.z));
}
}
/** Expand this range by distances a in only the x direction. */
public extendXOnly(x: number): void {
if (x < this.low.x)
this.low.x = x;
if (x > this.high.x)
this.high.x = x;
}
/** Expand this range by distances a in only the x direction. */
public extendYOnly(y: number): void {
if (y < this.low.y)
this.low.y = y;
if (y > this.high.y)
this.high.y = y;
}
/** Expand this range by distances a in only the x direction. */
public extendZOnly(z: number): void {
if (z < this.low.z)
this.low.z = z;
if (z > this.high.z)
this.high.z = z;
}
/** Expand one component of this range */
public extendSingleAxis(a: number, axisIndex: AxisIndex): void {
if (axisIndex === AxisIndex.X)
this.extendXOnly(a);
if (axisIndex === AxisIndex.Y)
this.extendYOnly(a);
if (axisIndex === AxisIndex.Z)
this.extendZOnly(a);
}
/** Expand this range by distances a (weighted and possibly signed) in all directions */
public extendXYZW(x: number, y: number, z: number, w: number): void {
if (!Geometry.isSmallMetricDistance(w))
this.extendXYZ(x / w, y / w, z / w);
}
/** Expand this range to include a point. */
public extendPoint(point: Point3d, transform?: Transform): void {
if (transform) {
this.extendTransformedXYZ(transform, point.x, point.y, point.z);
} else {
this.extendXYZ(point.x, point.y, point.z);
}
}
/** Expand this range to include a transformed point. */
public extendTransformedPoint(transform: Transform, point: Point3d): void {
this.extendTransformedXYZ(transform, point.x, point.y, point.z);
}
/** Expand this range to include a range. */
public extendRange(other: LowAndHighXYZ): void {
if (!Range3d.isNull(other)) {
this.extendXYZ(other.low.x, other.low.y, other.low.z);
this.extendXYZ(other.high.x, other.high.y, other.high.z);
}
}
/**
* In each direction look at the difference between this range limit and that of interiorRange.
* * If this range is larger, expand it by extrapolationFactor.
*/
public extendWhenLarger(other: LowAndHighXYZ, extrapolationFactor: number): void {
if (!Range3d.isNull(other) && !Range3d.isNull(this)) {
this.high.x += RangeBase.multiplyIfPositive(this.high.x - other.high.x, extrapolationFactor);
this.high.y += RangeBase.multiplyIfPositive(this.high.y - other.high.y, extrapolationFactor);
this.high.z += RangeBase.multiplyIfPositive(this.high.z - other.high.z, extrapolationFactor);
this.low.x -= RangeBase.multiplyIfPositive(other.low.x - this.low.x, extrapolationFactor);
this.low.y -= RangeBase.multiplyIfPositive(other.low.y - this.low.y, extrapolationFactor);
this.low.z -= RangeBase.multiplyIfPositive(other.low.z - this.low.z, extrapolationFactor);
}
}
/** Return the intersection of ranges. */
public intersect(other: Range3d, result?: Range3d): Range3d {
if (!this.intersectsRange(other))
return Range3d.createNull(result);
return Range3d.createXYZXYZOrCorrectToNull(
Math.max(this.low.x, other.low.x), Math.max(this.low.y, other.low.y), Math.max(this.low.z, other.low.z),
Math.min(this.high.x, other.high.x), Math.min(this.high.y, other.high.y), Math.min(this.high.z, other.high.z),
result,
);
}
/** Return the union of ranges. */
public union(other: Range3d, result?: Range3d): Range3d {
if (this.isNull)
return other.clone(result);
if (other.isNull)
return this.clone(result as this);
// we trust null ranges have EXTREME values, so a null in either input leads to expected results.
return Range3d.createXYZXYZOrCorrectToNull(
Math.min(this.low.x, other.low.x), Math.min(this.low.y, other.low.y), Math.min(this.low.z, other.low.z),
Math.max(this.high.x, other.high.x), Math.max(this.high.y, other.high.y), Math.max(this.high.z, other.high.z),
result,
);
}
/**
* Move low and high points by scaleFactor around the center point.
* @param scaleFactor scale factor applied to low, high distance from center.
*/
public scaleAboutCenterInPlace(scaleFactor: number) {
if (!this.isNull) {
scaleFactor = Math.abs(scaleFactor);
// do the scalar stuff to avoid making a temporary object ....
const xMid = 0.5 * (this.low.x + this.high.x);
const yMid = 0.5 * (this.low.y + this.high.y);
const zMid = 0.5 * (this.low.z + this.high.z);
this.high.x = Geometry.interpolate(xMid, scaleFactor, this.high.x);
this.high.y = Geometry.interpolate(yMid, scaleFactor, this.high.y);
this.high.z = Geometry.interpolate(zMid, scaleFactor, this.high.z);
this.low.x = Geometry.interpolate(xMid, scaleFactor, this.low.x);
this.low.y = Geometry.interpolate(yMid, scaleFactor, this.low.y);
this.low.z = Geometry.interpolate(zMid, scaleFactor, this.low.z);
}
}
/**
* Move all limits by a fixed amount.
* * positive delta expands the range size
* * negative delta reduces the range size
* * if any dimension reduces below zero size, the whole range becomes null
* @param delta shift to apply.
*/
public expandInPlace(delta: number): void {
this.setDirect(
this.low.x - delta, this.low.y - delta, this.low.z - delta,
this.high.x + delta, this.high.y + delta, this.high.z + delta,
true,
);
}
/** Create a local to world transform from this range. */
public getLocalToWorldTransform(result?: Transform): Transform {
return Transform.createOriginAndMatrix(
Point3d.create(this.low.x, this.low.y, this.low.z),
Matrix3d.createRowValues(
this.high.x - this.low.x, 0, 0,
0, this.high.y - this.low.y, 0,
0, 0, this.high.z - this.low.z,
),
result,
);
}
/**
* Creates an NPC to world transformation to go from 000...111 to the globally aligned cube with diagonally
* opposite corners that are the
* min and max of this range. The diagonal component for any degenerate direction is 1.
*/
public getNpcToWorldRangeTransform(result?: Transform): Transform {
const transform = this.getLocalToWorldTransform(result);
const matrix = transform.matrix;
if (matrix.coffs[0] === 0)
matrix.coffs[0] = 1;
if (matrix.coffs[4] === 0)
matrix.coffs[4] = 1;
if (matrix.coffs[8] === 0)
matrix.coffs[8] = 1;
return transform;
}
/**
* Ensure that the length of each dimension of this Range3d is at least a minimum size. If not, expand
* to minimum about the center.
* @param min The minimum length for each dimension.
*/
public ensureMinLengths(min: number = .001): void {
let size = (min - this.xLength()) / 2.0;
if (size > 0) {
this.low.x -= size;
this.high.x += size;
}
size = (min - this.yLength()) / 2.0;
if (size > 0) {
this.low.y -= size;
this.high.y += size;
}
size = (min - this.zLength()) / 2.0;
if (size > 0) {
this.low.z -= size;
this.high.z += size;
}
}
}
/**
* Range on a 1d axis
* * `low` and `high` members are always non-null objects
* * having `low > high` indicates an empty range.
* * the range contains x values for which `low <= x <= high`
* @public
*/
export class Range1d extends RangeBase {
/** Low point coordinates. DO NOT MODIFY FROM OUTSIDE THIS CLASS */
public low: number;
/** High point coordinates. DO NOT MODIFY FROM OUTSIDE THIS CLASS */
public high: number;
/** Reset the low and high to null range state. */
public setNull() {
this.low = RangeBase._EXTREME_POSITIVE;
this.high = RangeBase._EXTREME_NEGATIVE;
}
// internal use only -- directly set all coordinates, test only if directed.
private setDirect(low: number, high: number, correctToNull: boolean = false) {
this.low = low;
this.high = high;