Packages

Phoenix component and LiveView hook for displaying 3D Gaussian splats in the browser, wrapping the PlayCanvas engine.

Current section

Files

Jump to
splat_viewer CHANGELOG.md
Raw

CHANGELOG.md

# Changelog
All notable changes to this project are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] - 2026-08-21
Initial release.
### Added
- `<.splat_viewer />` — a Phoenix component rendering a Gaussian splat, with
`src`, `camera`, `height`, `autorotate`, `background`, `dpr_cap`,
`pause_offscreen` and `interactive`.
- A LiveView hook, shipped in `priv/static/splat_viewer.js` and importable as
a node module from `deps/splat_viewer`.
- `SplatViewer.Camera` — a viewpoint, accepting either the struct
`splat_tools` produces or the JSON that comes back out of a database column.
### Fixed — first review pass
Found by running the hook against real PlayCanvas 2.21.4 in headless Chrome.
The package's own stub suite passed 17/17 throughout, and could not see any of
the first four — the stub was wrong in the same direction as the code, which
is its own lesson.
- **The device-pixel-ratio cap broke every HiDPI display.**
`AppBase#resizeCanvas` takes **CSS** pixels and sets `canvas.style`, applying
the device's own `maxPixelRatio` to get the backing store. Passing it
DPR-multiplied numbers set the canvas's CSS size to the backing-store number,
so the canvas overflowed a container with `overflow: hidden` and the visitor
saw a cropped corner of the scene, upscaled. The headline feature did the
opposite of what it claimed.
- **Two viewers on one page broke the first one.** `new pc.Entity(name)` falls
back to a module-global app set by the most recent `AppBase` constructor, so
two hooks mounting in the same tick gave the first one's entities to the
second one's app — and `addComponent` returned `null` against systems that
did not exist yet.
- **Teardown during load threw and leaked the canvas.** `this.app` was assigned
before `init()`, and `AppBase#destroy()` reads `graphicsDevice.canvas`
immediately — so the throw aborted the rest of `dispose()`, out of LiveView's
`destroyed()`.
- **Auto-framing was dead.** `gsplat.instance` is always `null` in unified
mode, which is the 2.x default, so the documented bounding-box fallback
silently framed the origin at radius one and every viewer without a stored
camera pointed at empty space.
- **`:background` accepted colours the renderer cannot read.** `#fff` rendered
blue and every named colour rendered black, because `Color#fromString` is
`parseInt` on the hex digits. Now hex only, with shorthand expanded.
- **One drag put a degenerate camera permanently at NaN**`Math.min`/`max`
do not filter a NaN. Guarded in the viewer, and `Camera` now refuses a pose
whose position equals its target.
- **The engine cache ignored the URL**, so a second viewer pointed elsewhere
silently received the first one's engine and reported itself ready.
- **`engine_path/0` was wired to nothing.** The config was documented in two
places and had no effect; only the JS global worked.
- **The status element was removed on first success**, so no later message —
the context-lost notice, a failed rebuild — could ever appear. It is hidden
now.
- **Restoring a WebGL context ignored `pause_offscreen`** and forced the state
to ready even from error.
- **`applySize` reallocated the drawing buffer on every call**, which during a
drag-resize is dozens of multi-megabyte reallocations a second.
- **`:height` was interpolated into the same style attribute as `:background`
without a guard**, so the one defence was on one of the two values.
- **`updated()` cleared the flag in-flight builds were watching**, so a stale
build carried on and overwrote the new one's observers. Replaced with a
build token.
- A browser with neither WebGL2 nor WebGPU got a null device and a `ready`
state, showing a blank box with no error.
### Design notes
- **The engine is not bundled.** PlayCanvas is about two megabytes; a Hex
package should neither ship that nor pin which copy an application uses. It
is imported at runtime from a configurable URL.
- **Options are validated in Elixir.** A bad camera or a bad colour raises at
render time with a message naming the problem. The same mistake caught in
the hook is a console warning nobody reads, on a page showing an empty box.
`:background` is interpolated into a style attribute, so it is checked
against a colour grammar rather than trusted.
- **The lifecycle is the substance.** Teardown, container-based resizing,
capped device pixel ratio, WebGL context loss, pausing off-screen, and
surviving LiveView re-renders — each is covered by the hook's own test
suite, which runs against a stub DOM under `mix test`.