# 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.2] - 2026-09-05

### Fixed

- **`.sog` files render.** A `.sog` stores its gaussians as WebP images inside
  a zip, and the hook registered only `GSplatHandler` — the asset still fired
  "load", the state went "ready", and the box stayed empty with no error
  anywhere. `TextureHandler` is now registered alongside it. A `.ply` never
  touched textures, which is why the ply-based suite could not see this.
- **Rounded corners survive rendering in Firefox on macOS.** A large,
  continuously presenting WebGL canvas gets promoted to a compositor surface
  whose clips are axis-aligned rectangles, so an ancestor's `border-radius`
  is dropped (computed styles all correct, paint square). Measured against a
  six-variant probe: `clip-path` and `opacity` do not help; a trivial opaque
  `mask-image` forces the canvas back into normal compositing. Applied
  automatically when the container is rounded; reported upstream as
  [Mozilla bug 2068303](https://bugzilla.mozilla.org/show_bug.cgi?id=2068303).
- The created canvas inherits the container's `border-radius`; a first fix
  set it as a property one line before a wholesale `cssText` assignment wiped
  it.

### Added

- **`camera_mode="look"`** — first-person interaction: dragging turns the
  camera in place (the pointer leads the view) and the wheel walks forward
  and back. Orbit stays the default; look mode is for standing inside a
  scanned room, where orbiting around a close pivot pans the view opposite
  to the drag.
- **`load="click"`** — nothing is fetched (not even the engine) until the
  visitor presses a button; `load_label` sets its text. Engine plus scan is
  easily ten megabytes of somebody's mobile data.
- **`autorotate_speed`** in degrees per second (default 12). An interior
  wants 4–6; 12 sweeps through a wall within seconds.

### Documented

- SOG compression stated per its spec: typically 15–20× and lossy by
  design, not the 45× previously claimed.

## [0.1.1] - 2026-08-21

### Fixed

- **WebGPU is no longer requested without the shader compilers it needs.**
  PlayCanvas's WebGPU backend requires `glslangUrl` and `twgslUrl` — two
  assets this package does not ship and cannot guess the location of. Asking
  for it without them made the engine log a `TypeError` on **every mount**,
  in every browser with a partial WebGPU stack, before quietly falling back
  to WebGL2. It worked, and it was noise in someone else's console on a page
  they did not write. WebGL2 is now the default; supply `:glslang_url` and
  `:twgsl_url` to opt back in.

### Documented

- `app.start()` is deliberately never called — it would run PlayCanvas's own
  tick loop alongside this one and render every frame twice. The consequence,
  which was not written down anywhere, is that the engine's `framerender`
  event never fires, so an LOD or octree splat would never stream. A plain
  `.sog` is unaffected.

## [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`.
