-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgeom2d.go
349 lines (328 loc) · 14.6 KB
/
geom2d.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
// Package geom2d provides a comprehensive set of tools for computational geometry in two dimensions.
//
// The geom2d package is built around core types like [Point], [LineSegment], [Circle], [Rectangle], and [PolyTree],
// supporting a wide range of operations including transformations, boolean geometry, and spatial relationships.
//
// Designed for both performance and clarity, geom2d leverages Go generics to handle various numeric types
// and provides intuitive APIs for working with 2D geometric data.
//
// # Coordinate System
//
// This library assumes a standard Cartesian coordinate system where the x-axis increases to the right and the y-axis
// increases upward. This system is commonly referred to as a right-handed Cartesian coordinate system.
// All geometric operations and relationships (e.g., clockwise or counterclockwise points) are based on this convention.
//
// # Core Geometric Types
//
// The library includes support for the following 2D geometric types:
//
// - [Point]: Represents a single coordinate in 2D space.
// - [LineSegment]: Represents a straight line segment defined by two endpoints.
// - [Rectangle]: Represents an axis-aligned rectangle, defined by its corners.
// - [Circle]: Represents a circle defined by a center point and radius.
// - [PolyTree]: Represents a hierarchical structure of polygons, supporting sibling polygons,
// nested holes and nested islands.
//
// # Support for Generics
//
// geom2d leverages Go’s generics, allowing you to use the library with different numeric types
// (int, float32, float64, etc.). This flexibility ensures the library can adapt to various applications,
// from integer-based grids to floating-point precision computations.
//
// # Precision Control with Epsilon
//
// geom2d incorporates an epsilon parameter in many of its relationship methods to handle floating-point
// precision issues. This allows you to control the tolerance for comparisons, making the library robust
// for real-world applications where precision errors can occur.
//
// # Relationships Between Geometric Types
//
// This library provides methods to compute relationships between geometric types using a standardized set of relationships:
// [RelationshipDisjoint], [RelationshipIntersection], [RelationshipContainedBy], [RelationshipContains], and [RelationshipEqual].
//
// # Acknowledgments
//
// geom2d builds upon the work of others and is grateful for the foundations they have laid. Specifically:
//
// - Martínez et al.: Their paper on Boolean operations on polygons has been instrumental in the implementation of
// the Martínez algorithm in this library. See [A simple algorithm for Boolean operations on polygons].
// - Tom Wright: The inspiration for starting this library came from Tom Wright’s repository
// [Provably Correct Polygon Algorithms] and his accompanying paper. While geom2d follows its own approach,
// certain ideas have been influenced by his work.
// - Jack Bresenham: The Bresenham's Line Algorithm and Bresenham's Circle Algorithm implemented in this library are
// inspired by Jack Bresenham's pioneering work. These algorithms are efficient methods for rasterizing lines and
// circles in computer graphics. For more details, see Bresenham's original paper
// ["Algorithm for computer control of a digital plotter." IBM Systems Journal, 1965.]
// - This project is a collaborative effort, with assistance from [OpenAI's Assistant] for brainstorming, debugging,
// and refining implementations.
//
// [A simple algorithm for Boolean operations on polygons]: https://web.archive.org/web/20230514184409/https://www.sciencedirect.com/science/article/abs/pii/S0925772199000124
// [Provably Correct Polygon Algorithms]: https://github.com/TooOldCoder/Provably-Correct-Polygon-Algorithms
// ["Algorithm for computer control of a digital plotter." IBM Systems Journal, 1965.]: https://dl.acm.org/doi/10.1147/sj.41.025
// [OpenAI's Assistant]: https://openai.com/
package geom2d
import (
"fmt"
"math"
)
func init() {
logDebugf("debug logging enabled")
}
// Option is a functional option type used to configure optional parameters
// in geometric operations. Functions that accept an Option parameter allow
// users to customize behavior without modifying the primary function signature.
//
// Option functions take a pointer to an geomOptions struct and modify its fields
// to apply specific configurations.
type Option func(*geomOptions)
// WithEpsilon returns an [Option] that sets the Epsilon value for functions that support it.
// Epsilon is a small positive value used to adjust for floating-point precision errors,
// ensuring numerical stability in geometric calculations.
//
// Parameters:
// - epsilon: A small positive value specifying the tolerance range. Values within
// [-epsilon, epsilon] are treated as zero.
//
// Behavior:
// - When this option is applied, functions will use the specified Epsilon value
// to handle near-zero results caused by floating-point arithmetic.
// - If a negative epsilon is provided, it will default to 0 (no adjustment).
// - If not set (default), Epsilon remains 0, and no adjustment is applied.
//
// Returns:
// - An [Option] function that modifies the Epsilon field in the geomOptions struct.
func WithEpsilon(epsilon float64) Option {
return func(opts *geomOptions) {
if epsilon < 0 {
epsilon = 0 // Default to no adjustment
}
opts.epsilon = epsilon
}
}
// Relationship defines the spatial or geometric relationship between two shapes or entities
// in 2D space. This type is used across various functions to represent how one geometric
// entity relates to another.
//
// The possible relationships include:
// - Disjoint: The entities do not intersect, overlap, or touch in any way.
// - Intersection: The entities share some boundary or overlap.
// - Contained: One entity is fully within the boundary of another.
// - ContainedBy: One entity is fully enclosed by another.
// - Equal: The entities are identical in shape, size, and position.
//
// Each relationship type is represented as a constant value of the Relationship type.
// Functions that evaluate relationships between geometric entities typically return one
// of these constants to describe the spatial relationship between them.
//
// See the individual constant definitions for more details.
type Relationship uint8
// Valid values for Relationship:
const (
// RelationshipDisjoint indicates that the two entities are completely separate,
// with no overlap, touching, or intersection.
RelationshipDisjoint Relationship = iota
// RelationshipIntersection indicates that the two entities overlap or share
// a boundary. This includes cases where the entities partially intersect
// or where they touch at one or more points.
RelationshipIntersection
// RelationshipContainedBy indicates that the first entity is fully enclosed
// within the second entity. The boundary of the first entity does not extend
// outside the boundary of the second entity.
RelationshipContainedBy
// RelationshipContains indicates that the first entity fully encloses the
// second entity. The boundary of the second entity does not extend outside
// the boundary of the first entity.
RelationshipContains
// RelationshipEqual indicates that the two entities are identical in shape,
// size, and position. This includes cases where their boundaries coincide exactly.
RelationshipEqual
)
// flipContainment reverses containment relationships for a [Relationship].
//
// This method is used to swap the roles of containment when interpreting
// relationships. Specifically:
// - If the [Relationship] is RelationshipContainedBy, it is flipped to RelationshipContains.
// - If the [Relationship] is RelationshipContains, it is flipped to RelationshipContainedBy.
// - All other [Relationship] values are returned unchanged.
//
// Returns:
// - [Relationship]: The flipped or unchanged [Relationship].
//
// Example:
//
// rel := RelationshipContainedBy
// flipped := rel.flipContainment()
// fmt.Println(flipped) // Output: RelationshipContains
func (r Relationship) flipContainment() Relationship {
switch r {
case RelationshipContainedBy:
return RelationshipContains
case RelationshipContains:
return RelationshipContainedBy
default:
return r
}
}
// String converts a [Relationship] value to its string representation.
//
// This method provides a human-readable string corresponding to the [Relationship]
// constant, such as RelationshipDisjoint or RelationshipContainedBy. It is useful
// for debugging and logging purposes.
//
// Supported [Relationship] values:
// - [RelationshipDisjoint]: The objects are disjoint and do not touch or intersect.
// - [RelationshipIntersection]: The objects intersect or overlap at some point.
// - [RelationshipContainedBy]: The object is fully contained within another object.
// - [RelationshipContains]: The object fully contains another object.
// - [RelationshipEqual]: The objects are identical in size, shape, and position.
//
// Returns:
// - string: The string representation of the [Relationship].
//
// Panics:
// - If the [Relationship] value is not supported, this method panics with an error message.
func (r Relationship) String() string {
switch r {
case RelationshipDisjoint:
return "RelationshipDisjoint"
case RelationshipIntersection:
return "RelationshipIntersection"
case RelationshipContainedBy:
return "RelationshipContainedBy"
case RelationshipContains:
return "RelationshipContains"
case RelationshipEqual:
return "RelationshipEqual"
default:
panic(fmt.Errorf("unsupported Relationship: %d", r))
}
}
// SignedNumber is a generic interface representing signed numeric types supported by this package.
// This interface allows functions and structs to operate generically on various numeric types,
// including integer and floating-point types, while restricting to signed values only.
//
// Supported types:
// - int
// - int32
// - int64
// - float32
// - float64
//
// By using SignedNumber, functions can handle multiple numeric types without needing to be rewritten
// for each specific type, enabling flexible and type-safe operations across different numeric data.
type SignedNumber interface {
int | int32 | int64 | float32 | float64
}
// ReflectionAxis specifies the axis or line across which a point or line segment should be reflected.
//
// This type defines the possible axes for reflection, including the standard x-axis and y-axis,
// as well as an arbitrary line defined by a custom line segment.
type ReflectionAxis int
const (
// ReflectAcrossXAxis reflects a point or line segment across the x-axis, flipping the y-coordinate.
ReflectAcrossXAxis ReflectionAxis = iota
// ReflectAcrossYAxis reflects a point or line segment across the y-axis, flipping the x-coordinate.
ReflectAcrossYAxis
// ReflectAcrossCustomLine reflects a point or line segment across an arbitrary line defined by a LineSegment.
// This line segment can be specified as an additional argument to the Reflect method.
ReflectAcrossCustomLine
)
// geomOptions defines a set of configurable parameters for geometric operations.
// These options allow users to customize the behavior of functions in the library,
// such as applying numerical stability adjustments or other optional features.
type geomOptions struct {
// epsilon is a small positive value used to adjust for floating-point precision errors.
// When set, values within the range [-Epsilon, Epsilon] are treated as zero in
// calculations to improve numerical stability. A value of 0 disables this adjustment.
//
// For example:
// - epsilon > 0: Small deviations caused by floating-point arithmetic are corrected.
// - epsilon = 0: No adjustment is applied, leaving results as-is.
//
// Default: 0 (no epsilon adjustment)
epsilon float64
}
// applyOptions applies a set of functional options to a given options struct,
// starting with a set of default values.
//
// Parameters:
// - defaults: The initial geomOptions struct containing default values.
// - opts: A variadic slice of Option functions that modify the geomOptions struct.
//
// Behavior:
// - Each Option function in the opts slice is applied in the order it is provided.
// - The defaults parameter serves as a base configuration, which can be
// overridden by the provided geomOptions.
//
// Returns:
//
// A new geomOptions struct that reflects the default values combined with any
// modifications made by the Option functions.
//
// Example:
//
// defaults := geomOptions{Epsilon: 0}
// geomOptions := applyOptions(defaults, WithEpsilon(1e-10))
// fmt.Println(geomOptions.Epsilon) // Output: 1e-10
//
// This function is used internally to provide a consistent way to handle
// optional parameters across multiple functions.
func applyOptions(defaults geomOptions, opts ...Option) geomOptions {
for _, opt := range opts {
opt(&defaults)
}
return defaults
}
// applyEpsilon adjusts a floating-point value to eliminate small numerical imprecisions
// by snapping it to the nearest whole number if the difference is within a specified epsilon.
//
// Parameters:
// - value: The floating-point number to adjust.
// - epsilon: A small positive threshold. If the absolute difference between `value` and
// the nearest whole number is less than `epsilon`, the value is snapped to that whole number.
//
// Behavior:
// - If `math.Abs(value - math.Round(value)) < epsilon`, the function returns the rounded value.
// - Otherwise, it returns the original value unchanged.
//
// Example:
//
// result := applyEpsilon(-0.9999999999999998, 1e-4)
// // Output: -1.0 (snapped to the nearest whole number)
//
// result := applyEpsilon(1.0001, 1e-4)
// // Output: 1.0001 (unchanged, as it's outside the epsilon threshold)
//
// Returns:
//
// A floating-point number adjusted based on the specified epsilon, or the original value
// if no adjustment is needed.
//
// Notes:
// - This function is commonly used to address floating-point imprecision in geometric
// computations where small deviations can accumulate and affect results.
func applyEpsilon(value, epsilon float64) float64 {
// Round to the nearest whole number if within epsilon
rounded := math.Round(value)
if math.Abs(value-rounded) < epsilon {
return rounded
}
// Otherwise, return the original value
return value
}
// abs computes the absolute value of a signed number.
//
// This function is generic and works for any type that satisfies the
// [SignedNumber] constraint (e.g., int, int32, int64, float32, float64).
//
// Parameters:
// - n (T): The signed number whose absolute value is to be computed.
//
// Returns:
// - The absolute value of the input number.
func abs[T SignedNumber](n T) T {
if n < 0 {
return -n
}
return n
}