-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpolytree.go
3052 lines (2746 loc) · 116 KB
/
polytree.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"
"slices"
"strings"
)
// BooleanOperation defines the types of Boolean operations that can be performed on polygons.
// These operations are fundamental in computational geometry for combining or modifying shapes.
type BooleanOperation uint8
// Valid values for BooleanOperation
const (
// BooleanUnion represents the union operation, which combines two polygons into a single polygon
// that encompasses the areas of both input polygons. Overlapping regions are merged.
BooleanUnion BooleanOperation = iota
// BooleanIntersection represents the intersection operation, which computes the region(s)
// where two polygons overlap. The result is one or more polygons that covers only the shared area.
BooleanIntersection
// BooleanSubtraction represents the subtraction operation, which removes the area of one polygon
// from another. The result is one or more polygons representing the area of the first polygon excluding
// the overlapping region with the second polygon.
BooleanSubtraction
)
// String returns the string representation of a BooleanOperation.
// This is useful for debugging and logging purposes.
//
// Supported operations:
// - BooleanUnion: "BooleanUnion"
// - BooleanIntersection: "BooleanIntersection"
// - BooleanSubtraction: "BooleanSubtraction"
//
// Panics if an unsupported BooleanOperation is encountered.
func (o *BooleanOperation) String() string {
switch *o {
case BooleanUnion:
return "BooleanUnion"
case BooleanIntersection:
return "BooleanIntersection"
case BooleanSubtraction:
return "BooleanSubtraction"
default:
panic(fmt.Errorf("unsupported BooleanOperation"))
}
}
// NewPolyTreeOption defines a functional option type for configuring a new [PolyTree] during creation.
//
// This type allows for flexible and extensible initialization of [PolyTree] objects by applying optional
// configurations after the core properties have been set.
//
// Parameters:
// - T: The numeric type of the coordinates in the [PolyTree], constrained by the [SignedNumber] interface.
//
// This pattern makes it easy to add optional properties to a PolyTree without requiring an extensive list
// of parameters in the NewPolyTree function.
type NewPolyTreeOption[T SignedNumber] func(*PolyTree[T]) error
// WithChildren is an option for the [NewPolyTree] function that assigns child polygons to the created [PolyTree].
// It also sets up parent-child relationships and orders the children for consistency.
//
// Parameters:
// - children: A variadic list of pointers to [PolyTree] objects representing the child polygons.
//
// Behavior:
// - The function assigns the provided children to the [PolyTree] being created.
// - It establishes the parent-child relationship by setting the parent of each child to the newly created [PolyTree].
//
// Returns:
// - A [NewPolyTreeOption] that can be passed to the [NewPolyTree] function.
func WithChildren[T SignedNumber](children ...*PolyTree[T]) NewPolyTreeOption[T] {
return func(p *PolyTree[T]) error {
for _, child := range children {
err := p.AddChild(child)
if err != nil {
return err
}
}
return nil
}
}
// WithSiblings is an option for the [NewPolyTree] function that assigns sibling polygons to the created [PolyTree].
//
// Parameters:
// - siblings: A variadic list of pointers to [PolyTree] objects representing the sibling polygons.
//
// Behavior:
// - The function assigns the provided children to the [PolyTree] being created.
//
// Returns:
// - A [NewPolyTreeOption] that can be passed to the [NewPolyTree] function.
func WithSiblings[T SignedNumber](siblings ...*PolyTree[T]) NewPolyTreeOption[T] {
return func(p *PolyTree[T]) error {
for _, sibling := range siblings {
err := p.AddSibling(sibling)
if err != nil {
return err
}
}
return nil
}
}
// Valid values for PolygonType
const (
// PTSolid represents a solid region of the polygon, commonly referred to as an "island."
// PTSolid polygons are the primary filled areas, excluding any void regions (holes).
PTSolid PolygonType = iota
// PTHole represents a void region of the polygon, often nested within a solid polygon.
// Holes are not part of the filled area of the polygon and are treated as exclusions.
PTHole
)
// String returns a string representation of the [PolygonType].
//
// This method converts the enum value of a [PolygonType] into a human-readable string,
// making it useful for debugging, logging, or providing textual representations.
//
// Returns:
// - string: A string representation of the [PolygonType], such as [PTSolid] or [PTHole].
//
// Panics:
// - If the [PolygonType] value is unsupported, the method will panic with an appropriate error message.
//
// Notes:
// - This method assumes that the [PolygonType] is valid. If new polygon types are added
// in the future, ensure this method is updated to include them to avoid panics.
func (t PolygonType) String() string {
switch t {
case PTSolid:
return "PTSolid"
case PTHole:
return "PTHole"
default:
panic(fmt.Errorf("unsupported PolygonType"))
}
}
// Valid values for polyIntersectionType
const (
// intersectionTypeNotSet indicates that the intersection type has not been set.
// This is the default value for uninitialized points or non-intersection points.
intersectionTypeNotSet polyIntersectionType = iota
// intersectionTypeEntry indicates that the point serves as an entry point to the area of interest
// when traversing the polygon during an operation.
intersectionTypeEntry
// intersectionTypeExit indicates that the point serves as an exit point from the area of interest
// when traversing the polygon during an operation.
intersectionTypeExit
)
// polyPointType defines the type of point in a polygon, used to distinguish between
// original vertices and additional points introduced during polygon operations.
//
// This type is essential for managing polygon data during Boolean operations (e.g., union,
// intersection, subtraction) and other algorithms that require distinguishing original
// points from dynamically added points.
type polyPointType uint8
// Valid values for polyPointType
const (
// pointTypeOriginal indicates that the point is an original, unmodified vertex of the polygon.
// These points are part of the polygon's initial definition.
pointTypeOriginal polyPointType = iota
// pointTypeOriginalAndIntersection indicates that the original point is also an intersection point
// that was identified during a polygon operation.
pointTypeOriginalAndIntersection
// pointTypeAddedIntersection indicates that the point is an intersection point that was added
// during a polygon operation. These points are not part of the polygon's original definition
// but are dynamically introduced for computational purposes.
pointTypeAddedIntersection
)
func (t polyPointType) String() string {
switch t {
case pointTypeOriginal:
return "pointTypeOriginal"
case pointTypeOriginalAndIntersection:
return "pointTypeOriginalAndIntersection"
case pointTypeAddedIntersection:
return "pointTypeAddedIntersection"
default:
panic(fmt.Errorf("unsupported polyPointType"))
}
}
// Valid values for polyTraversalDirection
const (
// polyTraversalForward specifies that the traversal proceeds
// in a counterclockwise direction through the polygon's vertices or edges.
polyTraversalForward = polyTraversalDirection(iota)
// polyTraversalReverse specifies that the traversal proceeds
// in a clockwise direction through the polygon's vertices or edges.
polyTraversalReverse
)
// PolyTreeMismatch represents a bitmask of potential mismatches between two PolyTree structures.
type PolyTreeMismatch uint8
// Valid values for PolyTreeMismatch
const (
// PTMNoMismatch indicates that there is no mismatch between the compared PolyTree structures.
PTMNoMismatch PolyTreeMismatch = 0 << iota
// PTMNilPolygonMismatch indicates that one of the PolyTree structures is nil while the other is not.
PTMNilPolygonMismatch PolyTreeMismatch = 1
// PTMContourMismatch indicates that the contours of the compared PolyTree structures are not identical.
// This could mean differences in the points, their order, or their overall shape.
PTMContourMismatch PolyTreeMismatch = 2
// PTMSiblingMismatch indicates that the siblings of the compared PolyTree structures do not match.
// This could mean differences in the number of siblings, their contours, or their structure.
PTMSiblingMismatch PolyTreeMismatch = 4
// PTMChildMismatch indicates that the children of the compared PolyTree structures do not match.
// This could mean differences in the number of children, their contours, or their structure.
PTMChildMismatch PolyTreeMismatch = 8
)
// entryExitPointLookUpTable is a lookup table that determines the entry/exit type of intersection points
// between two polygons based on their types and the Boolean operation being performed.
//
// The structure of the lookup table is as follows:
//
// map[BooleanOperation]map[PolygonType]map[PolygonType]map[bool]struct{
// poly1PointType polyIntersectionType
// poly2PointType polyIntersectionType
// }
//
// Keys:
// - BooleanOperation: The type of Boolean operation (e.g., Union, Intersection, Subtraction).
// - PolygonType: The type of the polygons being compared (PTSolid or PTHole).
// - bool: Whether the traversal direction indicates that poly1 is entering poly2 (true) or exiting (false).
//
// Values:
// - poly1PointType: The intersection type for the current point in poly1.
// - poly2PointType: The intersection type for the current point in poly2.
//
// Example Usage:
// When determining how to mark intersection points during a Boolean operation,
// this table provides the correct entry and exit types for each polygon.
//
// Structure:
//
// For BooleanUnion:
// - Solid-Solid: Determines entry/exit depending on whether poly1 is entering or exiting poly2.
// - Solid-Hole: Adjusts the entry/exit direction for the hole.
//
// For BooleanIntersection:
// - Adjusts entry/exit depending on whether the traversal is into or out of holes and solids.
//
// For BooleanSubtraction:
// - Marks both intersection points as exits or entries, depending on traversal.
var entryExitPointLookUpTable = map[BooleanOperation]map[PolygonType]map[PolygonType]map[bool]struct {
poly1PointType polyIntersectionType
poly2PointType polyIntersectionType
}{
BooleanUnion: {
PTSolid: {
PTSolid: {
true: {
poly1PointType: intersectionTypeExit,
poly2PointType: intersectionTypeEntry,
},
false: {
poly1PointType: intersectionTypeEntry,
poly2PointType: intersectionTypeExit,
},
},
PTHole: {
true: {
poly1PointType: intersectionTypeEntry,
poly2PointType: intersectionTypeExit,
},
false: {
poly1PointType: intersectionTypeExit,
poly2PointType: intersectionTypeEntry,
},
},
},
PTHole: {
PTSolid: {
true: {
poly1PointType: intersectionTypeExit,
poly2PointType: intersectionTypeEntry,
},
false: {
poly1PointType: intersectionTypeEntry,
poly2PointType: intersectionTypeExit,
},
},
PTHole: {
true: {
poly1PointType: intersectionTypeEntry,
poly2PointType: intersectionTypeExit,
},
false: {
poly1PointType: intersectionTypeExit,
poly2PointType: intersectionTypeEntry,
},
},
},
},
BooleanIntersection: {
PTSolid: {
PTSolid: {
true: {poly1PointType: intersectionTypeEntry, poly2PointType: intersectionTypeExit},
false: {poly1PointType: intersectionTypeExit, poly2PointType: intersectionTypeEntry},
},
PTHole: {
true: {poly1PointType: intersectionTypeExit, poly2PointType: intersectionTypeEntry},
false: {poly1PointType: intersectionTypeEntry, poly2PointType: intersectionTypeExit},
},
},
PTHole: {
PTSolid: {
true: {poly1PointType: intersectionTypeEntry, poly2PointType: intersectionTypeExit},
false: {poly1PointType: intersectionTypeExit, poly2PointType: intersectionTypeEntry},
},
PTHole: {
true: {poly1PointType: intersectionTypeExit, poly2PointType: intersectionTypeEntry},
false: {poly1PointType: intersectionTypeEntry, poly2PointType: intersectionTypeExit},
},
},
},
BooleanSubtraction: {
PTSolid: {
PTSolid: {
true: {poly1PointType: intersectionTypeExit, poly2PointType: intersectionTypeExit},
false: {poly1PointType: intersectionTypeEntry, poly2PointType: intersectionTypeEntry},
},
PTHole: {
true: {poly1PointType: intersectionTypeEntry, poly2PointType: intersectionTypeEntry},
false: {poly1PointType: intersectionTypeExit, poly2PointType: intersectionTypeExit},
},
},
PTHole: {
PTSolid: {
true: {poly1PointType: intersectionTypeExit, poly2PointType: intersectionTypeExit},
false: {poly1PointType: intersectionTypeEntry, poly2PointType: intersectionTypeEntry},
},
PTHole: {
true: {poly1PointType: intersectionTypeEntry, poly2PointType: intersectionTypeEntry},
false: {poly1PointType: intersectionTypeExit, poly2PointType: intersectionTypeExit},
},
},
},
}
// PolygonType (PT) defines whether the inside of the contour of a polygon represents either a solid region (island)
// or a void region (hole). This distinction is essential for operations involving polygons
// with complex structures, such as those containing holes or nested islands.
type PolygonType uint8
// PolyTree represents a polygon that can be part of a hierarchical structure. It supports
// complex polygons with holes and islands and enables operations like unions, intersections,
// and subtractions.
//
// A PolyTree has the following characteristics:
// - A contour: The outer boundary of the polygon, represented as a sequence of vertices.
// This includes all the original points, intersection points, and added midpoints as needed
// for polygon operations.
// - A [PolygonType]: Indicates whether the polygon is a solid region ([PTSolid]) or a hole ([PTHole]).
// This classification is essential for understanding the relationship between the polygon
// and its children.
// - A parent: a pointer to the parent polygon in the hierarchy. For example, a hole's parent
// would be the solid polygon that contains it. If a polygon is the root polygon in the PolyTree, its parent is nil.
// - Zero or more siblings: A list of sibling polygons that are not nested within each other but share
// the same parent. Siblings must be of the same [PolygonType].
// - Zero or more children: A list of child polygons nested within this polygon. If the [PolygonType] is
// [PTSolid], the children are holes ([PTHole]). If the [PolygonType] is [PTHole], the children
// are solid islands ([PTSolid]).
//
// Hierarchy Rules:
// - A solid polygon ([PTSolid]) can contain holes ([PTHole]) as its children.
// - A hole ([PTHole]) can contain solid polygons ([PTSolid]) as its children.
// - Siblings are polygons of the same [PolygonType] that do not overlap.
//
// These relationships form a tree structure, where each node is a PolyTree, allowing for
// complex geometric modeling and operations.
//
// Note: Internal optimizations, such as convex hull caching and point indexing, are abstracted
// away from the user.
type PolyTree[T SignedNumber] struct {
// contour defines the complete outline of the polygon, including all vertices, intersection points,
// and midpoints added during boolean operations. The points in the contour are doubled to avoid
// precision issues (e.g., rounding errors or loss of accuracy) when calculating midpoints, such as
// during entry/exit point determination for boolean operations. This ensures accurate and consistent
// results, especially when working with integer-based coordinates.
contour contour[T]
// polygonType specifies whether the polygon is solid (PTSolid) or a hole (PTHole).
// This determines the interpretation of its children: solids contain holes, and holes contain solids.
polygonType PolygonType
// siblings stores polygons of the same type that share the same parent. This maintains
// adjacency relationships between polygons without nesting.
siblings []*PolyTree[T]
// children contains polygons nested within this polygon. These are either holes (for solids)
// or solid islands (for holes), forming a hierarchical structure.
children []*PolyTree[T]
// parent refers to the polygon that contains this one, maintaining the tree structure.
// A nil parent indicates that this polygon is the root of the hierarchy.
parent *PolyTree[T]
// hull caches the convex hull of the polygon for faster point-in-polygon checks.
hull simpleConvexPolygon[T]
}
// contour represents the outline of a polygon as a slice of polyTreePoint entries.
// Each entry contains metadata about the point, such as whether it is a normal vertex,
// an intersection point, or a midpoint between intersections.
//
// The contour is used to define the polygon's shape and is processed during boolean
// operations. Contour within the contour are typically doubled to facilitate calculations
// involving midpoints and to avoid precision issues when working with integer-based
// coordinates.
type contour[T SignedNumber] []polyTreePoint[T]
// insertIntersectionPoint inserts an intersection point into a contour between two specified indices.
// The insertion position is determined based on the proximity of the intersection point to the
// corresponding line segment, ensuring correct ordering for polygon operations.
//
// Parameters:
// - start: The index of the starting point of the line segment in the contour.
// - end: The index of the ending point of the line segment in the contour.
// - intersection: The intersection point to insert, represented as a polyTreePoint.
//
// Notes:
// - The function assumes that start and end indices are valid and start < end.
// - The insertion logic ensures that the intersection is placed in the correct position relative to
// other intermediate points, preserving the geometric consistency of the contour.
// - Simply inserting at the start index is not sufficient because the contour may already contain
// intermediate points between start and end. Proper ordering is necessary to maintain the
// validity of polygon operations such as traversals and Boolean operations.
func (c *contour[T]) insertIntersectionPoint(start, end int, intersection polyTreePoint[T]) {
// Define the line segment between the start and end points.
segment := NewLineSegment((*c)[start].point, (*c)[end].point)
// Initialize the insertion position to the end index.
insertPos := end
// Iterate through the intermediate points in the contour between start and end.
// Find the correct position to insert the intersection point.
for i := start + 1; i < end; i++ {
// Define the segment from the start point to the current point.
existingSegment := NewLineSegment((*c)[start].point, (*c)[i].point)
// Compare the distance of the intersection to the original segment
// with its distance to the intermediate segment.
if segment.DistanceToPoint(intersection.point) < existingSegment.DistanceToPoint((*c)[i].point) {
// Update the insertion position if the intersection is closer to the original segment.
insertPos = i
break
}
}
// Insert the intersection point into the contour at the calculated position.
*c = slices.Insert(*c, insertPos, intersection)
}
// String returns a string representation of the contour, listing all its points in order.
//
// This method iterates through all polyTreePoints in the contour and appends their coordinates
// to a human-readable string.
//
// Returns:
// - string: A formatted string showing the list of points in the contour.
func (c *contour[T]) String() string {
var builder strings.Builder
builder.WriteString("Contour Points: [")
first := true
for _, pt := range c.toPoints() {
if first {
builder.WriteString(fmt.Sprintf("(%v, %v)", pt.x, pt.y))
first = false
continue
}
builder.WriteString(fmt.Sprintf(", (%v, %v)", pt.x, pt.y))
}
builder.WriteString("]")
return builder.String()
}
// polyEdge represents an edge of a polygon, storing the geometric line segment
// and additional metadata for polygon operations.
//
// This type is used internally for operations such as determining point-in-polygon
// relationships and handling ray intersection tests. It provides both the edge's
// geometric representation and its relationship with a ray used in algorithms.
type polyEdge[T SignedNumber] struct {
// lineSegment represents the geometric edge of the polygon as a line segment.
// This field is used for geometric operations such as intersection checks and edge traversal.
lineSegment LineSegment[T]
// rel specifies the relationship of this edge with a ray during point-in-polygon tests.
// This field is primarily used for algorithms like ray-casting to determine whether
// a point is inside or outside the polygon.
rel detailedLineSegmentRelationship
}
// polyIntersectionType defines the type of intersection point in polygon operations,
// distinguishing between entry and exit (of area of interest) points during traversal.
//
// This type is primarily used in Boolean operations (e.g., union, intersection, subtraction)
// to identify transitions at intersection points between polygons.
type polyIntersectionType int
// polyTraversalDirection defines the direction in which a polygon's vertices are traversed.
// This can either be clockwise or counterclockwise and is used to specify how to iterate
// through a polygon's vertices or edges during boolean operations and other processing.
type polyTraversalDirection uint8
// polyTreeEqOption defines a functional option for configuring the behaviour of the Eq method
// on a PolyTree. These options are used to customize the comparison logic, such as whether to
// track visited nodes to prevent infinite recursion during comparisons of siblings and children.
//
// For example, `WithVisited` can be used to provide a map of visited nodes to avoid reprocessing
// already-checked polygons in recursive structures.
type polyTreeEqOption[T SignedNumber] func(*polyTreeEqConfig[T])
// polyTreeEqConfig is a configuration struct used to control the behaviour of the Eq method
// on a PolyTree. It provides additional context and state to support complex comparisons,
// such as tracking visited nodes to prevent infinite recursion when comparing nested or cyclic
// structures.
//
// Fields:
// - visited: A map used to track which PolyTree nodes have already been visited during the
// comparison. This prevents infinite loops when comparing siblings or children that may
// reference each other in complex hierarchies.
type polyTreeEqConfig[T SignedNumber] struct {
visited map[*PolyTree[T]]bool // Tracks visited nodes to avoid infinite recursion.
}
// polyTreePoint represents a point in a polygon, with additional metadata used
// for polygon operations such as Boolean operations and traversal.
//
// Fields:
// - point: The geometric coordinates of the point in 2D space.
// - pointType: Indicates the type of the point, which can be a normal vertex,
// an intersection point, or a midpoint between intersections.
// - entryExit: Specifies whether this point is an entry or exit point during
// polygon traversal in Boolean operations. This field is critical for determining
// traversal directions and relationships between polygons.
// - visited: Tracks whether this point has been visited during traversal algorithms,
// helping to avoid redundant processing.
// - intersectionPartner: A reference to the partner polygon involved in an intersection,
// if this point is an intersection point. This field is nil for normal vertices.
// - intersectionPartnerPointIndex: The index of the corresponding intersection point in
// the partner polygon's contour, if this point is an intersection. A value of -1
// indicates no partner exists.
//
// Usage:
//
// This struct is primarily used in [PolyTree]'s contour to represent the points
// and their metadata, enabling advanced polygon operations such as union, intersection,
// and subtraction.
type polyTreePoint[T SignedNumber] struct {
// The geometric coordinates of the point in 2D space.
point Point[T]
// The type of the point, such as a normal vertex, intersection point, or midpoint.
pointType polyPointType
// Indicates whether this point is an entry or exit point during traversal.
entryExit polyIntersectionType
// Tracks whether this point has been visited during traversal algorithms.
visited bool
// Reference to the partner polygon for intersection points.
intersectionPartner *PolyTree[T]
// Index of the corresponding intersection point in the partner polygon.
intersectionPartnerPointIndex int
}
// simpleConvexPolygon represents a convex polygon, which is a polygon where all interior angles
// are less than 180 degrees, and no line segment between two points on the boundary extends outside the polygon.
//
// This type is used internally to optimize geometric operations such as point-in-polygon checks,
// as convex polygons allow for faster algorithms compared to general polygons.
//
// As this is used internally, no checks are in place to enforce convexity.
// The ConvexHull function returns this type.
type simpleConvexPolygon[T SignedNumber] struct {
// Points contains the ordered vertices of the convex polygon. The points are arranged
// sequentially in either clockwise or counterclockwise order, forming the boundary of the convex hull.
Points []Point[T]
}
// NewPolyTree creates a new [PolyTree] object from a set of points defining the polygon's contour.
// The function also allows for optional configuration using [NewPolyTreeOption].
//
// Parameters:
// - points ([][Point][T]): A slice of [Point][T] representing the vertices of the polygon.
// - t ([PolygonType]): The type of polygon, either [PTSolid] or [PTHole].
// - opts: A variadic slice of [NewPolyTreeOption] functions, see below.
//
// [NewPolyTreeOption] functions:
// - [WithChildren]: is an option for the [NewPolyTree] function that assigns child polygons to the
// created [PolyTree]. This can also be done later with the [PolyTree.AddChild] method.
// - [WithSiblings]: is an option for the [NewPolyTree] function that assigns sibling polygons to the
// created [PolyTree]. This can also be done later with the [PolyTree.AddSibling] method.
//
// Returns:
// - A pointer to the newly created PolyTree.
// - An error if the input points are invalid (e.g., less than three points or zero area).
//
// Notes:
// - The function ensures that the polygon's points are oriented correctly based on its type.
// - Contour are doubled internally to avoid integer division/precision issues during midpoint calculations.
// - The polygon's convex hull is computed and stored for potential optimisations.
// - Child polygons must have the opposite [PolygonType] (e.g., holes for a solid polygon and solids for a hole polygon).
func NewPolyTree[T SignedNumber](points []Point[T], t PolygonType, opts ...NewPolyTreeOption[T]) (*PolyTree[T], error) {
// Validate the polygon
_, err := IsWellFormedPolygon(points)
if err != nil {
return nil, fmt.Errorf("invalid polygon: %w", err)
}
// Create a new, zero-initialised PolyTree.
p := new(PolyTree[T])
// Set the polygon type (e.g., solid or hole).
p.polygonType = t
// Ensure the points are oriented correctly based on the polygon type.
orderedPoints := make([]Point[T], len(points))
copy(orderedPoints, points)
switch p.polygonType {
case PTSolid:
// Solid polygons must be counterclockwise.
EnsureCounterClockwise(orderedPoints)
case PTHole:
// Hole polygons must be clockwise.
EnsureClockwise(orderedPoints)
}
// Assign and double the points for precision handling.
p.contour = make([]polyTreePoint[T], len(orderedPoints))
for i, point := range orderedPoints {
p.contour[i] = polyTreePoint[T]{
point: NewPoint(point.x*2, point.y*2),
}
}
// Reorder contour points and reset intersection metadata.
p.resetIntersectionMetadataAndReorder()
// Compute and store the convex hull of the polygon for optimisation.
hull := ConvexHull(points)
EnsureCounterClockwise(hull) // Convex hulls are always counterclockwise.
p.hull = newSimpleConvexPolygon(hull)
// Apply optional configurations.
for _, opt := range opts {
err := opt(p)
if err != nil {
return nil, err
}
}
// Return the constructed PolyTree.
return p, nil
}
// newSimpleConvexPolygon creates a new simpleConvexPolygon from a given slice of points.
// The input points are assumed to be ordered to form a convex polygon.
//
// This function is primarily used internally to construct a convex polygon representation
// for optimization purposes, such as in point-in-polygon checks.
//
// Parameters:
//
// - points []Point[T]: A slice of points representing the vertices of the convex polygon.
// The points are assumed to be ordered sequentially, either clockwise
// or counterclockwise, to define the boundary of a convex polygon.
//
// Returns:
//
// - *simpleConvexPolygon[T]: A pointer to a new simpleConvexPolygon containing the provided points.
//
// Notes:
// - This function assumes that the input points are already ordered and form a valid convex polygon.
// No validation is performed to verify the convexity of the polygon or the order of the points.
// - As this function is used internally, it expects the caller to ensure the validity of the input.
//
// Example:
//
// points := []Point[float64]{
// {X: 0, Y: 0},
// {X: 4, Y: 0},
// {X: 4, Y: 4},
// {X: 0, Y: 4},
// }
// scp := newSimpleConvexPolygon(points)
// // scp represents a convex polygon with the given points.
func newSimpleConvexPolygon[T SignedNumber](points []Point[T]) simpleConvexPolygon[T] {
// Assume `points` is already ordered to form a convex polygon
return simpleConvexPolygon[T]{Points: points}
}
// contains checks whether a given point is present in the contour.
// It iterates through the contour to find a matching point by comparing
// their x and y coordinates.
//
// Parameters:
// - point: The Point to check for existence in the contour.
//
// Returns:
// - true if the given point exists in the contour, otherwise false.
func (c *contour[T]) contains(point Point[T]) bool {
// Use slices.ContainsFunc to determine if the given point exists in the contour.
return slices.ContainsFunc(*c, func(p polyTreePoint[T]) bool {
// Compare the x and y coordinates of the current point in the contour with the given point.
// Return true if they match, indicating the point exists in the contour.
if p.point.x == point.x && p.point.y == point.y {
return true
}
// Otherwise, return false to continue searching.
return false
})
}
// eq compares two contours to determine if they are equivalent.
// Contours are considered equal if they contain the same points in the same relative order,
// accounting for possible rotations and ensuring consistent orientation (e.g., clockwise or counterclockwise).
//
// Parameters:
// - other: The contour to compare with the current contour.
//
// Returns:
// - true if the contours are equivalent, otherwise false.
func (c *contour[T]) eq(other contour[T]) bool {
// Check if both contours are empty; two empty contours are considered equal.
if len(*c) == 0 && len(other) == 0 {
return true
}
// If the lengths of the contours differ, they cannot be equal.
if len(*c) != len(other) {
return false
}
// Create copies of the contours to avoid modifying the originals during comparison.
copyC := c.toPoints()
copyOther := other.toPoints()
// Ensure both contours have the same orientation.
// Reverse the order of points if the signed area indicates inconsistent orientation.
if SignedArea2X(copyC) > 0 {
slices.Reverse(copyC)
}
if SignedArea2X(copyOther) > 0 {
slices.Reverse(copyOther)
}
// Attempt to match contours by rotating the starting point of the second contour.
for start := 0; start < len(copyOther); start++ {
matches := true
// Check if this rotation aligns all points in both contours.
for i := 0; i < len(copyC); i++ {
if copyC[i] != copyOther[(i+start)%len(copyOther)] {
// If a mismatch is found, mark as not matching and break out of the loop.
matches = false
break
}
}
// If all points match for this rotation, the contours are equivalent.
if matches {
return true
}
}
// No rotation of the second contour resulted in a match, so the contours are not equivalent.
return false
}
// ensureClockwise ensures that the contour points are ordered in a clockwise direction.
//
// This function calculates the signed area of the contour. If the area is positive, it indicates
// that the points are in a counterclockwise order. In such cases, the function reverses the order
// of the points to make the contour clockwise. If the area is already negative (indicating clockwise
// order), no action is taken.
//
// This is particularly useful for consistency when dealing with hole polygons or when ensuring
// correct orientation for geometric operations like Boolean polygon operations.
//
// Notes:
// - A positive signed area indicates counterclockwise orientation.
// - A negative signed area indicates clockwise orientation.
func (c *contour[T]) ensureClockwise() {
area := SignedArea2X(c.toPoints())
if area < 0 {
return // Already clockwise
}
slices.Reverse(*c)
}
// ensureCounterClockwise ensures that the contour points are ordered in a counterclockwise direction.
//
// This function calculates the signed area of the contour. If the area is negative, it indicates
// that the points are in a clockwise order. In such cases, the function reverses the order of
// the points to make the contour counterclockwise. If the area is already positive (indicating
// counterclockwise order), no action is taken.
//
// This is particularly useful for consistency when dealing with solid polygons or for geometric
// operations that require specific point orientations.
//
// Notes:
// - A positive signed area indicates counterclockwise orientation.
// - A negative signed area indicates clockwise orientation.
func (c *contour[T]) ensureCounterClockwise() {
area := SignedArea2X(c.toPoints())
if area > 0 {
return // Already counterclockwise
}
slices.Reverse(*c)
}
// findLowestLeftmost identifies the lowest, leftmost point in a given contour.
//
// This method iterates through all points in the contour to find the point
// with the smallest y coordinate. If multiple points share the same y
// coordinate, it selects the one with the smallest x coordinate.
//
// Parameters:
// - c: A pointer to the contour, which is a slice of polyTreePoint.
//
// Returns:
// - The Point with the lowest y coordinate and, in case of ties, the
// smallest x coordinate.
func (c *contour[T]) findLowestLeftmost() Point[T] {
// Initialize the minimum point as the first point in the contour.
minimum := (*c)[0].point
// Iterate through each point in the contour to find the lowest, leftmost point.
for _, pt := range *c {
// Update the minimum point if:
// - The current point's y coordinate is smaller.
// - Or, if the y coordinates are equal, the current point's x coordinate is smaller.
if pt.point.y < minimum.y || (pt.point.y == minimum.y && pt.point.x < minimum.x) {
minimum = pt.point // Update the minimum to the current point.
}
}
// Return the identified lowest, leftmost point.
return minimum
}
//// insertIntersectionPoint inserts an intersection point into a contour between two specified indices.
//// The insertion position is determined based on the proximity of the intersection point to the
//// corresponding line segment, ensuring correct ordering for polygon operations.
////
//// Parameters:
//// - start: The index of the starting point of the line segment in the contour.
//// - end: The index of the ending point of the line segment in the contour.
//// - intersection: The intersection point to insert, represented as a polyTreePoint.
////
//// Notes:
//// - The function assumes that start and end indices are valid and start < end.
//// - The insertion logic ensures that the intersection is placed in the correct position relative to
//// other intermediate points, preserving the geometric consistency of the contour.
//// - Simply inserting at the start index is not sufficient because the contour may already contain
//// intermediate points between start and end. Proper ordering is necessary to maintain the
//// validity of polygon operations such as traversals and Boolean operations.
//func (c *contour[T]) insertIntersectionPoint(start, end int, intersection polyTreePoint[T]) {
// // Define the line segment between the start and end points.
// segment := NewLineSegment((*c)[start].point, (*c)[end].point)
//
// // Initialize the insertion position to the end index.
// insertPos := end
//
// // Iterate through the intermediate points in the contour between start and end.
// // Find the correct position to insert the intersection point.
// for i := start + 1; i < end; i++ {
// // Define the segment from the start point to the current point.
// existingSegment := NewLineSegment((*c)[start].point, (*c)[i].point)
//
// // Compare the distance of the intersection to the original segment
// // with its distance to the intermediate segment.
// if segment.DistanceToPoint(intersection.point) < existingSegment.DistanceToPoint((*c)[i].point) {
// // Update the insertion position if the intersection is closer to the original segment.
// insertPos = i
// break
// }
// }
//
// // Insert the intersection point into the contour at the calculated position.
// *c = slices.Insert(*c, insertPos, intersection)
//}
// isContourInside checks if all points of one contour (c2) are inside another contour (c).
//
// Parameters:
// - c2: The contour to check against the current contour (c).
//
// Returns:
// - true: If all points of c2 are inside c.
// - false: If any point of c2 is outside c.
//
// Notes:
// - This function iterates over each point in c2 and checks whether it is inside c using isPointInside.
// - If any point of c2 is found to be outside c, the function returns false immediately.
// - The function assumes that the orientation of c and c2 is correct (i.e., counter-clockwise for solids and clockwise for holes).
func (c *contour[T]) isContourInside(other contour[T]) bool {
// Iterate over each point in "other".
for _, p := range other {
// Check if the point is not inside c.
if !c.isPointInside(p.point) {
// If any point is outside, return false immediately.
return false
}
}
// If all points are inside, return true.
return true
}
// isPointInside determines if a given point is inside the contour.
//
// Parameters:
// - point: The point to check.
//
// Returns:
// - true: If the point is inside the contour.
// - false: If the point is outside the contour.
//
// Notes:
// - This function uses the ray-casting algorithm to determine the point's relationship to the contour.
// - A horizontal ray is cast from the point to the right, and the number of times it crosses contour edges is counted.
// - If the number of crossings is odd, the point is inside; if even, the point is outside.
// - Contour lying directly on the contour edges are considered inside.
func (c *contour[T]) isPointInside(point Point[T]) bool {
/////
// Abandon all hope, ye who enter here
/////
crosses := 0
// Calculate the farthest x-coordinate to cast a ray.
maxX := point.x
for _, p := range *c {
maxX = max(maxX, p.point.x)
}
maxX++ // Ensure the ray extends beyond the contour's bounds.
ray := NewLineSegment(
point,
NewPoint(maxX, point.y)) // Cast the ray.
// Convert the contour to edges (line segments).
edges := c.toEdges()
// Determine all relationships of the edges to the ray
for i := range edges {
// Determine the relationship of the edge to the ray.
edges[i].rel = ray.detailedRelationshipToLineSegment(edges[i].lineSegment)
// if the point is on the edge, then we bail out early
if edges[i].rel == lsrCollinearAonCD || edges[i].rel == lsrAonCD {
//fmt.Println(edges[i].lineSegment.String(), "on edge")
return true
}
}
//fmt.Println("ray:", ray.String())
// Analyze the relationships and count ray crossings.
for i := range edges {
// miss
if edges[i].rel == lsrMiss {
continue
}
// normal intersection
if edges[i].rel == lsrIntersects {