mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
A picked raster is now classified from its HEADER BYTES ONLY before anything heavy happens: src/backdrop-probe.ts parses PNG IHDR (+colour type/tRNS for alpha), JPEG SOF and WebP VP8/VP8L/VP8X at fixed offsets, never using a file field as an allocation size; hostile or truncated headers collapse to 'unknown', which warns without numbers instead of passing silently. The thresholds live in that module as the single calibration point (WARN_DECODED_BYTES 128 MiB ≈ 32 MP, HARD_DIMENSION 16384 — the browser canvas cap, DOWNSCALE_TARGET_PX 4096), derived from the desktop-Chromium matrix now committed as demo/benchmark_backdrop_decode.mjs with a conservative tablet margin documented in the spec. The shared pick flow (src/backdrop-pick.ts) feeds BOTH lazy runtimes — the editor space dialog and the onboarding first-space dialog — so the guard cannot drift between them, and nothing of it enters the eager View graph. Warn shows the real numbers and three actions; the reduced copy decodes EXIF-aware, keeps aspect and alpha (PNG stays PNG, opaque becomes JPEG q0.9) and flows through the ordinary planFile → upload path. Hard has two phases with one outcome: beyond 16384 px only Cancel; a failed or timed-out (10 s) reduce closes with a toast, clean staging and NO silent fallback to the original the user just declined. SVG never reaches the probe. The safe path swaps the manual byte-loop base64 for FileReader — half the JS-heap peak on every upload, byte-identical output (parity asserted in the smoke). Proofs: header-table units incl. a fuzz set of hostile headers and ±1 threshold bounds; smoke_backdrop_guard on the real bundle — zero decode calls before the choice, byte parity of keep-original, a real 6200 px reduce to 4096 for both alpha and opaque branches, cancel-only hard dialog, both phase-2 failures (reject and hang under the test-only timeout override), re-pick after refusal, SVG bypass; four registry mutants (probe-always-safe, alpha-dropped, hard-demoted, phase-2 silent fallback). Spec anchor corrected alongside: the server plan limit is 8 MB (MAX_PLAN_BYTES), attachments are the 50 MB path — an 8 MB JPEG is easily 80-160 MP decoded, so the client-side guard stays the primary defence. Issue: #39 User-Visible: yes
94 lines
3.8 KiB
Markdown
94 lines
3.8 KiB
Markdown
# Plan image backdrop
|
|
|
|
Status: current in v1.60.0. The complete Background editor contract is in
|
|
`DECOR-EDITOR.md`.
|
|
|
|
## Placement model
|
|
|
|
The source image is first fitted proportionally into the square plan canvas by
|
|
`fitInSquare(plan_aspect, NORM_W)`. The optional transform is then applied:
|
|
|
|
| Field | Meaning | Default |
|
|
|---|---|---:|
|
|
| `plan_x`, `plan_y` | top-left offset from the fitted rectangle, normalised by `NORM_W` | `0` |
|
|
| `plan_scale_x`, `plan_scale_y` | independent width/height multipliers | `1` |
|
|
| `plan_angle` | rotation around the transformed rectangle centre | `0°` |
|
|
| `plan_scale` | legacy uniform fallback for both axes | `1` |
|
|
|
|
`planRect()` is the single reader used by the full card, static card and content
|
|
bounds. New writes remove `plan_scale`; **Оптимизировать планы** converts it
|
|
losslessly to both axis fields.
|
|
|
|
## Editor behaviour
|
|
|
|
The Background editor opens on **Select**, never on the image tool. The image
|
|
is interactive only under **Plan backdrop**:
|
|
|
|
- body drag moves it;
|
|
- four corner handles preserve ratio by default;
|
|
- `Shift` allows independent axes;
|
|
- the upper handle rotates in 5° steps, or freely with `Shift`;
|
|
- double click opens numeric width, height and angle;
|
|
- `Esc` restores the transform at pointer-down;
|
|
- release creates one named command in the shared 50-step history;
|
|
- Reset removes all six transform fields and is itself undoable.
|
|
|
|
In the Background editor the image is at opacity `0.5` under every other tool
|
|
and cannot receive pointer events from the controller. Under Plan backdrop it
|
|
is opaque. View and the other editors always show it at opacity `1`.
|
|
|
|
Move and resulting top-left coordinates are grid-bound. Width and height are
|
|
quantised when resized or entered numerically. There is no positional modifier
|
|
that bypasses the grid.
|
|
|
|
## Rendering and content
|
|
|
|
The image is purely decorative and never changes rooms, walls, openings,
|
|
devices, Glow or sun geometry. It is still a content item for fit/pan bounds.
|
|
For a rotated image all four rotated corners contribute to those bounds.
|
|
|
|
Layer order, top to bottom:
|
|
|
|
```text
|
|
devices and room labels
|
|
opening symbols / positive and zero-thickness walls / late room-hover outline
|
|
sun rays
|
|
live Glow pools
|
|
decor
|
|
room hover fill / Glow-base rooms and tunnels / data room fills and tunnels
|
|
plan image
|
|
room-shaped paper
|
|
scene background
|
|
```
|
|
|
|
The paper remains room-shaped; an image without rooms does not create opaque
|
|
paper behind itself.
|
|
|
|
## Large images (#39)
|
|
|
|
Before any decode the picked raster is classified from its header bytes only
|
|
(`src/backdrop-probe.ts`): PNG IHDR (+colour type/tRNS for alpha), JPEG SOF,
|
|
WebP VP8/VP8L/VP8X. Thresholds live in that module as the single calibration
|
|
point: a decoded size above `WARN_DECODED_BYTES` (128 MiB ≈ 32 MP) opens a
|
|
warning dialog with the real numbers and offers an aspect-preserving reduced
|
|
copy (longest side `DOWNSCALE_TARGET_PX` = 4096; PNG with alpha stays PNG,
|
|
opaque images become JPEG q0.9, EXIF orientation honoured); a side beyond
|
|
`HARD_DIMENSION` (16384, the browser canvas cap) offers only Cancel. A failed
|
|
or timed-out decode of the reduced copy shows a toast and leaves staging clean
|
|
— the original the user declined is never uploaded silently. Unreadable
|
|
headers behave like a warning without numbers. SVG is never rasterised and
|
|
skips the probe entirely. Calibration matrix and rationale:
|
|
`docs/specs/039-large-backdrops.md`, rerun via
|
|
`demo/benchmark_backdrop_decode.mjs`.
|
|
|
|
## Ownership
|
|
|
|
| Concern | File |
|
|
|---|---|
|
|
| fitted/transformed rectangle and rotated bounds | `src/space-geometry.ts` |
|
|
| gestures, frame, numeric dialog, history/reset | `src/houseplan-card.ts` |
|
|
| full-card SVG image | `src/houseplan-card.ts` |
|
|
| static-card SVG image | `src/space-render.ts` |
|
|
| schema ranges | `custom_components/houseplan/validation.py` |
|
|
| legacy conversion | `src/plan-optimizer.ts` |
|