# `ExCodecs.Spatial.PointCloud`
[🔗](https://github.com/AnimaLogica/codecs/blob/v0.2.4/lib/ex_codecs/spatial/point_cloud.ex#L1)

A collection of `%ExCodecs.Spatial.Point{}` values with bounds and metadata.

## Fields

  * `:points` — `[Point.t()]`; defaults to `[]`. Ordering is preserved.
    Coordinate units and conventions are defined by the application.
  * `:bounds` — `Bounds.t() | nil`; defaults to `nil`. `new/2` computes an
    axis-aligned box by default; `nil` also represents an empty cloud or
    deliberately uncomputed bounds.
  * `:metadata` — `Metadata.t()`; defaults to `%Metadata{}`.

## Example

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> cloud = PointCloud.new([Point.new(0, 0, 0), Point.new(2, 1, -1)])
    iex> {PointCloud.size(cloud), cloud.bounds.max_x}
    {2, 2.0}

# `t`

```elixir
@type t() :: %ExCodecs.Spatial.PointCloud{
  bounds: ExCodecs.Spatial.Bounds.t() | nil,
  metadata: ExCodecs.Spatial.Metadata.t(),
  points: [ExCodecs.Spatial.Point.t()]
}
```

A point cloud. See the module documentation for every field, its default,
units or conventions, and a construction example.

# `add_point`

```elixir
@spec add_point(t(), ExCodecs.Spatial.Point.t()) :: t()
```

Appends one point and expands bounds.

Prefer `new/1` or `add_points/2` for bulk inserts (`++` is O(n)).

## Arguments

  * `cloud` — `%PointCloud{}`
  * `point` — `%Point{}`

## Returns

Updated `%PointCloud{}`

## Raises

* `FunctionClauseError` unless the arguments are a `%PointCloud{}` and a
  `%Point{}`, or if existing bounds are neither `nil` nor `%Bounds{}`.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> c = PointCloud.new([])
    iex> PointCloud.size(PointCloud.add_point(c, Point.new(1, 2, 3)))
    1

# `add_points`

```elixir
@spec add_points(t(), [ExCodecs.Spatial.Point.t()]) :: t()
```

Appends many points in one concat and one bounds fold (O(n)).

Prefer this over repeated `add_point/2` for bulk inserts.

## Arguments

  * `cloud` — `%PointCloud{}`
  * `new_points` — list of `%Point{}`

## Returns

Updated `%PointCloud{}`

## Raises

* `FunctionClauseError` unless `cloud` is a `%PointCloud{}` and
  `new_points` is a list, or if any element is not a `%Point{}`.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> c = PointCloud.add_points(PointCloud.new([]), [Point.new(0, 0, 0), Point.new(1, 1, 1)])
    iex> PointCloud.size(c)
    2

# `colored?`

```elixir
@spec colored?(t()) :: boolean()
```

Returns `true` when every point has color (empty cloud → `false`).

## Arguments

  * `cloud` — `%PointCloud{}`

## Returns

boolean

## Raises

* `FunctionClauseError` if `cloud` is not a `%PointCloud{}`, or if any
  point has an unsupported `:color` value.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> PointCloud.colored?(PointCloud.new([Point.new(0, 0, 0, color: {1, 2, 3})]))
    true

# `has_normals?`

```elixir
@spec has_normals?(t()) :: boolean()
```

Returns `true` when every point has a normal (empty → `false`).

## Arguments

  * `cloud` — `%PointCloud{}`

## Returns

boolean

## Raises

* `FunctionClauseError` if `cloud` is not a `%PointCloud{}`, or if any
  point has an unsupported `:normal` value.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> PointCloud.has_normals?(PointCloud.new([Point.new(0, 0, 0)]))
    false

# `new`

```elixir
@spec new(
  [ExCodecs.Spatial.Point.t()],
  keyword()
) :: t()
```

Builds a point cloud from a list of points.

## Arguments

  * `points` — list of `%Point{}`
  * `opts`:
    * `:compute_bounds` — boolean (default `true`)
    * `:bounds` — explicit `%Bounds{}` (overrides compute)
    * `:metadata` — `%Metadata{}`

## Returns

`%PointCloud{}`

## Raises

* `FunctionClauseError` if `points` is not a list, an element is not
  point-like while bounds are computed, or `opts` is not a keyword list.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> cloud = PointCloud.new([Point.new(0, 0, 0), Point.new(1, 1, 1)])
    iex> PointCloud.size(cloud)
    2

# `size`

```elixir
@spec size(t()) :: non_neg_integer()
```

Number of points.

## Arguments

  * `cloud` — `%PointCloud{}`

## Returns

non-negative integer

## Raises

* `FunctionClauseError` if `cloud` is not a `%PointCloud{}`.
* `ArgumentError` if a manually constructed cloud has a non-list `:points`.

## Examples

    iex> ExCodecs.Spatial.PointCloud.size(ExCodecs.Spatial.PointCloud.new([]))
    0

# `with_bounds`

```elixir
@spec with_bounds(t()) :: t()
```

Recomputes axis-aligned bounds from points.

## Arguments

  * `cloud` — `%PointCloud{}`

## Returns

`%PointCloud{}` with updated `bounds`

## Raises

* `FunctionClauseError` if `cloud` is not a `%PointCloud{}` or a point is
  not a supported point-like value.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> cloud = PointCloud.new([Point.new(0, 0, 0)], compute_bounds: false)
    iex> PointCloud.with_bounds(cloud).bounds != nil
    true

---

*Consult [api-reference.md](api-reference.md) for complete listing*
