-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlinesegment.go
1294 lines (1168 loc) · 52.2 KB
/
linesegment.go
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
package geom2d
import (
"fmt"
"math"
)
// detailedLineSegmentRelationship defines the possible spatial relationships
// between two line segments in a 2D plane.
//
// This enumeration categorizes how two line segments relate to each other based on
// their positions, intersections, collinearity, etc.
type detailedLineSegmentRelationship int8
// Valid values for detailedLineSegmentRelationship
const (
// lsrCollinearDisjoint indicates that the segments are collinear and do not intersect, overlap, or touch at any point.
lsrCollinearDisjoint detailedLineSegmentRelationship = iota - 1
// lsrMiss indicates that the segments are not collinear, disjoint, and do not intersect, overlap, or touch at any point.
lsrMiss
// lsrIntersects indicates that the segments intersect at a unique point that is not an endpoint.
lsrIntersects
// lsrAeqC indicates that point A of segment AB coincides with Point C of segment CD.
lsrAeqC
// lsrAeqD indicates that point A of segment AB coincides with Point D of segment CD.
lsrAeqD
// lsrBeqC indicates that the endpoint of segment AB coincides with Point C of segment CD.
lsrBeqC
// lsrBeqD indicates that the endpoint of segment AB coincides with Point D of segment CD.
lsrBeqD
// lsrAonCD indicates that point A lies on LineSegment CD.
lsrAonCD
// lsrBonCD indicates that the endpoint lies on LineSegment CD.
lsrBonCD
// lsrConAB indicates that point C lies on LineSegment AB.
lsrConAB
// lsrDonAB indicates that point D lies on LineSegment AB.
lsrDonAB
// lsrCollinearAonCD indicates that point A lies on LineSegment CD (partial overlap), and the line segments are collinear.
lsrCollinearAonCD
// lsrCollinearBonCD indicates that the endpoint lies on LineSegment CD (partial overlap), and the line segments are collinear.
lsrCollinearBonCD
// lsrCollinearABinCD indicates that segment AB is fully contained within segment CD.
lsrCollinearABinCD
// lsrCollinearCDinAB indicates that segment CD is fully contained within segment AB.
lsrCollinearCDinAB
// lsrCollinearEqual indicates that the segments AB and CD are exactly equal, sharing both endpoints in the same locations.
lsrCollinearEqual
)
// String provides a string representation of a [detailedLineSegmentRelationship] value.
//
// This method converts the [detailedLineSegmentRelationship] enum value into a human-readable string,
// allowing for easier debugging and output interpretation. Each enum value maps to its corresponding
// string name.
//
// Returns:
// - string: A string representation of the [detailedLineSegmentRelationship].
//
// Panics:
// - If the enum value is not recognized, the method panics with an error indicating
// an unsupported [detailedLineSegmentRelationship] value.
func (r detailedLineSegmentRelationship) String() string {
switch r {
case lsrCollinearDisjoint:
return "lsrCollinearDisjoint"
case lsrMiss:
return "lsrMiss"
case lsrIntersects:
return "lsrIntersects"
case lsrAeqC:
return "lsrAeqC"
case lsrAeqD:
return "lsrAeqD"
case lsrBeqC:
return "lsrBeqC"
case lsrBeqD:
return "lsrBeqD"
case lsrAonCD:
return "lsrAonCD"
case lsrBonCD:
return "lsrBonCD"
case lsrConAB:
return "lsrConAB"
case lsrDonAB:
return "lsrDonAB"
case lsrCollinearAonCD:
return "lsrCollinearAonCD"
case lsrCollinearBonCD:
return "lsrCollinearBonCD"
case lsrCollinearABinCD:
return "lsrCollinearABinCD"
case lsrCollinearCDinAB:
return "lsrCollinearCDinAB"
case lsrCollinearEqual:
return "lsrCollinearEqual"
default:
panic(fmt.Errorf("unsupported detailedLineSegmentRelationship"))
}
}
// LineSegment represents a line segment in a 2D space, defined by two endpoints, the start [Point] and end [Point].
//
// The generic type parameter T must satisfy the [SignedNumber] constraint, allowing the segment
// to use various numeric types such as int or float64 for its coordinates.
type LineSegment[T SignedNumber] struct {
start Point[T]
end Point[T]
}
// NewLineSegment creates a new line segment from two endpoints, a start [Point] and end [Point].
//
// This constructor function initializes a [LineSegment] with the specified starting and ending points.
// The generic type parameter "T" must satisfy the [SignedNumber] constraint, allowing various numeric types
// (such as int or float64) to be used for the segment’s coordinates.
//
// Parameters:
// - start ([Point][T]): The starting [Point] of the LineSegment.
// - end ([Point][T]): The ending [Point] of the LineSegment.
//
// Returns:
// - LineSegment[T] - A new line segment defined by the start and end points.
func NewLineSegment[T SignedNumber](start, end Point[T]) LineSegment[T] {
return LineSegment[T]{
start: start,
end: end,
}
}
// AddLineSegment adds the start and end points of another line segment to this one.
//
// This method performs an element-wise addition, where the start and end points
// of the other line segment are added to the corresponding start and end points
// of the current line segment.
//
// Parameters:
// - other (LineSegment[T]): The line segment to add to the current one.
//
// Returns:
// - LineSegment[T] - A new line segment where each endpoint is the sum of the corresponding endpoints.
func (l LineSegment[T]) AddLineSegment(other LineSegment[T]) LineSegment[T] {
return NewLineSegment(
l.start.Translate(other.start),
l.end.Translate(other.end),
)
}
// AsFloat32 converts the line segment to a LineSegment[float32] type.
//
// This function converts both endpoints of the LineSegment l to [Point][float32]
// values, creating a new line segment with floating-point coordinates.
// It is useful for precise calculations where floating-point accuracy is needed.
//
// Returns:
// - LineSegment[float32] - The line segment with both endpoints converted to float32.
func (l LineSegment[T]) AsFloat32() LineSegment[float32] {
return NewLineSegment(l.start.AsFloat32(), l.end.AsFloat32())
}
// AsFloat64 converts the line segment to a LineSegment[float64] type.
//
// This function converts both endpoints of the LineSegment l to [Point][float64]
// values, creating a new line segment with floating-point coordinates.
// It is useful for precise calculations where floating-point accuracy is needed.
//
// Returns:
// - LineSegment[float64] - The line segment with both endpoints converted to float64.
func (l LineSegment[T]) AsFloat64() LineSegment[float64] {
return NewLineSegment(l.start.AsFloat64(), l.end.AsFloat64())
}
// AsInt converts the line segment to a LineSegment[int] type.
//
// This function converts both endpoints of the line segment l to [Point][int]
// by truncating any decimal places. It is useful for converting a floating-point
// line segment to integer coordinates without rounding.
//
// Returns:
// - LineSegment[int] - The line segment with both endpoints converted to integer coordinates by truncation.
func (l LineSegment[T]) AsInt() LineSegment[int] {
return NewLineSegment(l.start.AsInt(), l.end.AsInt())
}
// AsIntRounded converts the line segment to a LineSegment[int] type with rounded coordinates.
//
// This function converts both endpoints of the line segment l to [Point][int]
// by rounding each coordinate to the nearest integer. It is useful when you need to
// approximate the segment’s position with integer coordinates while minimizing the
// rounding error.
//
// Returns:
// - LineSegment[int] - The line segment with both endpoints converted to integer coordinates by rounding.
func (l LineSegment[T]) AsIntRounded() LineSegment[int] {
return NewLineSegment(l.start.AsIntRounded(), l.end.AsIntRounded())
}
// BoundingBox computes the smallest axis-aligned [Rectangle] that fully contains the LineSegment.
//
// Returns:
// - [Rectangle][T]: A [Rectangle] defined by the opposite corners of the LineSegment.
//
// Notes:
// - This method is useful for spatial queries, collision detection, or visual rendering.
func (l LineSegment[T]) BoundingBox() Rectangle[T] {
points := []Point[T]{
l.start,
NewPoint(l.start.x, l.end.y),
NewPoint(l.end.x, l.start.y),
l.end,
}
return NewRectangle(points)
}
// Center calculates the midpoint of the line segment, optionally applying an epsilon
// threshold to adjust the precision of the result.
//
// Parameters:
// - opts: A variadic slice of [Option] functions to customize the behavior of the calculation.
// [WithEpsilon](epsilon float64): Specifies a tolerance for snapping near-integer or
// near-zero results to cleaner values, improving robustness in floating-point calculations.
//
// Behavior:
// - The midpoint is calculated by averaging the x and y coordinates of the start and end
// points of the line segment.
// - If [WithEpsilon] is provided, the resulting midpoint coordinates are adjusted such that
// small deviations due to floating-point precision errors are corrected.
//
// Returns:
// - [Point][float64]: The midpoint of the line segment as a point with floating-point coordinates,
// optionally adjusted based on epsilon.
//
// Notes:
// - Epsilon adjustment is particularly useful when working with floating-point coordinates
// where minor imprecision could affect the midpoint calculation.
// - The midpoint is always returned as [Point][float64], ensuring precision regardless of the
// coordinate type of the original line segment.
func (l LineSegment[T]) Center(opts ...Option) Point[float64] {
// Apply geomOptions with defaults
options := applyOptions(geomOptions{epsilon: 0}, opts...)
start := l.start.AsFloat64()
end := l.end.AsFloat64()
midX := (start.x + end.x) / 2
midY := (start.y + end.y) / 2
// Apply epsilon if specified
if options.epsilon > 0 {
midX = applyEpsilon(midX, options.epsilon)
midY = applyEpsilon(midY, options.epsilon)
}
return NewPoint[float64](midX, midY)
}
// ContainsPoint determines whether the given [Point] lies on the LineSegment.
//
// This method first checks if the [Point] is collinear with the endpoints of the
// [LineSegment] using an [Orientation]. If the [Point] is not collinear, it
// cannot be on the segment. If the [Point] is collinear, the function then verifies
// if the [Point] lies within the bounding box defined by the segment's endpoints.
//
// Parameters:
// - p ([Point][T]): The [Point] to test against the LineSegment
//
// Returns:
// - bool: true if the [Point] lies on the LineSegment, false otherwise.
func (l LineSegment[T]) ContainsPoint(p Point[T]) bool {
// Check collinearity first; if not collinear, point is not on the line segment
if Orientation(p, l.start, l.end) != PointsCollinear {
return false
}
// Check if the point lies within the bounding box defined by A and End
return p.x >= min(l.start.x, l.end.x) && p.x <= max(l.start.x, l.end.x) &&
p.y >= min(l.start.y, l.end.y) && p.y <= max(l.start.y, l.end.y)
}
// detailedRelationshipToLineSegment determines the spatial relationship between two line segments, l and other.
//
// - Let A = l.Start()
// - Let B = l.End()
// - Let C = other.Start()
// - Let D = other.End()
//
// This function evaluates the relationship between two line segments, AB and CD, by checking for
// endpoint coincidences, intersections, collinear relationships, and containment. It returns a
// [detailedLineSegmentRelationship] constant that describes the exact relationship between the segments,
// such as intersection, partial overlap, or full containment.
//
// The output constants may reference A, B, C, or D for brevity.
//
// Parameters:
// - other (LineSegment[T]): The line segment to compare with l.
// - opts: A variadic slice of [Option] functions to customize the behavior of the relationship check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing points and collinearity calculations,
// improving robustness against floating-point precision errors.
//
// Behavior:
// - The function first checks if the two line segments are exactly equal (or approximately equal if an epsilon is provided).
// - It evaluates endpoint coincidences, collinearity, intersection, and containment using orientation tests,
// point-on-segment checks, and direct comparisons.
// - If [WithEpsilon] is provided, epsilon adjustments are applied to point comparisons, collinearity checks, and
// point-on-segment tests to handle floating-point imprecision.
//
// Returns:
// - [detailedLineSegmentRelationship]: A constant describing the relationship between segments AB and CD.
//
// Notes:
// - Epsilon adjustment is particularly useful when working with floating-point coordinates, where small
// precision errors might lead to incorrect results.
func (l LineSegment[T]) detailedRelationshipToLineSegment(other LineSegment[T], opts ...Option) detailedLineSegmentRelationship {
// Check if segments are exactly equal
if (l.start.Eq(other.start, opts...) && l.end.Eq(other.end, opts...)) || (l.start.Eq(other.end, opts...) && l.end.Eq(other.start, opts...)) {
return lsrCollinearEqual
}
switch {
// Check if A and C coincide
case l.start.Eq(other.start, opts...):
return lsrAeqC
// Check if A and D coincide
case l.start.Eq(other.end, opts...):
return lsrAeqD
// Check if B and C coincide
case l.end.Eq(other.start, opts...):
return lsrBeqC
// Check if B and D coincide
case l.end.Eq(other.end, opts...):
return lsrBeqD
}
// Determine orientations for intersection and collinearity checks
o1 := Orientation(l.start, l.end, other.start)
o2 := Orientation(l.start, l.end, other.end)
o3 := Orientation(other.start, other.end, l.start)
o4 := Orientation(other.start, other.end, l.end)
// Non-collinear intersection cases
if o1 != o2 && o3 != o4 {
switch {
// Check if A lies on CD
case other.ContainsPoint(l.start) && !other.ContainsPoint(l.end):
return lsrAonCD
// Check if B lies on CD
case !other.ContainsPoint(l.start) && other.ContainsPoint(l.end):
return lsrBonCD
// Check if C lies on l
case l.ContainsPoint(other.start) && !l.ContainsPoint(other.end):
return lsrConAB
// Check if D lies on l
case !l.ContainsPoint(other.start) && l.ContainsPoint(other.end):
return lsrDonAB
// Default case that lines intersect without any "edge cases"
default:
return lsrIntersects
}
}
// PointsCollinear cases: All orientations are zero
if o1 == 0 && o2 == 0 && o3 == 0 && o4 == 0 {
// Check if segments are collinear and disjoint
if !other.ContainsPoint(l.start) && !other.ContainsPoint(l.end) &&
!l.ContainsPoint(other.start) && !l.ContainsPoint(other.end) {
return lsrCollinearDisjoint
}
// Check if AB is fully contained within CD
if other.ContainsPoint(l.start) && other.ContainsPoint(l.end) {
return lsrCollinearABinCD
}
// Check if CD is fully contained within AB
if l.ContainsPoint(other.start) && l.ContainsPoint(other.end) {
return lsrCollinearCDinAB
}
// Check specific collinear partial overlaps
if other.ContainsPoint(l.start) {
return lsrCollinearAonCD
}
if other.ContainsPoint(l.end) {
return lsrCollinearBonCD
}
}
// If none of the conditions matched, the segments are disjoint
return lsrMiss
}
// DistanceToLineSegment calculates the minimum distance between two line segments, l and other.
//
// If the segments intersect or touch at any point, the function returns 0, as the distance is effectively zero.
// Otherwise, it calculates the shortest distance by considering distances between segment endpoints and the
// perpendicular projections of each endpoint onto the opposite segment.
//
// Parameters:
// - other (LineSegment[T]): The line segment to compare with l.
// - opts: A variadic slice of [Option] functions to customize the behavior of the calculation.
// [WithEpsilon](epsilon float64): Specifies a tolerance for snapping near-zero results to zero or
// for handling small floating-point deviations in distance calculations.
//
// Behavior:
// - The function checks whether the segments intersect or touch. If so, the distance is immediately returned as 0.
// - For non-intersecting segments, the function calculates the shortest distance using the following steps:
// 1. Compute direct distances between the endpoints of l and other.
// 2. Compute distances to the perpendicular projections of each endpoint onto the opposite segment.
// 3. Track the minimum distance among all calculations and return this value.
// - If [WithEpsilon] is provided, epsilon adjustments are applied to the calculated distances and projections
// to ensure robustness against floating-point precision errors.
//
// Returns:
// - float64: The minimum distance between the two segments. If the segments intersect or touch, this value is 0.
func (l LineSegment[T]) DistanceToLineSegment(other LineSegment[T], opts ...Option) float64 {
// If line segments intersect, the distance is zero.
if l.IntersectsLineSegment(other) {
return 0
}
// Convert segments to float for precise calculations.
ABf, CDf := l.AsFloat64(), other.AsFloat64()
// Track the minimum distance.
minDist := math.MaxFloat64
// Helper function to update minimum distance.
updateMinDist := func(d float64) {
if d < minDist {
minDist = d
}
}
// Calculate distances between endpoints.
updateMinDist(ABf.start.DistanceToPoint(CDf.start, opts...))
updateMinDist(ABf.start.DistanceToPoint(CDf.end, opts...))
updateMinDist(ABf.end.DistanceToPoint(CDf.start, opts...))
updateMinDist(ABf.end.DistanceToPoint(CDf.end, opts...))
// Calculate distances to projections on the opposite segment.
updateMinDist(ABf.start.DistanceToPoint(ABf.start.ProjectOntoLineSegment(CDf), opts...))
updateMinDist(ABf.end.DistanceToPoint(ABf.end.ProjectOntoLineSegment(CDf), opts...))
updateMinDist(CDf.start.DistanceToPoint(CDf.start.ProjectOntoLineSegment(ABf), opts...))
updateMinDist(CDf.end.DistanceToPoint(CDf.end.ProjectOntoLineSegment(ABf), opts...))
return minDist
}
// DistanceToPoint calculates the orthogonal (shortest) distance from a specified [Point] p
// to the LineSegment l. This distance is determined by projecting the [Point] p onto the
// LineSegment and measuring the distance from p to the projected [Point].
//
// Parameters:
// - p ([Point][T]): The [Point] for which the distance to the LineSegment l is calculated.
// - opts: A variadic slice of [Option] functions to customize the behavior of the calculation.
// [WithEpsilon](epsilon float64): Specifies a tolerance for snapping near-zero results to zero
// or handling small floating-point deviations in distance calculations.
//
// Behavior:
// - The function computes the projection of p onto the given LineSegment l. This is the closest
// point on l to p, whether it lies within the segment bounds or at an endpoint.
// - The orthogonal distance is then calculated as the Euclidean distance between p and the
// projected point.
// - If [WithEpsilon] is provided, epsilon adjustments are applied to the calculated distance
// to ensure robustness against floating-point precision errors.
//
// Returns:
// - float64: The shortest distance between the [Point] p and the LineSegment l, optionally
// adjusted based on epsilon.
//
// Notes:
// - This function leverages the [Point.DistanceToLineSegment] method to perform the calculation,
// ensuring precision and consistency across related operations.
// - Epsilon adjustment is particularly useful for applications involving floating-point data,
// where small deviations can affect the accuracy of results.
func (l LineSegment[T]) DistanceToPoint(p Point[T], opts ...Option) float64 {
return p.DistanceToLineSegment(l, opts...)
}
// End returns the ending [Point] of the line segment.
//
// This function provides access to the ending [Point] of the line segment l, typically representing
// the endpoint of the segment.
func (l LineSegment[T]) End() Point[T] {
return l.end
}
// Eq checks if two line segments are equal by comparing their start and end points.
// Equality can be evaluated either exactly (default) or approximately using an epsilon threshold.
//
// Parameters:
// - other (LineSegment[T]): The line segment to compare with the current line segment.
// - opts: A variadic slice of [Option] functions to customize the equality check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing the start and end
// points of the line segments. If the absolute difference between the coordinates of
// the points is less than epsilon, they are considered equal.
//
// Behavior:
// - By default, the function performs an exact equality check, returning true only if
// both the start and end points of l and other are identical.
// - If [WithEpsilon] is provided, the function performs an approximate equality check,
// considering the points equal if their coordinate differences are within the specified
// epsilon threshold.
//
// Returns:
// - bool: Returns true if both line segments have identical (or approximately equal with epsilon) start
// and end points; otherwise, false.
//
// Notes:
// - Approximate equality is useful when comparing line segments with floating-point coordinates,
// where small precision errors might otherwise cause inequality.
// - This function relies on the [Point.Eq] method, which supports epsilon adjustments.
func (l LineSegment[T]) Eq(other LineSegment[T], opts ...Option) bool {
return l.start.Eq(other.start, opts...) && l.end.Eq(other.end, opts...)
}
// IntersectionPoint calculates the intersection [Point] between two line segments, if one exists.
//
// This method checks if the LineSegment l and LineSegment other intersect within their boundaries
// and, if so, calculates and returns the intersection point. If the segments do not intersect
// or are parallel, it returns a zero-value [Point] and false.
//
// It uses the parametric form of the line segments to solve for intersection parameters t and u.
// If t and u are both in the range [0, 1], the intersection point lies within the bounds of
// both segments.
//
// Parameters:
// - other (LineSegment[T]): The second line segment to check for an intersection.
//
// Returns:
// - [Point][T]: The intersection point.
// - bool: If true, the first element is the intersection point. If false, there is
// no intersection within the segments’ bounds or the segments are parallel.
//
// todo: update doc comments and reorder
func (l LineSegment[T]) IntersectionGeometry(other LineSegment[T], opts ...Option) LineSegmentIntersectionResult[float64] {
// Apply options with defaults
options := applyOptions(geomOptions{epsilon: 1e-10}, opts...)
// Define segment endpoints for AB (l) and CD (other)
A, B := l.start.AsFloat64(), l.end.AsFloat64()
C, D := other.start.AsFloat64(), other.end.AsFloat64()
// Calculate the direction vectors
dir1 := B.Translate(A.Negate())
dir2 := D.Translate(C.Negate())
// Calculate the determinants
denominator := dir1.CrossProduct(dir2)
// Handle collinear case (denominator == 0)
if denominator == 0 {
// Check if the segments are collinear
AC := C.Translate(A.Negate())
if AC.CrossProduct(dir1) != 0 {
return LineSegmentIntersectionResult[float64]{IntersectionType: IntersectionNone} // Parallel but not collinear
}
// Check overlap by projecting points onto the line
tStart := (C.Translate(A.Negate())).DotProduct(dir1) / dir1.DotProduct(dir1)
tEnd := (D.Translate(A.Negate())).DotProduct(dir1) / dir1.DotProduct(dir1)
// Ensure tStart < tEnd for consistency
if tStart > tEnd {
tStart, tEnd = tEnd, tStart
}
// Check for overlap
tOverlapStart := max(0.0, tStart)
tOverlapEnd := min(1.0, tEnd)
if tOverlapStart > tOverlapEnd {
return LineSegmentIntersectionResult[float64]{IntersectionType: IntersectionNone} // No overlap
}
// Calculate the overlapping segment
overlapStart := RoundPointToEpsilon(A.Translate(dir1.Scale(NewPoint[float64](0, 0), tOverlapStart)), options.epsilon)
overlapEnd := RoundPointToEpsilon(A.Translate(dir1.Scale(NewPoint[float64](0, 0), tOverlapEnd)), options.epsilon)
return LineSegmentIntersectionResult[float64]{
IntersectionType: IntersectionSegment,
IntersectionSegment: NewLineSegment(overlapStart, overlapEnd),
}
}
// Calculate parameters t and u for non-collinear case
AC := C.Translate(A.Negate())
tNumerator := AC.CrossProduct(dir2)
uNumerator := AC.CrossProduct(dir1)
t := tNumerator / denominator
u := uNumerator / denominator
// Check if intersection occurs within the segment bounds
if t < 0 || t > 1 || u < 0 || u > 1 {
return LineSegmentIntersectionResult[float64]{IntersectionType: IntersectionNone} // Intersection is outside the segments
}
// Calculate the intersection point
intersection := RoundPointToEpsilon(A.Translate(dir1.Scale(NewPoint[float64](0, 0), t)), options.epsilon)
return LineSegmentIntersectionResult[float64]{
IntersectionType: IntersectionPoint,
IntersectionPoint: intersection,
}
}
// IntersectsLineSegment checks whether there is any intersection or overlap between LineSegment l and LineSegment other.
//
// This function returns true if segments l and other have an intersecting spatial relationship, such as intersection,
// overlap, containment, or endpoint coincidence.
//
// Parameters:
// - other (LineSegment[T]): The line segment to compare with l.
//
// Returns:
// - bool: Returns true if l and other intersect, overlap, or share any form of intersecting relationship, and false if they are completely disjoint.
func (l LineSegment[T]) IntersectsLineSegment(other LineSegment[T]) bool {
return l.detailedRelationshipToLineSegment(other) > lsrMiss
}
// Length calculates the length of the line segment, optionally using an epsilon threshold
// to adjust the precision of the calculation.
//
// Parameters:
// - opts: A variadic slice of [Option] functions to customize the behavior of the calculation.
// [WithEpsilon](epsilon float64): Specifies a tolerance for snapping small floating-point
// deviations in the calculated length to cleaner values, improving robustness.
//
// Behavior:
// - The function computes the Euclidean distance between the start and end points of the
// line segment using the [LineSegment.DistanceToPoint] method.
// - If [WithEpsilon] is provided, the resulting length is adjusted such that small deviations
// due to floating-point precision errors are corrected.
//
// Returns:
// - float64: The length of the line segment, optionally adjusted based on epsilon.
//
// Notes:
// - This function relies on [LineSegment.DistanceToPoint], which supports epsilon adjustments for distance
// calculations. Epsilon is particularly useful for floating-point coordinates where minor
// imprecision might affect the result.
func (l LineSegment[T]) Length(opts ...Option) float64 {
return l.start.DistanceToPoint(l.end, opts...)
}
// Normalize returns a "normalized" version of the line segment where the start point
// is guaranteed to be the leftmost-lowest point. This ensures that:
// - The point with the smallest X-coordinate is the start point.
// - If the X-coordinates are equal, the point with the smallest Y-coordinate is the start point.
//
// This normalization is useful for algorithms that require consistent ordering of line segments,
// such as the Bentley-Ottmann sweep line algorithm or Boolean operations on polygons.
//
// Returns a new LineSegment with the points swapped if necessary.
func (l LineSegment[T]) Normalize() LineSegment[T] {
// if start point is not leftest-lowest, swap points
if l.start.x > l.end.x || (l.start.x == l.end.x && l.start.y > l.end.y) {
return NewLineSegment[T](l.end, l.start)
}
// else, return original point
return l
}
// Points returns the two endpoints of the line segment as a slice of Points.
// The order of the points is [start, end].
//
// Returns:
// - [][Point][T]: A slice containing the start and end points of the line segment.
func (l LineSegment[T]) Points() []Point[T] {
return []Point[T]{l.start, l.end}
}
// Reflect reflects the line segment across the specified axis or custom line.
//
// Parameters:
// - axis ([ReflectionAxis]): The axis or line to reflect across ([ReflectAcrossXAxis], [ReflectAcrossYAxis], or [ReflectAcrossCustomLine]).
// - line (LineSegment[float64]): Optional. The line segment for [ReflectAcrossCustomLine] reflection.
//
// Returns:
// - LineSegment[float64] - A new line segment where both endpoints are reflected accordingly.
func (l LineSegment[T]) Reflect(axis ReflectionAxis, line ...LineSegment[float64]) LineSegment[float64] {
var startReflected, endReflected Point[float64]
switch axis {
case ReflectAcrossXAxis:
// Reflect across the x-axis
startReflected = l.start.Reflect(ReflectAcrossXAxis)
endReflected = l.end.Reflect(ReflectAcrossXAxis)
case ReflectAcrossYAxis:
// Reflect across the y-axis
startReflected = l.start.Reflect(ReflectAcrossYAxis)
endReflected = l.end.Reflect(ReflectAcrossYAxis)
case ReflectAcrossCustomLine:
// Reflect across a custom line if provided
if len(line) > 0 {
startReflected = l.start.Reflect(ReflectAcrossCustomLine, line[0])
endReflected = l.end.Reflect(ReflectAcrossCustomLine, line[0])
} else {
// No custom line provided; return the original line segment unchanged
return l.AsFloat64()
}
default:
// Invalid axis, return the line segment unchanged
return l.AsFloat64()
}
// Return a new line segment with reflected points
return NewLineSegment(startReflected, endReflected)
}
// RelationshipToCircle determines the spatial relationship between the current LineSegment and a Circle.
//
// This function evaluates whether the LineSegment:
// - Intersects the circle at any point.
// - Lies entirely within the circle.
// - Lies entirely outside the circle.
//
// Parameters:
// - c ([Circle][T]): The circle to compare with the line segment.
// - opts: A variadic slice of [Option] functions to customize the behavior of the relationship check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing distances, improving robustness against
// floating-point precision errors.
//
// Behavior:
// - The function calculates the distance between the circle's center and the endpoints of the line segment,
// as well as the shortest distance between the circle's center and the line segment itself.
// - If any of these distances are exactly equal to the circle's radius, the function determines that the
// line segment intersects the circle.
// - If both endpoints of the line segment lie within the circle, the function determines that the line segment
// is contained within the circle.
// - Otherwise, the function determines that the line segment lies entirely outside the circle.
//
// Returns:
//
// [Relationship]: A constant indicating the relationship of the line segment to the circle. Possible values are:
// - [RelationshipDisjoint]: The line segment lies entirely outside the circle.
// - [RelationshipIntersection]: The line segment intersects the circle at one or more points.
// - [RelationshipContainedBy]: The line segment lies entirely within the circle.
//
// Notes:
// - Epsilon adjustment is particularly useful when working with floating-point coordinates,
// where minor precision errors could otherwise lead to incorrect results.
func (l LineSegment[T]) RelationshipToCircle(c Circle[T], opts ...Option) Relationship {
circleFloat := c.AsFloat64()
distanceCircleCenterToLineSegment := c.center.DistanceToLineSegment(l, opts...)
distanceCircleCenterToLineSegmentStart := c.center.DistanceToPoint(l.start, opts...)
distanceCircleCenterToLineSegmentEnd := c.center.DistanceToPoint(l.end, opts...)
// check for intersection
if distanceCircleCenterToLineSegmentStart == circleFloat.radius ||
distanceCircleCenterToLineSegmentEnd == circleFloat.radius ||
distanceCircleCenterToLineSegment == circleFloat.radius {
return RelationshipIntersection
}
if (distanceCircleCenterToLineSegmentStart > circleFloat.radius || distanceCircleCenterToLineSegmentEnd > circleFloat.radius) &&
distanceCircleCenterToLineSegment <= circleFloat.radius {
return RelationshipIntersection
}
// check for containment
if distanceCircleCenterToLineSegmentStart < circleFloat.radius &&
distanceCircleCenterToLineSegmentEnd < circleFloat.radius {
return RelationshipContainedBy
}
return RelationshipDisjoint
}
// RelationshipToLineSegment determines the high-level spatial relationship between two line segments.
//
// It categorizes the relationship as:
// - Disjoint (no intersection or overlap).
// - Intersection (the segments intersect at one or more points).
// - Equal (both segments are collinear and share the same endpoints).
//
// Parameters:
// - other ([LineSegment][T]): The line segment to compare against the current one.
// - opts: A variadic slice of [Option] functions to customize the behavior of the relationship check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing points and collinearity calculations,
// allowing for robust handling of floating-point precision errors.
//
// Behavior:
//
// The detailed relationship mapped to a [Relationship] constant:
// - [RelationshipDisjoint]: If the segments are collinear but disjoint, or if they miss entirely.
// - [RelationshipIntersection]: The segments intersect at one or more points.
// - [RelationshipEqual]: If the segments are collinear and exactly equal (share the same endpoints).
//
// Returns:
// - [Relationship]: A constant describing the high-level relationship between the two line segments.
//
// Notes:
// - The use of epsilon adjustments ensures robustness against floating-point imprecision.
func (l LineSegment[T]) RelationshipToLineSegment(other LineSegment[T], opts ...Option) Relationship {
rel := l.detailedRelationshipToLineSegment(other, opts...)
switch rel {
case lsrCollinearDisjoint, lsrMiss:
return RelationshipDisjoint
case lsrCollinearEqual:
return RelationshipEqual
default:
return RelationshipIntersection
}
}
// RelationshipToPoint determines the high-level spatial relationship between a line segment and a point.
//
// This function evaluates how a line segment relates to a point by delegating the computation to the
// [Point.RelationshipToLineSegment] method. The relationship is determined based on whether the point
// lies on the segment, coincides with an endpoint, or is disjoint from the segment.
//
// Parameters:
// - p ([Point][T]): The point to compare against the line segment.
// - opts: A variadic slice of [Option] functions to customize the behavior of the relationship check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing distances and collinearity calculations,
// ensuring robust handling of floating-point precision errors.
//
// Behavior:
// - The function calls the [Point.RelationshipToLineSegment] method for computation.
// - The returned relationship constant describes whether the point is on the line segment ([RelationshipIntersection]), or
// disjoint from the line segment ([RelationshipDisjoint]).
//
// Returns:
// - [Relationship]: A constant describing the relationship between the line segment and the point.
//
// Notes:
// - Epsilon adjustment ensures robustness against floating-point imprecision.
func (l LineSegment[T]) RelationshipToPoint(p Point[T], opts ...Option) Relationship {
return p.RelationshipToLineSegment(l, opts...)
}
// RelationshipToPolyTree determines the relationship between a line segment and each polygon in a [PolyTree].
//
// This function evaluates how a line segment relates to the polygons in the tree, computing whether the segment
// is disjoint, intersects any edge, or is fully contained within a polygon. The result is returned as a map,
// where each key is a pointer to a polygon in the [PolyTree], and the value is the relationship between the
// line segment and that polygon.
//
// Parameters:
// - pt (*[PolyTree][T]): The [PolyTree] to analyze for relationships.
// - opts: A variadic slice of [Option] functions to customize the behavior of the relationship check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing distances and collinearity calculations,
// ensuring robust handling of floating-point precision errors.
//
// Behavior:
//
// The function first checks if the line segment intersects any edge of each polygon.
// - If an intersection or equality is found, the relationship is set to [RelationshipIntersection].
// - If the segment's start and end points are both contained within a polygon, the relationship is set to [RelationshipContainedBy].
// - If neither of the above conditions is satisfied, the relationship defaults to [RelationshipDisjoint].
//
// Returns:
// - map[*PolyTree[T]]Relationship: A map where the keys are polygons in the [PolyTree] and the values are their
// relationships with the line segment.
//
// Notes:
// - Epsilon adjustment ensures robustness against floating-point imprecision.
// - The function assumes polygons in the [PolyTree] have doubled points for accurate containment checks.
func (l LineSegment[T]) RelationshipToPolyTree(pt *PolyTree[T], opts ...Option) map[*PolyTree[T]]Relationship {
lDoubled := NewLineSegment[T](NewPoint[T](l.start.x*2, l.start.y*2), NewPoint[T](l.end.x*2, l.end.y*2))
output := make(map[*PolyTree[T]]Relationship, pt.Len())
RelationshipToPolyTreeIterPolys:
for poly := range pt.Nodes {
// check for intersection
for edge := range poly.contour.iterEdges {
rel := lDoubled.RelationshipToLineSegment(edge, opts...)
if rel == RelationshipIntersection || rel == RelationshipEqual {
output[poly] = RelationshipIntersection
continue RelationshipToPolyTreeIterPolys
}
}
// check for containment
lineStartInPoly := poly.contour.isPointInside(lDoubled.start)
lineEndInPoly := poly.contour.isPointInside(lDoubled.end)
if lineStartInPoly && lineEndInPoly {
output[poly] = RelationshipContainedBy
continue RelationshipToPolyTreeIterPolys
}
// otherwise, disjoint
output[poly] = RelationshipDisjoint
}
return output
}
// RelationshipToRectangle determines the high-level spatial relationship between a line segment and a rectangle.
//
// This function evaluates how a line segment relates to a rectangle, considering possible intersections,
// containment, or disjoint relationships. The function iterates through the rectangle's edges to check for
// intersections with the line segment and verifies whether the line segment is entirely contained within
// the rectangle.
//
// Parameters:
// - r ([Rectangle][T]): The rectangle to compare against the line segment.
// - opts: A variadic slice of [Option] functions to customize the behavior of the relationship check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing points and collinearity calculations,
// ensuring robust handling of floating-point precision errors.
//
// Behavior:
//
// The function checks each edge of the rectangle against the line segment:
// - If any edge intersects or is equal to the line segment, the function returns [RelationshipIntersection].
// - If both endpoints of the line segment are contained within the rectangle, the function returns [RelationshipContainedBy].
// - If no intersection or containment is found, the function returns [RelationshipDisjoint].
//
// Returns:
// - [Relationship]: A constant describing the relationship between the line segment and the rectangle.
//
// Notes:
// - Epsilon adjustment ensures robustness against floating-point imprecision.
func (l LineSegment[T]) RelationshipToRectangle(r Rectangle[T], opts ...Option) Relationship {
for _, edge := range r.Edges() {
rel := edge.RelationshipToLineSegment(l, opts...)
if rel == RelationshipIntersection || rel == RelationshipEqual {
return RelationshipIntersection
}
}
if r.ContainsPoint(l.start) && r.ContainsPoint(l.end) {
return RelationshipContainedBy
}
return RelationshipDisjoint
}
// Rotate rotates the LineSegment around a given pivot [Point] by a specified angle in radians counterclockwise.
// Optionally, an epsilon threshold can be applied to adjust the precision of the resulting coordinates.
//
// Parameters:
// - pivot ([Point][T]): The point around which to rotate the line segment.
// - radians (float64): The rotation angle in radians.
// - opts: A variadic slice of [Option] functions to customize the behavior of the rotation.
// [WithEpsilon](epsilon float64): Specifies a tolerance for snapping near-zero or near-integer
// values in the resulting coordinates to cleaner values, improving robustness.
//
// Behavior:
// - The function rotates the start and end points of the line segment around the given pivot
// point by the specified angle using the [Point.Rotate] method.
// - If [WithEpsilon] is provided, epsilon adjustments are applied to the rotated coordinates to
// handle floating-point precision errors.
//
// Returns:
// - LineSegment[float64]: A new line segment representing the rotated position, with floating-point coordinates.
//
// Notes:
// - Epsilon adjustment is particularly useful when the rotation involves floating-point
// calculations that could result in minor inaccuracies.
// - The returned line segment always has float64 coordinates, ensuring precision regardless
// of the coordinate type of the original line segment.
func (l LineSegment[T]) Rotate(pivot Point[T], radians float64, opts ...Option) LineSegment[float64] {
newStart := l.start.Rotate(pivot, radians, opts...)
newEnd := l.end.Rotate(pivot, radians, opts...)
return NewLineSegment(newStart, newEnd)
}
// RoundToEpsilon returns a new LineSegment where the coordinates of both the start
// and end points are rounded to the nearest multiple of the given epsilon.
//
// Parameters:
// - epsilon: The value to which the coordinates should be rounded.
//
// Returns:
//
// A new LineSegment with rounded coordinates.
//
// Notes:
// - The epsilon value must be greater than 0. If it is 0 or negative,
// the function will panic.
func (l LineSegment[T]) RoundToEpsilon(epsilon float64) LineSegment[float64] {
return NewLineSegment(
RoundPointToEpsilon(l.start.AsFloat64(), epsilon),
RoundPointToEpsilon(l.end.AsFloat64(), epsilon),
)
}
// Scale scales the line segment by a given factor from a specified reference point.