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

Simple little-endian binary format for Gaussian splat clouds.

## Layout

    magic:   "GSPL" (4 bytes)
    version: u16 LE = 1
    flags:   u16 LE  (bit0 = has SH rest coeffs count in header)
    count:   u64 LE
    sh_rest: u16 LE  (number of f_rest floats per Gaussian; 0 if none)
    records: count × record

Each record:

    position:  3 × f32
    color:     3 × f32   (f_dc)
    opacity:   f32
    scale:     3 × f32
    rotation:  4 × f32   (w, x, y, z)
    sh_rest:   sh_rest × f32 (optional)

# `decode`

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

Decodes a GSPL version 1 payload into a Gaussian cloud.

The decoder reads the header and declared record count described by
`encode/2`. When the shared SH-rest count is nonzero, each decoded
Gaussian's `sh` is `[[r, g, b] | Enum.chunk_every(rest, 3)]`; otherwise it
is `nil`. Trailing bytes and unknown flag bits are currently ignored.
Decoded cloud metadata contains `%{"format" => "gsplat", "version" => 1}`.

## Arguments

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

## Returns

  * `{:ok, %GaussianCloud{}}`
  * `{:error, %ExCodecs.Error{reason: :invalid_data, codec: :gsplat}}` when
    the magic/header is invalid or too short, the version is not 1, a
    declared 14-float record is truncated, or its declared SH-rest values
    are truncated.

## Raises / exceptions

The external payload failures above are returned. `opts` is ignored, and
non-binary input is handled by the catch-all clause as `:invalid_data`; this
function does not intentionally raise for external payloads.

## Examples

    iex> alias ExCodecs.Spatial.{Gaussian, GaussianCloud}
    iex> {:ok, bin} = ExCodecs.Spatial.Codec.Gsplat.encode(GaussianCloud.new([Gaussian.new({1.0, 0.0, 0.0})]))
    iex> {:ok, %GaussianCloud{gaussians: [g]}} = ExCodecs.Spatial.Codec.Gsplat.decode(bin)
    iex> elem(g.position, 0)
    1.0

# `encode`

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

Encodes a Gaussian cloud in the GSPL version 1 binary format.

The 18-byte header contains `"GSPL"`, version `u16` little-endian, flags
`u16` little-endian, Gaussian count `u64` little-endian, and a shared
spherical-harmonic-rest count `u16` little-endian. Each record contains 14
little-endian `f32` values: position XYZ, DC color RGB, opacity, scale XYZ,
and quaternion rotation `(w, x, y, z)`, followed by the shared number of
little-endian `f32` SH-rest values.

The shared SH count is the longest flattened rest coefficient list in the
cloud; shorter lists are padded with zero. The DC coefficient is represented
by `Gaussian.color`. Gaussian metadata and cloud-level metadata are not
stored.

## Arguments

  * `data` (`GaussianCloud.t()`)  -  a cloud of valid `%Gaussian{}` structs.
    Each Gaussian has position/color/scale 3-tuples, a rotation 4-tuple,
    numeric opacity, and optional SH data shaped as `[dc | rest]` (nested
    rest lists are flattened).
  * `opts` (`keyword()`)  -  reserved and currently ignored.

## Returns

  * `{:ok, payload}` where `payload` is a GSPL `binary()`.
  * `{:error, %ExCodecs.Error{reason: :invalid_data, codec: :gsplat}}` when
    `data` is not a `%GaussianCloud{}`.

## Raises / exceptions

A wrong top-level type is returned as `:invalid_data`, and `opts` is ignored.
Manually malformed cloud/Gaussian structs can raise `FunctionClauseError`,
`MatchError`, `ArgumentError`, `ArithmeticError`, or a bitstring construction
exception for invalid SH shapes, tuple shapes, counts, or non-numeric values.

## Examples

    iex> alias ExCodecs.Spatial.{Gaussian, GaussianCloud}
    iex> cloud = GaussianCloud.new([Gaussian.new({0, 0, 0}, opacity: 0.75)])
    iex> {:ok, <<"GSPL", 1::little-16, _flags::little-16, 1::little-64,
    ...>          0::little-16, _record::binary>>} =
    ...>   ExCodecs.Spatial.Codec.Gsplat.encode(cloud)
    iex> true
    true

# `stream_decode`

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

Returns an enumerable over Gaussians in a GSPL 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 `.gspl` paths.
Pass `accel: false` to force the pure-Elixir path.

## Arguments

  * `source` (`Path.t() | binary()`)  -  filesystem path or complete GSPL
    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 `%Gaussian{}` 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: :gsplat`)

## Raises / exceptions

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

## Examples

    iex> alias ExCodecs.Spatial.{Gaussian, GaussianCloud}
    iex> {:ok, bin} = ExCodecs.Spatial.Codec.Gsplat.encode(GaussianCloud.new([Gaussian.new({0, 0, 0})]))
    iex> [%Gaussian{opacity: 1.0}] =
    ...>   ExCodecs.Spatial.Codec.Gsplat.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 `%Gaussian{}` values to a GSPL file using an explicit schema.

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

## Arguments

  * `enumerable` (`Enumerable.t()`)  -  `%Gaussian{}` elements.
  * `path` (`Path.t()`)  -  destination file path.
  * `opts` (`keyword()`)  -  requires `:schema`. Use `[]` / `%{}` for no SH
    rest coefficients, or `[sh_rest: n]` / `%{sh_rest: n}` for `n` shared
    rest floats per Gaussian (shorter lists are zero-padded).

## 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 `%Gaussian{}`
  * `{:error, %ExCodecs.Error{reason: :io_error}}` on file failures

## Raises / exceptions

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

## Examples

    iex> alias ExCodecs.Spatial.{Gaussian, Codec.Gsplat}
    iex> path = Path.join(System.tmp_dir!(), "gspl_enc_73763.gspl")
    iex> :ok = Gsplat.stream_encode_to_file([Gaussian.new({0, 0, 0})], path, schema: [])
    iex> {:ok, <<"GSPL", _::binary>>} = File.read(path)
    iex> File.rm!(path)
    :ok

---

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