mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
147 lines
8.4 KiB
Markdown
147 lines
8.4 KiB
Markdown
# Background editor: current contract
|
||
|
||
Status: implemented in v1.60.0. This document is the source of
|
||
truth for the decorative layer. `BACKDROP.md`, `LIVE-TEXT.md` and
|
||
`FURNITURE.md` describe the specialised parts.
|
||
|
||
## Purpose and invariants
|
||
|
||
The Background editor is a visual annotation layer. Its shapes, furniture,
|
||
text and plan image never participate in rooms, wall geometry, light routing,
|
||
device state or Home Assistant actions.
|
||
|
||
| Invariant | Contract |
|
||
|---|---|
|
||
| Coordinates | Axis-aligned positions and every box dimension are quantised to the space grid. A rotated resize keeps its opposite world-space corner fixed and quantises dimensions; its derived unrotated storage origin is not post-snapped, because that would translate the entire object. `Shift` never disables dimension snapping. |
|
||
| Physical values | Stroke and text sizes are stored in centimetres; the UI shows cm/in for small values and m/ft for object dimensions. |
|
||
| Undo | Decor and backdrop use the same named 50-command stack as plan geometry. |
|
||
| Cancel | `Esc` restores the state at the start of an active drag/draw/resize/rotate gesture. A completed gesture is reverted with Undo. |
|
||
| Selection | One click selects; drag moves; the selected object has one common transform frame; double click opens all editable properties. |
|
||
| Scale | Corner drag preserves aspect ratio. Hold `Shift` for independent axes. |
|
||
| Rotation | 5° steps by default; `Shift` gives free rotation. Lines use endpoint handles instead of a rotation handle. |
|
||
| Magnet targets | Only other decor objects and room contours: corners, edge centres, centres and edges. The image, devices and openings are excluded. |
|
||
| Context emphasis | Decor and its editing chrome stay fully opaque. Rooms, labels, devices, openings, solid/thick walls and dashed virtual walls are contextual only and render at 35% opacity. |
|
||
| Compatibility | Legacy `width`, text `size/scale` and `plan_scale` remain readable. New writes use `width_cm`, `size_cm` and `plan_scale_x/y`. |
|
||
|
||
## Tools
|
||
|
||
| Tool | Pointer action | Live feedback | Properties |
|
||
|---|---|---|---|
|
||
| Select | Select/move any decor object; corner handles resize; upper handle rotates | common selection frame and standard move/resize/rotate cursors | double click opens geometry, angle, contour/fill colour and opacity; text also exposes its content |
|
||
| Plan backdrop | Move the image by its body; resize/rotate by the frame | size badge; 5° rotation step | double click opens width, height and angle |
|
||
| Line | Drag between two grid points | length, angle, magnetic alignment guides | endpoint handles; length, angle, stroke colour/opacity/thickness; Solid or Dashed style |
|
||
| Rectangle | Drag a diagonal; `Shift` makes a square | width × height and area | size, angle, contour, optional independent fill colour/opacity |
|
||
| Oval | Drag its bounding box; `Shift` makes a circle | `R` for a circle, `Rx × Ry` for an oval | bounding size, angle, contour and optional fill |
|
||
| Text | Click to open the text form | the saved label is selected immediately | content, HA variables, colour/opacity, physical size and angle |
|
||
| Furniture | Pick a symbol, then click its centre | wall magnet unless `Shift` is held | size, angle and contour style; the common frame preserves ratio unless `Shift` is held |
|
||
| Erase | Click a decor object; text uses its whole logical bounding box, including spaces between glyphs | confirmation dialog, then atomic removal of the whole object | A miss changes nothing; Undo restores a removed object |
|
||
|
||
The editor always opens on **Select**. If the space has an image, the Plan
|
||
backdrop tool appears next to Select but is never armed implicitly.
|
||
|
||
## Plan image behaviour
|
||
|
||
The plan image is interactive only while **Plan backdrop** is selected.
|
||
|
||
| Context | Image opacity | Frame/body interaction |
|
||
|---|---:|---|
|
||
| View, Plan editor, Device editor | 1.0 | none |
|
||
| Background editor, Select/drawing/furniture/erase | 0.5 | none; the stage remains available for pan/draw |
|
||
| Background editor, Plan backdrop | 1.0 | move, proportional/independent resize, rotate |
|
||
|
||
Stored transform fields:
|
||
|
||
```text
|
||
plan_x, plan_y top-left offset, normalised to the square canvas
|
||
plan_scale_x, plan_scale_y independent positive multipliers
|
||
plan_angle degrees, normalised to -180..180
|
||
```
|
||
|
||
`plan_scale` is the legacy uniform fallback. The maintenance command converts
|
||
it losslessly to both axis fields. Reset removes every transform field.
|
||
|
||
## Style and units
|
||
|
||
New decor writes use:
|
||
|
||
```text
|
||
color, opacity, width_cm
|
||
fill, fill_color, fill_opacity rectangles and ovals only
|
||
size_cm text only
|
||
line_style: dashed dashed lines only; absence means Solid
|
||
```
|
||
|
||
`width_cm` and text `size_cm` are independent physical styles. Resizing a
|
||
rectangle or sofa does not make its outline thicker. A legacy object with render-unit `width` stays
|
||
pixel-identical until edited or explicitly optimised; conversion is
|
||
`width_cm = width / GRID_PITCH × cell_cm`.
|
||
|
||
The active drawing tool's values are session defaults for newly drawn objects.
|
||
They live in the shared context tray over the stage rather than changing the
|
||
height of the permanent editor toolbar. Colour is a compact swatch; its popover
|
||
owns both the native colour field and opacity. Double-click properties edit an
|
||
existing object without creating a second style model. Select actions and the
|
||
furniture palette use variants of the same overlay host, so opening them never
|
||
refits the plan.
|
||
|
||
Line style is intentionally absent from the drawing toolbar. Every new and
|
||
legacy line is Solid by default. Double-click a line with **Select** to switch
|
||
that individual object between **Solid** and **Dashed**; switching back removes
|
||
the optional `line_style` key instead of persisting a redundant default.
|
||
|
||
## Interaction state machine
|
||
|
||
```text
|
||
idle → draft/move/scale/rotate → release → named history command → debounced save
|
||
↘ Esc → restore transaction start → idle
|
||
```
|
||
|
||
Only the active tool owns pointer events. Drawing tools can therefore start a
|
||
new line or figure exactly on top of an existing object. Select and Erase are
|
||
the general tools that target existing decor. The deliberate exception is
|
||
Text: clicking an existing text label with Text opens that label's editor;
|
||
clicking any non-text shape still starts a new label. The backdrop body belongs
|
||
only to the Plan backdrop tool.
|
||
|
||
`Ctrl/Cmd+Z` first cancels an unfinished draft or live gesture, then walks the
|
||
shared history. `Ctrl+Shift+Z` and `Ctrl+Y` obey the same transaction boundary:
|
||
the first invocation cancels a live gesture, the next redoes. Native text-field
|
||
history wins while focus is inside an input.
|
||
|
||
## Code ownership
|
||
|
||
| Concern | File |
|
||
|---|---|
|
||
| persisted decor types and future custom-image transform contract | `src/editors/decor/types.ts` |
|
||
| physical style conversion, oriented boxes, resize and snapping | `src/editors/decor/geometry.ts` |
|
||
| shared colour/opacity field | `src/hp-color-opacity.ts` |
|
||
| orchestration, dialogs and SVG transform frames | `src/houseplan-card.ts` |
|
||
| static/full-card backdrop rendering and content bounds | `src/space-render.ts`, `src/space-geometry.ts` |
|
||
| accepted persisted ranges | `custom_components/houseplan/validation.py` |
|
||
| explicit legacy conversion | `src/plan-optimizer.ts` |
|
||
|
||
The current root card still owns orchestration. Future extraction should move
|
||
it into `src/editors/decor/decor-editor.ts` without changing the typed model or
|
||
geometry helpers.
|
||
|
||
## Deferred custom images
|
||
|
||
User-provided decorative images are not exposed yet. The typed
|
||
`DecorImageTransform` contract already matches the common selection controller
|
||
(`x/y/w/h/angle/opacity`). A future image kind must additionally define safe
|
||
upload/reference lifecycle, quotas, copy-on-write and deletion semantics before
|
||
it is added to `DecorShape`; it must not reuse the plan backdrop file lifecycle
|
||
implicitly.
|
||
|
||
## Edge cases
|
||
|
||
- Degenerate drafts below half a cell are discarded and do not enter history.
|
||
- Unknown furniture symbols remain stored but render nothing in an older card.
|
||
- A rotated box contributes all four rotated corners to content bounds.
|
||
- A rotated backdrop is hit-tested in its own local coordinate system.
|
||
- Changing `cell_cm` changes the rendered width/font size of canonical physical
|
||
styles, as expected: it changes the scale of the whole space. Legacy
|
||
render-unit strokes/text retain their old pixels until migration.
|
||
- External config revisions clear the session history; undo never applies a
|
||
command to a different server revision.
|