-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcircle.go
555 lines (503 loc) · 22.2 KB
/
circle.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
package geom2d
import (
"fmt"
"math"
)
// Circle represents a circle in 2D space with a center point and a radius.
//
// The Circle type provides methods for calculating its circumference and area,
// determining if a point lies within the circle, checking if a line segment
// intersects the circle, and checking the relationship between the circle and other geometric shapes.
type Circle[T SignedNumber] struct {
center Point[T] // The center point of the circle
radius T // The radius of the circle
}
// NewCircle creates a new [Circle] with the specified center point and radius.
//
// Parameters:
// - center ([Point][T]): The center [Point] of the [Circle].
// - radius (T): The radius of the circle, of generic type T, which must satisfy the [SignedNumber] constraint.
//
// Returns:
// - Circle[T]: A new Circle with the specified center and radius.
func NewCircle[T SignedNumber](center Point[T], radius T) Circle[T] {
return Circle[T]{
center: center,
radius: radius,
}
}
// Area calculates the area of the circle.
//
// Returns:
// - float64: The area of the circle, computed as π * radius^2.
func (c Circle[T]) Area() float64 {
return math.Pi * float64(c.radius) * float64(c.radius)
}
// AsFloat32 converts the Circle's center coordinates and radius to the float32 type, returning a new Circle[float32].
// This method is useful for cases where higher precision or floating-point arithmetic is required.
//
// Returns:
// - Circle[float32]: A new Circle with the center coordinates and radius converted to float64.
func (c Circle[T]) AsFloat32() Circle[float32] {
return Circle[float32]{
center: c.center.AsFloat32(),
radius: float32(c.radius),
}
}
// AsFloat64 converts the Circle's center coordinates and radius to the float64 type, returning a new Circle[float64].
// This method is useful for cases where higher precision or floating-point arithmetic is required.
//
// Returns:
// - Circle[float64]: A new Circle with the center coordinates and radius converted to float64.
func (c Circle[T]) AsFloat64() Circle[float64] {
return Circle[float64]{
center: c.center.AsFloat64(),
radius: float64(c.radius),
}
}
// AsInt converts the Circle's center coordinates and radius to the int type by truncating any decimal values.
// This method is useful when integer values are needed, such as for pixel-based or grid-based calculations.
//
// Returns:
// - Circle[int]: A new Circle with center coordinates and radius converted to int by truncating any decimal portion.
func (c Circle[T]) AsInt() Circle[int] {
return Circle[int]{
center: c.center.AsInt(),
radius: int(c.radius),
}
}
// AsIntRounded converts the Circle's center coordinates and radius to the int type by rounding to the nearest integer.
// This method is useful when integer values are needed and rounding provides a more accurate representation
// compared to truncation.
//
// Returns:
// - Circle[int]: A new Circle with center coordinates and radius converted to int by rounding to the nearest integer.
func (c Circle[T]) AsIntRounded() Circle[int] {
return Circle[int]{
center: c.center.AsIntRounded(),
radius: int(math.Round(float64(c.radius))),
}
}
// BoundingBox calculates the axis-aligned bounding box (AABB) of the circle.
//
// The bounding box is the smallest rectangle, aligned with the coordinate axes, that completely encloses the circle.
// This is useful for collision detection, spatial partitioning, and other geometric operations.
//
// Returns:
// - [Rectangle][T]: The axis-aligned bounding box that encloses the circle.
//
// Notes:
// - The bounding box is a rectangle defined by the four corner points derived from the circle's center and radius.
func (c Circle[T]) BoundingBox() Rectangle[T] {
return NewRectangle[T]([]Point[T]{
NewPoint(c.center.x-c.radius, c.center.y-c.radius),
NewPoint(c.center.x+c.radius, c.center.y-c.radius),
NewPoint(c.center.x+c.radius, c.center.y+c.radius),
NewPoint(c.center.x-c.radius, c.center.y+c.radius),
})
}
// Center returns the center [Point] of the Circle.
//
// Returns:
// - Point[T]: The center [Point] of the Circle.
func (c Circle[T]) Center() Point[T] {
return c.center
}
// Circumference calculates the circumference of the circle.
//
// Returns:
// - float64: The circumference of the circle, computed as 2 * π * radius.
func (c Circle[T]) Circumference() float64 {
return 2 * math.Pi * float64(c.radius)
}
// Eq determines whether the calling Circle (c) is equal to another Circle (other), either exactly (default)
// or approximately using an epsilon threshold.
//
// Parameters
// - other: The Circle to compare with the calling Circle.
// - opts: A variadic slice of [Option] functions to customize the equality check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing the center coordinates
// and radii of the circles, allowing for robust handling of floating-point precision errors.
//
// Behavior
// - The function checks whether the center points of the two circles are equal (using the [Point.Eq] method
// of the [Point] type) and whether their radii are equal.
// - If [WithEpsilon] is provided, the comparison allows for small differences in the radius values
// and center coordinates within the epsilon threshold.
//
// Returns
// - bool: true if the center coordinates and radius of the two circles are equal (or approximately equal
// within epsilon); otherwise, false.
//
// Notes:
// - Approximate equality is particularly useful when comparing circles with floating-point
// coordinates or radii, where small precision errors might otherwise cause inequality.
func (c Circle[T]) Eq(other Circle[T], opts ...Option) bool {
// Apply options with defaults
options := applyOptions(geomOptions{epsilon: 0}, opts...)
// Check equality for the center points
centersEqual := c.center.Eq(other.center, opts...)
// Check equality for the radii with epsilon adjustment
radiiEqual := c.radius == other.radius
if options.epsilon > 0 {
radiiEqual = math.Abs(float64(c.radius)-float64(other.radius)) < options.epsilon
}
return centersEqual && radiiEqual
}
// Radius returns the radius of the Circle.
//
// Returns:
// - T: The radius of the Circle.
func (c Circle[T]) Radius() T {
return c.radius
}
// RelationshipToCircle determines the spatial relationship between two circles.
//
// This function evaluates the relationship between the current circle and another
// circle by comparing their center points and radii. The possible relationships include:
// - [RelationshipEqual]: The circles are identical.
// - [RelationshipContainedBy]: The current circle is completely contained within the other circle.
// - [RelationshipContains]: The current circle completely contains the other circle.
// - [RelationshipIntersection]: The circles overlap, including tangency.
// - [RelationshipDisjoint]: The circles do not overlap.
//
// Parameters:
// - other (Circle[T]): The circle to compare against the current circle.
// - opts: A variadic slice of [Option] functions to customize the equality check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing the center coordinates
// and radii of the circles, allowing for robust handling of floating-point precision errors.
//
// Returns:
// - [Relationship]: A constant representing the relationship between the circles.
//
// Behavior:
// - The function first checks for equality by comparing center points and radii.
// - It then checks for containment by comparing the distance between centers and radii.
// - Intersection is detected if the distance between centers is less than or equal to the sum of the radii.
// - If no other relationship is found, the circles are considered disjoint.
func (c Circle[T]) RelationshipToCircle(other Circle[T], opts ...Option) Relationship {
distanceBetweenCenters := c.center.DistanceToPoint(other.center, opts...)
cFloat := c.AsFloat64()
otherFloat := other.AsFloat64()
// check for equality
if c.Eq(other) {
return RelationshipEqual
}
// check for c contained by other
if distanceBetweenCenters+cFloat.radius < otherFloat.radius {
return RelationshipContainedBy
}
// check for c contains other
if distanceBetweenCenters+otherFloat.radius < cFloat.radius {
return RelationshipContains
}
// check for intersection
if distanceBetweenCenters <= cFloat.radius+otherFloat.radius {
return RelationshipIntersection
}
return RelationshipDisjoint
}
// RelationshipToLineSegment determines the spatial relationship between the current Circle and a
// given [LineSegment].
//
// This function evaluates the relationship between the circle and the line segment,
// which can be one of the following:
// - [RelationshipDisjoint]: The line segment lies entirely outside the circle.
// - [RelationshipIntersection]: The line segment intersects the circle's boundary.
// - [RelationshipContains]: The line segment is fully contained within the circle.
//
// This method internally calls [LineSegment.RelationshipToCircle], flipping the containment
// direction to align with the perspective of the circle.
//
// Parameters:
// - l ([LineSegment][T]): The line segment to compare against the circle.
// - opts: A variadic slice of [Option] functions to customize the equality check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing the center coordinates
// and radii of the circles, allowing for robust handling of floating-point precision errors.
//
// Returns:
// - [Relationship]: A constant representing the relationship between the circle and the line segment.
func (c Circle[T]) RelationshipToLineSegment(l LineSegment[T], opts ...Option) Relationship {
return l.RelationshipToCircle(c, opts...).flipContainment()
}
// RelationshipToPoint determines the spatial relationship between the current Circle and a given [Point].
//
// This function evaluates the relationship between the circle and the point,
// which can be one of the following:
// - [RelationshipDisjoint]: The point lies outside the circle.
// - [RelationshipIntersection]: The point lies exactly on the circle's boundary.
// - [RelationshipContains]: The point lies inside the circle.
//
// This method internally calls [Point.RelationshipToCircle], flipping the containment
// direction to align with the perspective of the circle.
//
// Parameters:
// - p ([Point][T]): The point to compare against the circle.
// - opts: A variadic slice of [Option] functions to customize the equality check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing the center coordinates
// and radii of the circles, allowing for robust handling of floating-point precision errors.
//
// Returns:
// - [Relationship]: A constant representing the relationship between the circle and the point.
func (c Circle[T]) RelationshipToPoint(p Point[T], opts ...Option) Relationship {
return p.RelationshipToCircle(c, opts...).flipContainment()
}
// RelationshipToPolyTree determines the spatial relationships between the Circle and the polygons in the given [PolyTree].
//
// This method evaluates whether the circle intersects, contains, or is contained by each polygon in the [PolyTree].
// It uses a doubled representation of the Circle to align with the doubled points in the [PolyTree] for robust computations.
//
// Parameters:
// - pt (*[PolyTree][T]): The [PolyTree] whose polygons will be compared to the Circle.
// - opts: A variadic slice of [Option] functions to customize the equality check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for comparing the center coordinates
// and radii of the circles, allowing for robust handling of floating-point precision errors.
//
// Behavior:
// - For each polygon in the [PolyTree], the function iterates through its edges and checks the relationship to the Circle.
// - Intersection is checked first; if any edge of the polygon intersects the circle, the relationship is marked as [RelationshipIntersection].
// - If all edges of the polygon lie within the circle's radius, the relationship is marked as [RelationshipContains].
// - If the circle's center lies within the polygon and its minimum distance to any edge is greater than its radius, the relationship is marked as [RelationshipContainedBy].
// - If none of the above conditions are satisfied, the relationship is marked as [RelationshipDisjoint].
//
// Returns:
// - map[*[PolyTree][T]][Relationship]: A map where each key is a pointer to a polygon in the [PolyTree], and the value is the relationship between the Circle and that polygon.
func (c Circle[T]) RelationshipToPolyTree(pt *PolyTree[T], opts ...Option) map[*PolyTree[T]]Relationship {
output := make(map[*PolyTree[T]]Relationship, pt.Len())
cDoubled := NewCircle(NewPoint(c.center.x*2, c.center.y*2), c.radius*2)
cFloatDoubled := cDoubled.AsFloat64()
RelationshipToPolyTreeIterPolys:
for poly := range pt.Nodes {
minDistCircleCenterToEdge := math.MaxFloat64
allEdgesWithinCircle := true
for edge := range poly.contour.iterEdges {
rel := cDoubled.RelationshipToLineSegment(edge, opts...)
// Check for intersection
if rel == RelationshipIntersection {
output[poly] = RelationshipIntersection
continue RelationshipToPolyTreeIterPolys
}
// Check if all edges are within the circle's radius
distanceToEdge := cDoubled.center.DistanceToLineSegment(edge, opts...)
minDistCircleCenterToEdge = min(minDistCircleCenterToEdge, distanceToEdge)
if distanceToEdge > cFloatDoubled.radius {
allEdgesWithinCircle = false
}
}
// Check for containment: circle fully contains the polygon
if allEdgesWithinCircle {
output[poly] = RelationshipContains
continue RelationshipToPolyTreeIterPolys
}
// Check for containment: polygon fully contains the circle
if poly.contour.isPointInside(cDoubled.center) && minDistCircleCenterToEdge > cFloatDoubled.radius {
output[poly] = RelationshipContainedBy
continue RelationshipToPolyTreeIterPolys
}
// Default: no relationship found
output[poly] = RelationshipDisjoint
}
return output
}
// RelationshipToRectangle determines the spatial relationship between the circle and the rectangle.
//
// This function evaluates whether the circle is:
// - Disjoint from the rectangle (no overlap or touching),
// - Intersects with the rectangle (crosses its boundary),
// - Fully contains the rectangle (encloses it entirely),
// - Fully contained by the rectangle (is completely inside the rectangle).
//
// Parameters:
// - r ([Rectangle][T]): The rectangle to compare with the circle.
// - opts: A variadic slice of [Option] functions to customize the behavior of the relationship check.
// [WithEpsilon](epsilon float64): Specifies a tolerance for floating-point precision.
//
// Behavior:
// - The function checks each edge of the rectangle for potential intersections with the circle.
// - If all edges of the rectangle are fully contained within the circle, it returns [RelationshipContains].
// - If the rectangle fully contains the circle (the circle’s center is inside the rectangle, and the circle
// does not extend beyond any edge), it returns [RelationshipContainedBy].
// - If none of these conditions are met, it determines whether the circle and rectangle are disjoint.
//
// Returns:
//
// [Relationship]: One of the following constants:
// - [RelationshipDisjoint]: The circle and rectangle are entirely separate.
// - [RelationshipIntersection]: The circle intersects with one or more edges of the rectangle.
// - [RelationshipContains]: The circle completely encloses the rectangle.
// - [RelationshipContainedBy]: The circle is fully contained within the rectangle.
func (c Circle[T]) RelationshipToRectangle(r Rectangle[T], opts ...Option) Relationship {
cContainsR := true
cFloat := c.AsFloat64()
minDistCircleCenterToEdge := math.MaxFloat64
for _, edge := range r.Edges() {
rel := edge.RelationshipToCircle(c, opts...)
// check for intersection
if rel == RelationshipIntersection {
return RelationshipIntersection
}
// check for containment
if rel != RelationshipContainedBy {
cContainsR = false
}
edgeFloat := edge.AsFloat64()
minDistCircleCenterToEdge = min(minDistCircleCenterToEdge, cFloat.center.DistanceToLineSegment(edgeFloat, opts...))
}
// check c contain r
if cContainsR {
return RelationshipContains
}
// check r contains c
if r.ContainsPoint(c.center) && minDistCircleCenterToEdge > cFloat.radius {
return RelationshipContainedBy
}
return RelationshipDisjoint
}
// Rotate rotates the Circle's center around a specified pivot [Point] by a given angle in radians
// counterclockwise, while keeping the radius unchanged. 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 circle's center.
// - radians: The rotation angle in radians (counterclockwise).
// - opts: A variadic slice of [Option] functions to customize the behavior of the rotation.
// [WithEpsilon](epsilon float64): Specifies a tolerance for snapping the resulting center coordinates
// to cleaner values, improving robustness in floating-point calculations.
//
// Returns:
// - [Circle][float64]: A new [Circle] with the center rotated around the pivot [Point] by the specified angle,
// and with the radius unchanged.
//
// Behavior:
// - The function rotates the circle's center point around the given pivot by the specified angle using
// the [Point.Rotate] method.
// - The rotation is performed in a counterclockwise direction relative to the pivot point.
// - The radius remains unchanged in the resulting [Circle].
// - If [WithEpsilon] is provided, epsilon adjustments are applied to the rotated coordinates to handle
// floating-point precision errors.
//
// Notes:
// - Epsilon adjustment is particularly useful when the rotation involves floating-point calculations
// that could result in minor inaccuracies.
// - The returned [Circle] always has a center with float64 coordinates, ensuring precision regardless
// of the coordinate type of the original [Circle].
func (c Circle[T]) Rotate(pivot Point[T], radians float64, opts ...Option) Circle[float64] {
return NewCircle[float64](
c.center.Rotate(pivot, radians, opts...),
float64(c.radius),
)
}
// Scale scales the radius of the circle by a scalar factor.
//
// Parameters:
// - factor (T): The factor by which to scale the radius.
//
// Returns:
// - Circle[T]: A new circle with the radius scaled by the specified factor.
func (c Circle[T]) Scale(factor T) Circle[T] {
return Circle[T]{center: c.center, radius: c.radius * factor}
}
// String returns a string representation of the Circle, including its center coordinates and radius.
// This is useful for debugging and logging.
//
// Returns:
// - string: A string representation of the Circle in the format "Circle[center=(x, y), radius=r]".
func (c Circle[T]) String() string {
return fmt.Sprintf("Circle[center=(%v, %v), radius=%v]", c.center.x, c.center.y, c.radius)
}
// Translate moves the circle by a specified vector (given as a [Point]).
//
// This method shifts the circle's center by the given vector v, effectively
// translating the circle's position in the 2D plane. The radius of the circle
// remains unchanged.
//
// Parameters:
// - v ([Point][T]): The vector by which to translate the circle's center.
//
// Returns:
// - Circle[T]: A new Circle translated by the specified vector.
func (c Circle[T]) Translate(v Point[T]) Circle[T] {
return Circle[T]{center: c.center.Translate(v), radius: c.radius}
}
// Bresenham generates all points on the perimeter of a circle using Bresenham's circle-drawing algorithm.
//
// This method is typically used for rasterized circle rendering.
//
// The function is designed to be used with a for-loop, and thus takes a callback yield that processes each point.
// If the callback returns false at any point (if the calling for-loop is terminated, for example), the function
// halts further generation.
//
// This algorithm utilizes integer arithmetic to efficiently calculate the points on the circle,
// making it suitable for rendering or other grid-based operations.
//
// Parameters:
// - yield (func([Point][int]) bool): A function that processes each generated point.
// Returning false will stop further point generation.
//
// Note: This implementation requires the circle is defined with integer-type coordinates.
func (c Circle[int]) Bresenham(yield func(Point[int]) bool) {
var xc, yc, r, x, y, p int
xc = c.center.x
yc = c.center.y
r = c.radius
// Starting at the top of the circle
x = 0
y = r
p = 1 - r // Initial decision parameter
// Yield the initial points for all octants
for _, point := range reflectAcrossCircleOctants(xc, yc, x, y) {
if !yield(point) {
return
}
}
// Loop until x meets y
for x < y {
x++
if p < 0 {
// Midpoint is inside the circle
p += 2*x + 1
} else {
// Midpoint is outside or on the circle
y--
p += 2*(x-y) + 1
}
// Yield the points for the current x, y
for _, point := range reflectAcrossCircleOctants(xc, yc, x, y) {
if !yield(point) {
return
}
}
}
}
// reflectAcrossCircleOctants generates a slice of points that represent the reflection
// of a given point (x, y) across all eight octants of a circle centered at (xc, yc).
//
// The function is typically used in circle-drawing algorithms, such as Bresenham's Circle Algorithm,
// to exploit the symmetry of circles for efficient computation.
//
// Parameters:
// - xc, yc: The coordinates of the circle's center.
// - x, y: The coordinates of the point to reflect across the octants.
//
// Returns:
// - A slice of Point[T] containing the reflected points in the following order:
// 1. Octant 1: (xc + x, yc + y)
// 2. Octant 2: (xc - x, yc + y)
// 3. Octant 8: (xc + x, yc - y)
// 4. Octant 7: (xc - x, yc - y)
// 5. Octant 3: (xc + y, yc + x)
// 6. Octant 4: (xc - y, yc + x)
// 7. Octant 6: (xc + y, yc - x)
// 8. Octant 5: (xc - y, yc - x)
func reflectAcrossCircleOctants[T SignedNumber](xc, yc, x, y T) []Point[T] {
return []Point[T]{
NewPoint[T](xc+x, yc+y), // Octant 1
NewPoint[T](xc-x, yc+y), // Octant 2
NewPoint[T](xc+x, yc-y), // Octant 8
NewPoint[T](xc-x, yc-y), // Octant 7
NewPoint[T](xc+y, yc+x), // Octant 3
NewPoint[T](xc-y, yc+x), // Octant 4
NewPoint[T](xc+y, yc-x), // Octant 6
NewPoint[T](xc-y, yc-x), // Octant 5
}
}