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

ASCII and binary PLY for point clouds and Gaussian splats.

## Options

  * `:format` / `:ply_format`  -  wire encoding: `:ascii` (default),
    `:binary` / `:binary_le`, `:binary_be` (not Spatial's `format: :ply`)
  * `:comments`  -  header comments
  * `:as`  -  decode as `:auto` | `:point_cloud` | `:gaussian_cloud`
  * `:source`  -  for `stream_decode/2`: `:auto` | `:file` | `:binary`

Public functions: `encode/2`, `decode/2`, `stream_decode/2`.

# `ply_format`

```elixir
@type ply_format() :: :ascii | :binary | :binary_le | :binary_be
```

Wire encoding used for a PLY payload.

`:ascii` writes textual scalar values. `:binary_le` and `:binary_be` write
packed scalar values in little- and big-endian order, respectively.
`:binary` is an encoding-option shorthand for `:binary_le`; decoded headers
report the explicit endian atom.

For example, `encode(cloud, ply_format: :binary_be)` emits a
`format binary_big_endian 1.0` header.

# `decode`

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

Decodes a complete PLY 1.0 payload into a point or Gaussian cloud.

ASCII and binary little-/big-endian scalar vertex properties are supported:
`char`/`int8`, `uchar`/`uint8`, `short`/`int16`, `ushort`/`uint16`,
`int`/`int32`, `uint`/`uint32`, `float`/`float32`, and
`double`/`float64`. List properties and non-vertex payloads are unsupported.

Point decoding recognizes `x/y/z`, RGB or RGBA, and `nx/ny/nz`; all other
scalar properties become string-keyed point attributes. Gaussian decoding
interprets `f_dc_*`, opacity, scale, rotation, and ordered `f_rest_*`
properties. Header comments and the detected PLY format are retained in
cloud metadata.

## Arguments

  * `data` (`binary()`)  -  the complete PLY header and vertex body.
  * `opts` (`keyword()`)  -  `:as` may be `:auto` (default), `:point_cloud`, or
    `:gaussian_cloud`. `:auto` chooses Gaussian output when the property set
    contains `f_dc_0`, `scale_0`, or `rot_0`; otherwise it chooses points.
    The wire format is always read from the header.

## Returns

  * `{:ok, %PointCloud{}}` or `{:ok, %GaussianCloud{}}`, including metadata.
  * `{:error, %ExCodecs.Error{reason: :invalid_data, codec: :ply}}` for
    non-binary input, missing/invalid magic or format/header lines, missing
    vertex elements, invalid counts/properties, unsupported list/property
    types, invalid ASCII scalar tokens, token/property count mismatches,
    missing `x`/`y`/`z`, too few ASCII rows, or a short binary body.

## Raises / exceptions

The listed structural failures are returned. `Keyword.get/3` raises
`FunctionClauseError` when `opts` is not a proper keyword list. An unsupported
`:as` value raises `CaseClauseError`. Malformed binary row contents may also
raise (`KeyError`, `ArgumentError`, `FunctionClauseError`, or a bitstring match
exception) when scalar unpacking is not fully wrapped.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> cloud = PointCloud.new([Point.new(1.0, 0.0, 0.0, normal: {0.0, 0.0, 1.0})])
    iex> {:ok, ply} = ExCodecs.Spatial.Codec.PLY.encode(cloud, ply_format: :binary_le)
    iex> {:ok, %PointCloud{points: [point]}} = ExCodecs.Spatial.Codec.PLY.decode(ply)
    iex> point.normal
    {0.0, 0.0, 1.0}

# `encode`

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

Encodes a point cloud or Gaussian cloud as a PLY 1.0 payload.

Point vertices always contain `x`, `y`, and `z` as `float`. If any point has
color or a normal, the corresponding `uchar` RGB/RGBA or `float` normal
properties are emitted for every point; missing values are zero-filled
(alpha defaults to 255). The union of point attribute keys is emitted as
sorted `float` properties, with absent attributes set to `0.0`.

Gaussian vertices contain position, `f_dc_0..2`, opacity, scale, and
quaternion `rot_0..3` (`w, x, y, z`) as `float`. If spherical-harmonic
coefficients are present, flattened `f_rest_N` properties are appended and
shorter rows are zero-filled.

## Arguments

  * `data` (`PointCloud.t() | GaussianCloud.t()`)  -  a cloud containing valid
    `%Point{}` or `%Gaussian{}` structs.
  * `opts` (`keyword()`)  -  supported options:
    * `:ply_format`  -  preferred wire format: `:ascii` (default), `:binary`
      (little-endian), `:binary_le`, or `:binary_be`.
    * `:format`  -  legacy alias for `:ply_format` when calling this codec
      directly. `:ply_format` takes precedence.
    * `:comments`  -  enumerable of strings for PLY `comment` header lines;
      defaults to `cloud.metadata.comments`.

`:little` and `:big` are also normalized to little-/big-endian output.
Unrecognized wire-format values return `:invalid_options`.

## Returns

  * `{:ok, payload}` where `payload` is an ASCII or binary PLY `binary()`.
  * `{:error, %ExCodecs.Error{reason: :invalid_options, codec: :ply}}` when
    `:format` / `:ply_format` is not a supported wire encoding.
  * `{:error, %ExCodecs.Error{reason: :invalid_data, codec: :ply}}` when
    `data` is not a supported cloud or any exception occurs while deriving
    properties, building the header, or encoding rows.

## Raises / exceptions

The cloud clauses rescue encoding exceptions and return `:invalid_data`,
including invalid keyword options, malformed nested structs, non-string
comments, invalid scalar ranges/types, and bad attribute shapes. Unsupported
top-level data is also returned as `:invalid_data`; this function does not
intentionally raise.

## Examples

    iex> alias ExCodecs.Spatial.{Point, PointCloud}
    iex> cloud = PointCloud.new([Point.new(0, 1, 2, color: {255, 64, 0})])
    iex> {:ok, ply} = ExCodecs.Spatial.Codec.PLY.encode(cloud, ply_format: :ascii)
    iex> String.contains?(ply, "property uchar red")
    true

# `stream_decode`

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

Returns an enumerable over vertices from a PLY path or binary.

## Streaming behavior

  * `source: :file` (or `:auto` path detection): scans the header until
    `end_header`, then yields one vertex at a time  -  binary formats via
    fixed-stride `IO.binread/2`, ASCII via line reads. Peak memory is
    O(header + one vertex / read chunk), not the whole cloud.
  * `source: :binary`: still materializes through `decode/2`, then yields.

## Arguments

  * `source` (`Path.t() | binary()`)  -  a path or complete PLY payload.
  * `opts` (`keyword()`)  -  `:source` may be `:binary` (default), `:file`, or
    `:auto`. Prefer `:file` for paths and `:binary` for payloads; `:auto` is
    opt-in path sniffing. PLY also accepts `:as`.
    `:binary`. Auto mode treats a short path-like binary as a file only when
    it names a regular file; otherwise it is payload data. `:as` has the same
    meaning as in `decode/2`.

## Returns

An `Enumerable.t()` yielding `%Point{}` or `%Gaussian{}`. Decode failures,
including `reason: :invalid_data`, are represented by exactly one
`{:error, %ExCodecs.Error{}}` element. A selected path that cannot be read
yields one error with `reason: :io_error` and the file reason in `details`.

## Raises / exceptions

File and normal decode failures become stream elements. A non-binary
`source` raises `FunctionClauseError`. Keyword access raises
`FunctionClauseError` for invalid `opts`; unsupported `:source` values raise
`CaseClauseError`. Exceptions described by `decode/2` may occur while the
stream is initialized or enumerated.

## Examples

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

---

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