README.md

# Colliders

An Elixir library for detecting whether a bounding box (bbox) meaningfully
overlaps a polygon. Designed for use cases such as checking if an AI model
detection (a bbox) falls within a region of interest (a polygon).

A detection is considered a hit only when the overlap meets a configurable
minimum threshold (default: **5%**).

## How It Works

Colliders uses a two-phase approach for both correctness and performance:

1. **Axis-Aligned Bounding Box (AABB) filter** — Compares the bbox against the
   polygon's precomputed axis-aligned bounding box. If the bbox is entirely
   outside it, `false` is returned immediately without any further computation.

2. **Sutherland-Hodgman clipping** — If the AABB check passes, the polygon is
   clipped to the bbox using the
   [Sutherland-Hodgman algorithm](https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm).
   The area of the resulting intersection polygon is then compared to the bbox
   area to compute the overlap percentage.

## Installation

Add `colliders` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:colliders, "~> 0.1.3"}
  ]
end
```

Then fetch dependencies:

```bash
mix deps.get
```

## Usage

### 1. Define your polygon

Use `Colliders.Polygon.new/2` to create a prepared polygon. This precomputes the
polygon's bounding box bounds once, so repeated checks against it are fast.

The points list accepts `%PolygonPoint{}` structs, maps with `x`/`y` keys, or
`{x, y}` tuples.

```elixir
alias Colliders.Polygon
alias Colliders.Types.PolygonPoint

polygon =
  Polygon.new([
    %PolygonPoint{x: 100, y: 50},
    %PolygonPoint{x: 300, y: 50},
    %PolygonPoint{x: 300, y: 200},
    %PolygonPoint{x: 100, y: 200}
  ])
```

You can also attach arbitrary metadata to a polygon (e.g. an ID or label):

```elixir
polygon = Polygon.new(points, %{id: "zone_a", label: "Entrance"})
```

### 2. Check if a bounding box hits the polygon

```elixir
alias Colliders
alias Colliders.Types.BBox

# A detection bbox: x/y is the top-left corner, w/h are width and height
bbox = %BBox{x: 295, y: 100, w: 30, h: 40}

Colliders.bbox_intersects_polygon?(bbox, polygon)
# => true
```

The default threshold is **5%** — the bbox must overlap at least 5% of its own
area with the polygon to return `true`.

### 3. Custom threshold

```elixir
# Require at least 50% of the bbox to be inside the polygon
Colliders.bbox_intersects_polygon?(bbox, polygon, 50.0)
```

### 4. Get the exact overlap percentage

```elixir
Colliders.bbox_overlap_percentage(bbox, polygon)
# => 16.666...
```

Returns a float between `0.0` (no overlap) and `100.0` (bbox fully inside the
polygon).

## Types

### `%Colliders.Types.PolygonPoint{}`

A vertex in a polygon.

| Field | Type    | Description           |
| ----- | ------- | --------------------- |
| `x`   | `float` | Horizontal coordinate |
| `y`   | `float` | Vertical coordinate   |

Use `PolygonPoint.new/1` to build one from a map or tuple — integers are
automatically converted to floats:

```elixir
PolygonPoint.new({10, 20})                # => %PolygonPoint{x: 10.0, y: 20.0}
PolygonPoint.new(%{x: 10.0, y: 20.0})     # => %PolygonPoint{x: 10.0, y: 20.0}
PolygonPoint.new(%{"x" => 10, "y" => 20}) # => %PolygonPoint{x: 10.0, y: 20.0}
```

### `%Colliders.Types.BBox{}`

An axis-aligned bounding box. `x` and `y` are the **top-left corner**.

| Field | Type    | Description |
| ----- | ------- | ----------- |
| `x`   | `float` | Left edge   |
| `y`   | `float` | Top edge    |
| `w`   | `float` | Width       |
| `h`   | `float` | Height      |

Use `BBox.new/1` to build one from a map or tuple:

```elixir
BBox.new({10, 20, 100, 50})                              # => %BBox{x: 10.0, y: 20.0, w: 100.0, h: 50.0}
BBox.new(%{x: 10.0, y: 20.0, w: 100.0, h: 50.0})         # => %BBox{x: 10.0, y: 20.0, w: 100.0, h: 50.0}
BBox.new(%{"x" => 10, "y" => 20, "w" => 100, "h" => 50}) # => %BBox{x: 10.0, y: 20.0, w: 100.0, h: 50.0}
```

### `%Colliders.Polygon{}`

A prepared polygon. Create it with `Colliders.Polygon.new/2` — do **not** build
the struct manually, as the precomputed AABB bounds (`min_x`, `max_x`, `min_y`,
`max_y`) will be missing.

| Field    | Type                     | Description              |
| -------- | ------------------------ | ------------------------ |
| `points` | `list(PolygonPoint.t())` | Polygon vertices         |
| `meta`   | `map()`                  | Arbitrary metadata       |
| `min_x`  | `float`                  | Precomputed left bound   |
| `max_x`  | `float`                  | Precomputed right bound  |
| `min_y`  | `float`                  | Precomputed top bound    |
| `max_y`  | `float`                  | Precomputed bottom bound |

## API

### `Colliders.bbox_intersects_polygon?(bbox, polygon, threshold \\ 5.0)`

Returns `true` if at least `threshold`% of the bbox area overlaps the polygon.

### `Colliders.bbox_overlap_percentage(bbox, polygon)`

Returns the percentage of the bbox area that overlaps the polygon, as a float
between `0.0` and `100.0`.

### `Colliders.Polygon.new(points, meta \\ %{})`

Creates a `%Polygon{}` struct, precomputing its AABB bounds. The points list
accepts `%PolygonPoint{}` structs, atom/string-key maps, or `{x, y}` tuples —
all are normalized to `%PolygonPoint{}` with float coordinates. Raises
`ArgumentError` if fewer than 3 points are given.

### `Colliders.Types.PolygonPoint.new(point)`

Creates a `%PolygonPoint{}` from a `{x, y}` tuple, an atom-key map
(`%{x: ..., y: ...}`), or a string-key map (`%{"x" => ..., "y" => ...}`).
Integer values are converted to floats. Raises `ArgumentError` for any other
input.

### `Colliders.Types.BBox.new(coords)`

Creates a `%BBox{}` from a `{x, y, w, h}` tuple, an atom-key map
(`%{x: ..., y: ..., w: ..., h: ...}`), or a string-key map
(`%{"x" => ..., ...}`). Integer values are converted to floats. Raises
`ArgumentError` for any other input.

## Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b my-feature`)
3. Write tests for your changes
4. Make sure all tests pass (`mix test`)
5. Open a pull request

## License

See [LICENSE](https://github.com/monoflow-ayvu/colliders/blob/main/LICENSE) for
details.