Skip to content
Mike Nye edited this page Dec 1, 2024 · 10 revisions

geom2d

geom2d is a Go library for performing computational geometry operations. It provides robust and efficient primitives and algorithms for 2D geometric shapes, including:

  • Points
  • Line Segments
  • Circles
  • Rectangles
  • Polygons (via PolyTree)

The library is designed for use in applications such as graphics, CAD, GIS, and game development.

Key Features

  • Utilises Generics: Supports multiple numeric types (int, float32, float64), providing flexibility
  • Precision and Robustness: Built to handle computational geometry challenges with precision.
  • Boolean Operations: Supports union, intersection, and subtraction for polygons.
  • Transformations: Offers methods to scale, rotate, and reflect geometric shapes.
  • Hierarchy: Manage complex polygons with holes and islands using a tree-based structure.

Getting Started

Start by installing the library in your Go project:

go get github.com/mikenye/geom2d

Documentation

Explore the detailed documentation for each type and operation:

  • Point
  • LineSegment
  • Circle
  • Rectangle
  • PolyTree

Examples

Learn how to use geom2d through practical examples:

  • Basic Usage
  • Boolean Polygon Operations
  • Transformations

Generics Overview for the Uninitiated

This library leverages Go's generics (introduced in Go 1.18) to support multiple numeric types, providing flexibility while maintaining type safety and performance. Generics allow functions, methods, and types to operate on different data types while using a single, reusable implementation.

What is T?

In this library, T is a type parameter that represents any signed numeric type. The constraint for T ensures that only signed numeric types (like int, float64, etc.) are allowed, making it easier to use the library in a variety of applications.

For example, you can use this package with both integer and floating-point numbers:

p1 := NewPoint(3, 4)     // p1 will be Point[int] type
p2 := NewPoint(3.5, 4.5) // p2 will be Point[float64] type

In both cases, T resolves to the respective numeric type (int or float64).

Furthermore, the type can be specified on the function:

p3 := NewPoint[float64](3, 4) // p2 will be Point[float64] type, even though integer values given

Why Use Generics?

Generics allow you to:

  • Avoid type conversions (e.g., between int and float64).
  • Prevent type-related runtime errors by enforcing type safety at compile time.
  • Reuse code for different numeric types without duplication.

Understanding Function Signatures

You’ll often see methods and functions returning or accepting T. For example:

func (p Point[T]) X() T

Here, X() returns the x-coordinate of the point, with the type of T depending on how the Point was created (e.g., int, float64, etc.). This means the method will work seamlessly whether the Point is defined with integer or floating-point types.

Default Return of float64 in Certain Functions

While this library supports any signed numeric type for input (via T), some functions return results as float64 by design. This is particularly relevant for functions that involve calculations which are inherently decimal, such as distances or geometric intersections.

For example, the Point[T].ProjectOntoLineSegment function calculates the orthogonal projection of a Point onto a LineSegment. The result of this operation is returned as a Point[float64]:

func (p Point[T]) DistanceToLineSegment(l LineSegment[T]) float64

Even if the input types are integers, the projection’s coordinates are likely to include decimal components. Returning a Point[float64] ensures precision and avoids truncation during such calculations.

Why float64 is Used

  • Precision: Calculations like distance or intersection often require fractional precision to be meaningful. Using float64 avoids rounding errors or truncation.
  • Simplicity: Returning float64 avoids forcing users to handle type conversions for commonly expected results, especially in mathematical contexts.

Handling the Result

If you need the result in integer form, the library provides helper functions for type conversion:

  • type.AsInt(): Truncates the decimal component of each coordinate.
  • type.AsIntRounded(): Rounds each coordinate to the nearest integer.

Example

p := geom2d.NewPoint[int](3, 4)
l := geom2d.NewLineSegment[int](geom2d.NewPoint(0, 0), geom2d.NewPoint(10, 0))

projection := p.ProjectOntoLineSegment(l) // Returns a Point[float64]
fmt.Println("Projection as Point[float64]:", projection)

projectionAsInt := projection.AsInt()               // Truncated
projectionAsIntRounded := projection.AsIntRounded() // Rounded

fmt.Println("Projection as Point[int] (truncated):", projectionAsInt)
fmt.Println("Projection as Point[int] (rounded):", projectionAsIntRounded)

Output

Projection as Point[float64]: Point[(3, 0)]
Projection as Point[int] (truncated): Point[(3, 0)]
Projection as Point[int] (rounded): Point[(3, 0)]

Consistency Across Functions

This design is applied consistently across functions that return results with inherent decimal precision.

By defaulting to float64, the library ensures precision is preserved while leaving you in control of how to handle the result in your application.

Clone this wiki locally