# Spatial Codecs: Point Clouds and Gaussian Splats

```elixir
# Use this install to work with the source code
# Mix.install(
#   [
#     {:ex_codecs, path: Path.join(__DIR__, "..")}, 
#     {:rustler, "~> 0.36"}
#   ],
#   config:  [rustler_precompiled: [force_build: [ex_codecs: true]]]
# )

Mix.install([
  {:ex_codecs, "~> 0.2.3"}
])
```

## Series

| #   | Livebook                                                       |
| --- | -------------------------------------------------------------- |
| 01  | [Introduction](01_introduction.livemd)                         |
| 02  | [Compression Fundamentals](02_compression_fundamentals.livemd) |
| 03  | [Codec Comparison](03_codec_comparison.livemd)                 |
| 04  | [Building Storage Systems](04_building_storage_systems.livemd) |
| 05  | [Zarr-Style Workloads](05_zarr_style_workloads.livemd)         |
| 06  | **Spatial Codecs** (you are here)                              |

## One Framework, Specialized Category APIs

Compression codecs and spatial formats share discovery metadata and
`%ExCodecs.Error{}` results. Their operation APIs differ because compression
maps binary to binary, while spatial codecs map domain structs to formats.

```elixir
ExCodecs.available_codecs(:spatial)
```

```elixir
for format <- ExCodecs.Spatial.available_formats() do
  {:ok, info} = ExCodecs.codec_info(format)

  %{
    format: info.name,
    interface: info.interface,
    configurable?: info.configurable?,
    version: info.version
  }
end
```

## Build a Point Cloud

`Point.new/4` stores coordinates as floats and normalizes attribute keys to
strings. `PointCloud.new/2` computes axis-aligned bounds by default.

```elixir
alias ExCodecs.Spatial.{Gaussian, GaussianCloud, Point, PointCloud}

points = [
  Point.new(0, 0, 0,
    color: {255, 32, 32},
    normal: {0.0, 0.0, 1.0},
    attributes: %{intensity: 0.25}
  ),
  Point.new(1, 0, 0,
    color: {32, 255, 32},
    normal: {0.0, 0.0, 1.0},
    attributes: %{intensity: 0.5}
  ),
  Point.new(0, 1, 0,
    color: {32, 32, 255},
    normal: {0.0, 0.0, 1.0},
    attributes: %{intensity: 0.75}
  )
]

cloud = PointCloud.new(points)

%{
  points: PointCloud.size(cloud),
  bounds: cloud.bounds,
  attribute_keys: cloud.points |> hd() |> Map.fetch!(:attributes) |> Map.keys()
}
```

## PLY Interchange

PLY supports ASCII and binary little-/big-endian representations. The default
is binary little-endian.

```elixir
{:ok, ply_ascii} =
  ExCodecs.Spatial.encode(cloud,
    format: :ply,
    ply_format: :ascii,
    comments: ["created by the ExCodecs spatial Livebook"]
  )

ply_ascii
|> String.split("\n")
|> Enum.take(14)
|> Enum.join("\n")
```

```elixir
{:ok, ply_binary} = ExCodecs.Spatial.encode(cloud, format: :ply)
{:ok, decoded_ply} = ExCodecs.Spatial.decode(ply_binary, format: :ply)

%{
  encoded_bytes: byte_size(ply_binary),
  decoded_points: PointCloud.size(decoded_ply),
  first_point: hd(decoded_ply.points)
}
```

## Compact Point-Cloud Binary

`:spatial_binary` uses the versioned ExCodecs point format (`EXCP`). It is
intended for compact ExCodecs-to-ExCodecs interchange.

```elixir
{:ok, excp} = ExCodecs.Spatial.encode(cloud, format: :spatial_binary)
{:ok, decoded_excp} = ExCodecs.Spatial.decode(excp, format: :spatial_binary)

%{
  magic: binary_part(excp, 0, 4),
  encoded_bytes: byte_size(excp),
  decoded_points: PointCloud.size(decoded_excp)
}
```

## Spatial Encoding Plus Compression

Spatial encoding chooses a geometry format. Compression is a separate,
composable binary step.

```elixir
{:ok, compressed_excp} = ExCodecs.encode(:zstd, excp, level: 7)
{:ok, restored_excp} = ExCodecs.decode(:zstd, compressed_excp)
{:ok, restored_cloud} =
  ExCodecs.Spatial.decode(restored_excp, format: :spatial_binary)

%{
  excp_bytes: byte_size(excp),
  compressed_bytes: byte_size(compressed_excp),
  round_trip_points: PointCloud.size(restored_cloud)
}
```

## Gaussian Splats

`Gaussian` uses scalar-first quaternions (`{w, x, y, z}`). `:gsplat` is the
versioned ExCodecs Gaussian format (`GSPL`).

```elixir
gaussian_cloud =
  GaussianCloud.new([
    Gaussian.new({0, 0, 0},
      scale: {0.1, 0.2, 0.1},
      opacity: 0.9,
      color: {1.0, 0.2, 0.1}
    ),
    Gaussian.new({1, 0.5, -0.25},
      rotation: {0.9239, 0.0, 0.3827, 0.0},
      scale: {0.25, 0.1, 0.15},
      opacity: 0.65,
      color: {0.1, 0.4, 1.0}
    )
  ])

{:ok, gspl} = ExCodecs.Spatial.encode(gaussian_cloud, format: :gsplat)
{:ok, decoded_gaussians} = ExCodecs.Spatial.decode(gspl, format: :gsplat)

%{
  magic: binary_part(gspl, 0, 4),
  encoded_bytes: byte_size(gspl),
  decoded_gaussians: GaussianCloud.size(decoded_gaussians)
}
```

## Enumerable Helpers

`stream_decode/2` with `source: :file` reads one record/vertex at a time
(bounded memory; mmap + Rust unpack when available). `stream_encode/2` still
collects the enumerable, then encodes once — use
`ExCodecs.Spatial.Stream.encode_to_file/3` with an explicit `:schema` for
EXCP/GSPL incremental writes.

```elixir
{:ok, streamed_payload} =
  ExCodecs.Spatial.stream_encode(points, format: :spatial_binary)

streamed_points =
  streamed_payload
  |> ExCodecs.Spatial.stream_decode(format: :spatial_binary, source: :binary)
  |> Enum.to_list()

length(streamed_points)
```

```elixir
path = Path.join(System.tmp_dir!(), "ex_codecs_spatial_#{System.unique_integer([:positive])}.excp")

:ok =
  ExCodecs.Spatial.Stream.encode_to_file(points, path,
    format: :spatial_binary,
    schema: []
  )

file_points =
  path
  |> ExCodecs.Spatial.stream_decode(format: :spatial_binary, source: :file)
  |> Enum.to_list()

File.rm(path)
length(file_points)
```

## Category-Safe Dispatch

The shared catalog does not overload the binary API with struct inputs.
Choosing a spatial catalog entry through `ExCodecs.encode/3` returns guidance
to use the spatial category API.

```elixir
{:error, error} = ExCodecs.encode(:ply, ply_binary)

%{reason: error.reason, message: error.message}
```

## Further Reading

* [Understanding Spatial Codecs](https://hexdocs.pm/ex_codecs/understanding_spatial_codecs.html) — schema and format trade-offs
* [Spatial Wire Formats](https://hexdocs.pm/ex_codecs/spatial_formats.html) — frozen EXCP / GSPL / PLY layouts
* Add Zstd after spatial encoding when storage or transfer size matters.

## Navigation

**Previous:** [Zarr-Style Workloads](05_zarr_style_workloads.livemd)
