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

Compact little-endian binary format for point clouds.

## Layout

    magic:   "EXCP" (4 bytes)
    version: u16 LE = 1
    flags:   u16 LE  (bit0=color, bit1=alpha, bit2=normal)
    count:   u64 LE
    records: count × record

Each record always has `x,y,z` as `f32`. Optional `rgb`/`rgba` as `u8`
and normals as `f32` follow according to flags.

Generic attributes are not stored in this format  -  use PLY for that.

# `decode`

```elixir
@spec decode(
  binary(),
  keyword()
) :: {:ok, ExCodecs.Spatial.PointCloud.t()} | {:error, ExCodecs.Error.t()}
```

Decodes an EXCP version 1 payload into a point cloud.

The decoder reads the global color/alpha/normal flags and the declared number
of fixed-shape records described by `encode/2`. Trailing bytes are currently
ignored. Decoded metadata contains `%{"format" => "excp", "version" => 1}`;
attributes and other cloud-level fields use their constructor defaults.

## Arguments

  * `data` (`binary()`)  -  a complete EXCP payload.
  * `opts` (`keyword()`)  -  reserved and currently ignored.

## Returns

  * `{:ok, %PointCloud{}}`
  * `{:error, %ExCodecs.Error{reason: :invalid_data,
    codec: :spatial_binary}}` when the magic/header is invalid or too short,
    the version is not 1, or a declared coordinate, RGB/RGBA, or normal field
    is truncated.

## Raises / exceptions

Corrupt/truncated payloads covered above are returned, and `opts` is ignored.
This function has a catch-all data clause, so non-binary input also returns
`:invalid_data`; it does not intentionally raise for external payloads.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> {:ok, bin} = ExCodecs.Spatial.Codec.Binary.encode(PointCloud.new([Point.new(1.0, 2.0, 3.0)]))
    iex> {:ok, %PointCloud{points: [p]}} = ExCodecs.Spatial.Codec.Binary.decode(bin)
    iex> p.x
    1.0

# `encode`

```elixir
@spec encode(
  ExCodecs.Spatial.PointCloud.t(),
  keyword()
) :: {:ok, binary()} | {:error, ExCodecs.Error.t()}
```

Encodes a point cloud in the EXCP version 1 binary format.

The 16-byte header is `"EXCP"`, version `u16` little-endian, global flags
`u16` little-endian, and point count `u64` little-endian. Each record starts
with three little-endian `f32` coordinates. Global flag bits then add RGB
(3 bytes), RGBA (4 bytes; alpha takes precedence), and/or three little-endian
`f32` normal components to every record.

Flags are selected from the whole cloud. Consequently, points missing an
optional field are zero-filled (missing alpha is 255). Point attributes,
cloud bounds, transform, and metadata are not represented.

## Arguments

  * `data` (`PointCloud.t()`)  -  a cloud whose `points` are valid `%Point{}`
    structs with numeric coordinates, optional `{r, g, b}` or
    `{r, g, b, a}` color, and optional `{nx, ny, nz}` normal.
  * `opts` (`keyword()`)  -  reserved and currently ignored.

## Returns

  * `{:ok, payload}` where `payload` is an EXCP `binary()`.
  * `{:error, %ExCodecs.Error{reason: :invalid_data,
    codec: :spatial_binary}}` when `data` is not a `%PointCloud{}`.

## Raises / exceptions

A wrong top-level type is returned as `:invalid_data`. Because `opts` is
ignored, even a non-keyword value does not raise. Manually malformed cloud or
point structs can raise `FunctionClauseError`, `MatchError`, `ArgumentError`,
or a bitstring construction exception when a point, tuple shape, channel
range, or numeric field violates the struct contract.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> cloud = PointCloud.new([Point.new(1, 2, 3, color: {10, 20, 30})])
    iex> {:ok, <<"EXCP", 1::little-16, flags::little-16, 1::little-64, _::binary>>} =
    ...>   ExCodecs.Spatial.Codec.Binary.encode(cloud)
    iex> Bitwise.band(flags, 0b001)
    1

