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

A 3D point with optional color, surface normal, and attributes.

## Fields

  * `:x`, `:y`, `:z` — `float()` Cartesian coordinates. `new/4` requires
    them and converts numbers to floats; the struct defaults are `0.0`.
    Units are application-defined and must be consistent within a cloud.
  * `:color` — `rgb() | rgba() | nil`; defaults to `nil`. Channels are
    non-negative integers conventionally in `0..255`; alpha is last.
  * `:normal` — `normal() | nil`; defaults to `nil`. Components follow the
    same axis convention as the coordinates and are normally unit length,
    though this module does not normalize them.
  * `:attributes` — `attributes()`; defaults to `%{}`. Keys are **strings**
    (atoms passed to `new/4` are converted via `to_string/1`). Values are
    numbers or binaries.

## Example

    iex> ExCodecs.Spatial.Point.new(1, 2.5, -3, color: {255, 128, 0}, normal: {0.0, 0.0, 1.0})
    %ExCodecs.Spatial.Point{
      x: 1.0,
      y: 2.5,
      z: -3.0,
      color: {255, 128, 0},
      normal: {0.0, 0.0, 1.0},
      attributes: %{}
    }

# `attributes`

```elixir
@type attributes() :: %{optional(String.t()) =&gt; number() | binary()}
```

User attributes keyed by strings, with numeric or binary values.
For example, `%{"intensity" => 0.82, "classification" => 2}`.
Atom keys passed to `new/4` are normalized to strings.

PLY encode (the only codec that stores attributes) accepts **numeric**
attribute values only; binary values will cause an encode error. Use
cloud-level `Metadata` for non-numeric payloads.

# `normal`

```elixir
@type normal() :: {float(), float(), float()}
```

A surface-normal vector `{nx, ny, nz}` in the point coordinate convention.
Unit length is conventional but is not enforced. For example,
`{0.0, 0.0, 1.0}` points along the positive Z axis.

# `rgb`

```elixir
@type rgb() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}
```

An RGB color tuple `{red, green, blue}`. Channels are non-negative integers,
conventionally in `0..255`; that range is not enforced. For example,
`{255, 128, 0}` represents orange.

# `rgba`

```elixir
@type rgba() ::
  {non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
```

An RGBA color tuple `{red, green, blue, alpha}`. Channels are non-negative
integers, conventionally in `0..255`; that range is not enforced. For
example, `{0, 64, 255, 128}` is a half-transparent blue.

# `t`

```elixir
@type t() :: %ExCodecs.Spatial.Point{
  attributes: attributes(),
  color: rgb() | rgba() | nil,
  normal: normal() | nil,
  x: float(),
  y: float(),
  z: float()
}
```

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

# `colored?`

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

Returns whether the point has RGB or RGBA color.

## Arguments

  * `point` — `%Point{}`

## Returns

`true` | `false`

## Raises

* `FunctionClauseError` if the argument is not a `%Point{}` or its `:color`
  is neither `nil`, a 3-tuple, nor a 4-tuple.

## Examples

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

# `coords`

```elixir
@spec coords(t()) :: {float(), float(), float()}
```

Returns `{x, y, z}`.

## Arguments

  * `point` — `%Point{}`

## Returns

`{float(), float(), float()}`

## Raises

* `FunctionClauseError` if `point` is not a `%Point{}`.

## Examples

    iex> ExCodecs.Spatial.Point.coords(ExCodecs.Spatial.Point.new(1, 2, 3))
    {1.0, 2.0, 3.0}

# `has_normal?`

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

Returns whether the point has a surface normal.

## Arguments

  * `point` — `%Point{}`

## Returns

`true` | `false`

## Raises

* `FunctionClauseError` if the argument is not a `%Point{}` or its `:normal`
  is neither `nil` nor a 3-tuple.

## Examples

    iex> ExCodecs.Spatial.Point.has_normal?(ExCodecs.Spatial.Point.new(0, 0, 0))
    false
    iex> ExCodecs.Spatial.Point.has_normal?(ExCodecs.Spatial.Point.new(0, 0, 0, normal: {0.0, 1.0, 0.0}))
    true

# `new`

```elixir
@spec new(number(), number(), number(), keyword()) :: t()
```

Builds a point from coordinates and optional fields.

## Arguments

  * `x`, `y`, `z` — numbers (stored as floats)
  * `opts`:
    * `:color` — `rgb` or `rgba` tuple, or `nil`
    * `:normal` — `{nx, ny, nz}` or `nil`
    * `:attributes` — map of string keys (default `%{}`); atom keys are
      stringified

## Returns

`%ExCodecs.Spatial.Point{}`

## Raises

* `FunctionClauseError` if a coordinate is not numeric, or if `opts` is not
  a keyword list accepted by `Keyword.get/3`.

## Notes

Literal `%Point{}` construction with atom-keyed attributes bypasses
stringification; codecs that read attributes (e.g. PLY) treat atom keys as
missing and default them to `0.0`. Always use `Point.new/4` or pre-stringify
keys when the point will be encoded.

## Examples

    iex> p = ExCodecs.Spatial.Point.new(1.0, 2.0, 3.0)
    iex> {p.x, p.y, p.z}
    {1.0, 2.0, 3.0}

    iex> p = ExCodecs.Spatial.Point.new(0.0, 0.0, 0.0, color: {255, 128, 0})
    iex> p.color
    {255, 128, 0}

---

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