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

Stream helpers for spatial formats.

## What actually streams today

  * **EXCP** (`:spatial_binary`), **GSPL** (`:gsplat`), and **PLY** with
    `source: :file` (or `:auto` path detection) read the header, then one
    record/vertex at a time from disk  -  O(header + one record) memory.
    Binary PLY uses a fixed stride; ASCII PLY reads lines.
  * In-memory binaries still **materialize** through `decode/2`, then yield.
    A future Rust backend may memory-map large binaries.
  * **`encode_to_file/3`** with an explicit `:schema` streams EXCP/GSPL to
    disk (placeholder header, then seek-back count patch). Without `:schema`,
    encoding still collects in memory first.

Prefer `source: :file` when the argument is a filesystem path, or
`source: :binary` when it is an encoded payload (`:binary` is the default).
With explicit `:auto`, a short path-like binary that names a regular file is
opened; otherwise the argument is treated as payload bytes. Prefer explicit
`:file` / `:binary` for untrusted input  -  `:auto` can open local paths.
Path-like means under 4 KiB, no `ply`/`EXCP`/`GSPL` magic prefix, and
contains `/` or `\` or ends with `.ply`/`.excp`/`.gspl`/`.bin`, **and**
`File.regular?/1` is true.

# `decode`

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

Returns an enumerable over points or Gaussians decoded from a path or binary.

EXCP/GSPL/PLY file sources stream incrementally; in-memory binaries still
materialize before emitting elements.

## Arguments

  * `source` (`Path.t() | binary()`)  -  a path or complete encoded payload.
    Paths and payloads are both binaries, so `:source` controls resolution.
  * `opts` (`keyword()`)  -  requires `:format`: `:ply`,
    `:spatial_binary`, or `:gsplat`. `:source` may be `:binary` (default),
    `:file`, or `:auto`.
    `:file`, or `:binary`; PLY also accepts `:as`.

## Returns

An `Enumerable.t()` yielding `%Point{}` for PLY/EXCP point clouds or
`%Gaussian{}` for PLY/GSPL Gaussian clouds. A failure is delayed until
enumeration and represented by exactly one
`{:error, %ExCodecs.Error{}}` element:

  * `reason: :invalid_options`  -  `:format` is absent.
  * `reason: :unsupported_codec`  -  `:format` is unknown.
  * `reason: :io_error`  -  a selected file cannot be opened or read.
  * `reason: :invalid_data`  -  the payload is malformed, unsupported, or
    truncated.

## Raises / exceptions

A missing format does not raise. `Keyword.fetch/2` and decoder option access
raise `FunctionClauseError` when `opts` is not a proper keyword list. PLY
source dispatch raises `FunctionClauseError` for a non-binary `source`.
EXCP/GSPL source resolution has no clause for non-binaries and may return an
invalid value that causes `CaseClauseError`; an unsupported `:source` value
also raises `CaseClauseError`. Decoder exceptions may occur during
enumeration.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> {:ok, bin} = ExCodecs.Spatial.encode(PointCloud.new([Point.new(0.0, 0.0, 0.0)]), format: :ply)
    iex> [%Point{}] = ExCodecs.Spatial.Stream.decode(bin, format: :ply) |> Enum.to_list()
    iex> true
    true

# `encode`

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

Encodes an enumerable of points or Gaussians after collecting it in memory.

A non-empty enumerable is classified by its first element. Empty input is
encoded as an empty `%PointCloud{}` for PLY/EXCP and as an empty
`%GaussianCloud{}` for GSPL.

## Arguments

  * `enumerable` (`Enumerable.t()`)  -  `%Point{}` or `%Gaussian{}` elements.
    The list should be homogeneous and contain valid struct shapes.
  * `opts` (`keyword()`)  -  requires `:format`: `:ply`,
    `:spatial_binary`, or `:gsplat`. Remaining keys are forwarded to
    `ExCodecs.Spatial.encode/2`.

## Returns

  * `{:ok, payload}` where `payload` is a `binary()`.
  * `{:error, %ExCodecs.Error{reason: :invalid_options}}` if `:format` is
    absent.
  * `{:error, %ExCodecs.Error{reason: :unsupported_codec}}` for an unknown
    format (including empty input).
  * `{:error, %ExCodecs.Error{reason: :invalid_data}}` when the first item is
    an error tuple or is not a `%Point{}`/`%Gaussian{}`, when cloud and format
    are incompatible, or when codec validation fails.

## Raises / exceptions

`Enum.to_list/1` raises `Protocol.UndefinedError` for a non-enumerable and
propagates exceptions raised by enumeration. Keyword access raises
`FunctionClauseError` for a non-keyword `opts`. Manually malformed point or
Gaussian structs can raise codec shape/numeric exceptions; PLY converts its
encoding exceptions to `:invalid_data`.

## Examples

    iex> alias ExCodecs.Spatial.Point
    iex> {:ok, bin} = ExCodecs.Spatial.Stream.encode([Point.new(1.0, 0.0, 0.0)], format: :ply)
    iex> is_binary(bin)
    true

# `encode_to_file`

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

Encodes spatial data and writes the binary to `path`.

When `:schema` is present and `:format` is `:spatial_binary` or `:gsplat`,
points/Gaussians are written incrementally via
`Binary.stream_encode_to_file/3` / `Gsplat.stream_encode_to_file/3`
(placeholder header + seek-back count). Otherwise the payload is encoded in
memory and written with `File.write/2`.

## Arguments

  * `data` (`PointCloud.t() | GaussianCloud.t() | Enumerable.t()`)  -  a cloud,
    or an enumerable of `%Point{}`/`%Gaussian{}` values.
  * `path` (`Path.t()`)  -  destination path; parent directories must already
    exist.
  * `opts` (`keyword()`)  -  the options for `ExCodecs.Spatial.encode/2`, with
    `:format` required for enumerable input and defaulting to `:ply` for an
    already-built cloud. For streaming EXCP/GSPL writes, pass `:schema`
    (see codec docs).

## Returns

  * `:ok` after the complete payload is written.
  * any `:invalid_options`, `:unsupported_codec`, or `:invalid_data` error
    returned by encoding.
  * `{:error, %ExCodecs.Error{reason: :io_error, details: reason}}` when
    a file/POSIX error occurs (e.g. `:enoent`, `:eacces`, `:enospc`).

## Raises / exceptions

Normal path/POSIX failures are returned. Invalid path terms or non-path
binaries can raise `FunctionClauseError` or `ArgumentError` in the path/file
APIs. Encoding also has the enumerable, keyword, and malformed-struct
exceptions documented by `encode/2`.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> path = Path.join(System.tmp_dir!(), "ex_codecs_doc_80261.ply")
    iex> :ok = ExCodecs.Spatial.Stream.encode_to_file(PointCloud.new([Point.new(0, 0, 0)]), path, format: :ply)
    iex> {:ok, "ply" <> _} = File.read(path)
    iex> File.rm!(path)
    :ok

---

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