# `stream_decode`

```elixir
@spec stream_decode(
  Path.t() | binary(),
  keyword()
) :: Enumerable.t()
```

Returns an enumerable over points in an EXCP payload or file.

## Streaming behavior

  * `source: :file` (or `:auto` path detection): header then chunked
    records  -  Rust mmap + DirtyCpu unpack when available, otherwise
    `IO.binread/2` per record.
  * `source: :binary`: chunked Rust unpack when available; otherwise
    materializes through `decode/2`.

Prefer `source: :file` for multi-MB / multi-GB `.excp` paths.
Pass `accel: false` to force the pure-Elixir path.

## Arguments

  * `source` (`Path.t() | binary()`)  -  filesystem path or complete EXCP
    payload. Both are binaries, so `:source` controls resolution.
  * `opts` (`keyword()`)  -  `:source` may be `:binary` (default), `:file`, or
    `:auto`. Prefer `:file` for paths and `:binary` for payloads; `:auto` is
    opt-in path sniffing (see `docs/spatial_formats.md`).
    `:binary`.

## Returns

An `Enumerable.t()` yielding `%Point{}` values. Failures are delayed until
enumeration as exactly one `{:error, %ExCodecs.Error{}}`:

  * `reason: :io_error`  -  the file cannot be opened or read
  * `reason: :invalid_data`  -  bad magic/version, truncated header, or a
    truncated record mid-stream (`codec: :spatial_binary`)

## Raises / exceptions

Raises `FunctionClauseError` when `source` is not a binary. Unsupported
`:source` values raise `CaseClauseError`.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> {:ok, bin} = ExCodecs.Spatial.Codec.Binary.encode(PointCloud.new([Point.new(0, 0, 0)]))
    iex> [%Point{x: 0.0, y: 0.0, z: 0.0}] =
    ...>   ExCodecs.Spatial.Codec.Binary.stream_decode(bin) |> Enum.to_list()

# `stream_encode_to_file`

```elixir
@spec stream_encode_to_file(Enumerable.t(), Path.t(), keyword()) ::
  :ok | {:error, ExCodecs.Error.t()}
```

Streams `%Point{}` values to an EXCP file using an explicit schema.

Writes a placeholder header, encodes each point as it arrives, then seeks
back to patch the final count. Peak memory is O(one point), not the cloud.

## Arguments

  * `enumerable` (`Enumerable.t()`)  -  `%Point{}` elements.
  * `path` (`Path.t()`)  -  destination file path.
  * `opts` (`keyword()`)  -  requires `:schema`, a list such as `[]`,
    `[:color]`, `[:color, :alpha]`, `[:normal]`, or `[:color, :normal]`.
    Map form `%{color: true, alpha: false, normal: true}` is also accepted.
    `:alpha` implies color bytes (RGBA).

## Returns

  * `:ok`
  * `{:error, %ExCodecs.Error{reason: :invalid_options}}` when `:schema` is
    missing or invalid
  * `{:error, %ExCodecs.Error{reason: :invalid_data}}` when an element is not
    a `%Point{}`
  * `{:error, %ExCodecs.Error{reason: :io_error}}` on file failures

## Raises / exceptions

Schema and non-`%Point{}` element failures are returned. Invalid path terms or
non-keyword `opts` can raise `FunctionClauseError` or `ArgumentError` in the
path/file/keyword APIs. Malformed point field shapes can raise during
bitstring construction (same class of exceptions as `encode/2`).

## Examples

    iex> alias ExCodecs.Spatial.{Point, Codec.Binary}
    iex> path = Path.join(System.tmp_dir!(), "excp_enc_89156.excp")
    iex> :ok = Binary.stream_encode_to_file([Point.new(1, 2, 3, color: {1, 2, 3})], path, schema: [:color])
    iex> {:ok, <<"EXCP", _::binary>>} = File.read(path)
    iex> File.rm!(path)
    :ok

---

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