mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 06:59:46 +00:00
337 lines
17 KiB
Markdown
337 lines
17 KiB
Markdown
# #373 — Tight house framing for `houseplan-space-card`
|
|
|
|
Issue: [#373](https://github.com/Matysh/houseplan-card/issues/373)
|
|
|
|
## Scenario
|
|
|
|
The **home administrator** embeds `custom:houseplan-space-card` in a compact
|
|
Home Assistant dashboard for household members, a wall tablet or a phone. The
|
|
stored space also contains a large backdrop or auxiliary objects outside the
|
|
building, but this card is meant to show the house itself as large as possible.
|
|
|
|
The administrator selects a tighter frame for this one card instance. View and
|
|
kiosk consumers then see the complete structural plan without the ordinary
|
|
outer breathing room; no editor is involved.
|
|
|
|
## What the person sees before and after
|
|
|
|
Before, the static card keeps a 5% outer frame and lets non-house content widen
|
|
the view; after opting in, the complete house geometry touches the available
|
|
frame without intentional padding, while backdrop, decor, labels and devices
|
|
outside the house no longer make the house smaller.
|
|
|
|
## Problem
|
|
|
|
`renderSpaceStatic()` currently calls `spaceFrame(space, placed, 0.05)` (or the
|
|
per-edge #372 variant for `title: ""`). That frame is deliberately the broad
|
|
content frame from `docs/CANVAS.md`: rooms, the transformed backdrop and visible
|
|
placed objects can all vote, and the result is padded by 5% of its longer side.
|
|
This is correct for the existing default and for the full interactive card, but
|
|
it wastes scarce dashboard area when the embedded card is intended to show the
|
|
building rather than every auxiliary object.
|
|
|
|
The reporter chose the non-cropping alternative in the latest issue comment:
|
|
remove the 5% padding, keep all house geometry visible, and derive the fit from
|
|
room/house geometry instead of backdrop or decorative objects. A CSS-`cover`
|
|
mode that can crop the house is explicitly not requested.
|
|
|
|
## Scope
|
|
|
|
- only `custom:houseplan-space-card`;
|
|
- one opt-in Lovelace card setting, `fit: house`;
|
|
- visual-editor choice between the existing frame and the tight house frame;
|
|
- a zero-intentional-padding structural frame containing every rendered room,
|
|
room wall, independent wall/draft, column, zero-thickness wall and visible
|
|
door/window/gate symbol;
|
|
- exclusion of backdrop image, decor, room labels, device markers and their
|
|
badges from the `house` frame vote;
|
|
- safe fallback to the existing frame when the space has no valid structural
|
|
geometry;
|
|
- consistent geometry, themes and touch/desktop View behaviour;
|
|
- en/ru/de/fr UI strings, RU/EN user guide, architecture/canvas contract,
|
|
changelogs and targeted tests.
|
|
|
|
## Non-scope
|
|
|
|
- any change to the full `custom:houseplan-card`, its Fit all action, View,
|
|
kiosk, editors, zoom, pan or outlier hint;
|
|
- CSS `cover`, aspect-ratio distortion or any mode allowed to crop structural
|
|
house geometry;
|
|
- hiding, deleting or moving backdrop, decor, labels, markers or devices;
|
|
- changing marker size, room-label position, Glow, sunlight, wall geometry,
|
|
opening state or HA actions;
|
|
- changing the stored House Plan model, backend schema, `space.view_box`,
|
|
config/layout revisions, exports or imports;
|
|
- arbitrary user-configurable padding values;
|
|
- changing the #372 meaning of `title: ""` in the default frame;
|
|
- automatically enabling the new mode for existing cards.
|
|
|
|
## Behaviour contract
|
|
|
|
### Public card configuration
|
|
|
|
```yaml
|
|
type: custom:houseplan-space-card
|
|
space: ground_floor
|
|
fit: house
|
|
```
|
|
|
|
1. `fit` accepts `content | house`.
|
|
2. Missing, empty or unknown `fit` resolves fail-safe to `content`.
|
|
3. `content` is the exact current behaviour: the current content voters,
|
|
main-mass/outlier policy and 5% padding remain unchanged. Existing YAML
|
|
without `fit` must produce the same frame.
|
|
4. `house` selects the new tight structural frame described below. It never
|
|
becomes the implicit default.
|
|
|
|
### Tight structural frame
|
|
|
|
5. The structural frame contains the union of all finite, sane visible
|
|
architectural extents for the selected space:
|
|
- room polygon/rectangle floors;
|
|
- the complete outer envelope of positive-thickness room walls;
|
|
- independent walls, saved wall drafts and columns;
|
|
- zero-thickness room and independent wall axes;
|
|
- the full visible state-independent envelope of door, window and gate
|
|
symbols, including wall jambs, leaf tips and swing arcs in either state.
|
|
An open passage contributes through its structural wall/tunnel geometry
|
|
and has no extra standalone symbol.
|
|
6. All structural items participate. The broad content-frame outlier vote may
|
|
not discard a detached room or wing in `house` mode: sane structural
|
|
geometry is part of the house and must stay visible.
|
|
7. The resolved structural union receives **zero intentional padding** on all
|
|
four sides. Bounds still include the actual rendered wall/symbol stroke
|
|
envelope, so “zero padding” may not clip a stroke by half its width.
|
|
8. Backdrop image, decor, room labels, device markers, value/LQI badges, pulse
|
|
shells and purely visual Glow/sun extents do not enlarge this frame. They
|
|
remain rendered in the same coordinate system and may be clipped when they
|
|
lie outside the structural house frame; this is the user-confirmed trade-off.
|
|
9. The SVG, marker layer, room-label layer, backdrop, day-cycle environment and
|
|
continuity overlay all consume the single resolved `viewBox`. Nothing is
|
|
independently translated or scaled after the fit is chosen.
|
|
10. If no valid structural item exists (for example an image-only or empty
|
|
space), `house` falls back atomically to the ordinary `content` frame. It
|
|
must not create an empty, zero-sized, NaN or infinite `viewBox`.
|
|
11. Structural computation is state-independent. Opening sensor changes,
|
|
device state updates, Glow animation and language/theme changes do not
|
|
resize the card or move the house inside it.
|
|
|
|
### Interaction with existing options
|
|
|
|
12. `title`, `show_button`, `light_pools`, state/value options and footer deep
|
|
links retain their existing semantics.
|
|
13. With `fit: content`, explicit `title: ""` keeps #372: only the top 5%
|
|
padding disappears, while side/bottom padding remains.
|
|
14. With `fit: house`, all four intentional frame paddings are already zero;
|
|
`title: ""` only removes the header and does not change the structural
|
|
`viewBox` further.
|
|
|
|
## UX and accessibility
|
|
|
|
- The Lovelace visual editor exposes one dropdown labelled **Framing** with:
|
|
- **All visible content** → `content`;
|
|
- **Tight to house geometry** → `house`.
|
|
- A missing legacy value is displayed as **All visible content**. A subsequent
|
|
visual-editor save may materialise `fit: content`; this is semantically
|
|
lossless.
|
|
- No crop/cover choice is shown.
|
|
- The schematic remains `pointer-events:none`, non-focusable and read-only.
|
|
The existing footer button remains the only interactive element.
|
|
- No new hover, gesture or animation is introduced. The frame is identical on
|
|
fine/coarse pointers, narrow/wide cards and light/dark themes.
|
|
|
|
## Data model, migration and compatibility
|
|
|
|
The field belongs only to the Lovelace card instance:
|
|
|
|
```ts
|
|
interface SpaceCardConfig {
|
|
fit?: 'content' | 'house';
|
|
}
|
|
```
|
|
|
|
- no House Plan server-config or layout field is added;
|
|
- no model/store/schema version or backend validation changes;
|
|
- no data migration and no write to Home Assistant storage;
|
|
- old cards and old YAML remain on the `content` path;
|
|
- current code normalises only at read/render and visual-editor boundaries;
|
|
- an older frontend ignores the unknown Lovelace key and renders its historical
|
|
content frame, without damaging House Plan data;
|
|
- rollback is therefore data-free.
|
|
|
|
## i18n
|
|
|
|
Add complete keys to `src/i18n/{en,ru,de,fr}.json` for:
|
|
|
|
- Framing;
|
|
- All visible content;
|
|
- Tight to house geometry.
|
|
|
|
The technical YAML literals `content` and `house` are not translated. Missing
|
|
translations may not leave a raw key or an unlabelled select option.
|
|
|
|
## Performance and bundle
|
|
|
|
- Frame resolution remains one bounded O(rooms + walls + openings + columns)
|
|
pass per structural render candidate.
|
|
- HA state-only ticks reuse/calculate the same structural fingerprint; they may
|
|
not introduce DOM measurement or `getBBox()` layout reads.
|
|
- No second hidden SVG, canvas raster scan, network request or dependency.
|
|
- Existing `fit: content` cost and output remain unchanged.
|
|
- Initial View bundle budget is not raised.
|
|
|
|
## Touch and themes
|
|
|
|
- `houseplan-space-card` is a View surface, so narrow phone and wall-tablet
|
|
rendering is release-blocking.
|
|
- The new mode is presentation-only and must not cause horizontal overflow,
|
|
clipped structural strokes or a zero-height card at representative 320 px
|
|
and 900 px widths.
|
|
- Pointer/tap behaviour is unchanged; the stage stays inert and the footer tap
|
|
target stays usable.
|
|
- Light/dark and day-cycle modes share exactly the same structural frame.
|
|
|
|
## Affected files and modules
|
|
|
|
- `src/space-card.ts` — public `fit` type/default and static-render projection;
|
|
- `src/space-editor.ts` — localised framing dropdown and legacy/default
|
|
projection;
|
|
- `src/space-render.ts` — select ordinary vs structural frame and keep one `vb`;
|
|
- a narrow pure helper in `src/space-geometry.ts`, `src/render/opening-symbol.ts`
|
|
or a dedicated static-frame module — finite structural items and opening
|
|
envelopes without DOM measurement;
|
|
- `src/i18n/{en,ru,de,fr}.json` — editor labels;
|
|
- `test/**` — pure frame/config/opening-envelope regressions;
|
|
- `demo/smoke_space_card.mjs` — real-card comparison and View/touch contract;
|
|
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`, `docs/ARCHITECTURE.md`,
|
|
`docs/CANVAS.md` — public and canonical contracts;
|
|
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md` — release bullet linked to #373.
|
|
|
|
## Acceptance criteria
|
|
|
|
- **AC1 — default compatibility:** missing/empty/unknown `fit` and explicit
|
|
`fit: content` produce the same numeric `viewBox`, stage aspect and #372
|
|
title interaction as the pre-change control fixture. **Evidence:** unit tests
|
|
plus `demo/smoke_space_card.mjs` comparing four configurations.
|
|
- **AC2 — tight structural frame:** on a fixture with an oversized transformed
|
|
backdrop, far decor, far marker and room label, `fit: house` excludes those
|
|
items from its bounds, removes all four intentional 5% pads and yields a
|
|
strictly tighter frame than `content`. **Evidence:** pure unit assertions and
|
|
browser smoke over the rendered SVG `viewBox`.
|
|
- **AC3 — no structural crop:** rooms, positive/zero walls, independent walls,
|
|
drafts, columns and door/window/gate envelopes in their extreme states are
|
|
all within the `house` frame including visible strokes. **Evidence:** pure
|
|
geometry table tests and a browser semantic-pixel/bounds witness that fails
|
|
when any structural edge is clipped.
|
|
- **AC4 — stable and safe fallback:** an empty space, image-only space,
|
|
collinear structural geometry and invalid/unknown `fit` always produce a
|
|
finite positive frame; image-only `house` equals the ordinary content frame.
|
|
Opening/device state ticks do not change it. **Evidence:** unit + browser
|
|
smoke with state substitution.
|
|
- **AC5 — editor/i18n contract:** the GUI displays the default and house choices
|
|
in en/ru/de/fr, saves exact literals, reloads them, and never offers cover.
|
|
**Evidence:** unit/source contract plus browser editor smoke or review of an
|
|
existing faithful `ha-form` harness.
|
|
- **AC6 — View/touch parity:** tight mode renders without horizontal overflow
|
|
at 320 px and 900 px, in light/dark/day-cycle, keeps the stage inert and keeps
|
|
the footer action unchanged. **Evidence:** targeted browser smoke and code
|
|
review.
|
|
- **AC7 — performance:** state-only rerenders do not perform DOM geometry reads
|
|
and the structural pass stays bounded; bundle budget remains green.
|
|
**Evidence:** code review, `npm run bundle:budget` and existing static-card
|
|
performance profile before beta.
|
|
- **AC8 — documentation and release:** RU/EN guide documents `fit`, its default,
|
|
the intentional auxiliary-object crop and its interaction with `title: ""`;
|
|
architecture/canvas docs describe the second static-only frame; both
|
|
changelogs link #373. **Evidence:** docs/process gates and code review.
|
|
|
|
## Automated test plan
|
|
|
|
1. Pure unit: validate the `fit` resolver (`undefined`, `content`, `house`,
|
|
unknown) and prove the default branch retains current padding.
|
|
2. Pure unit: build a synthetic structural scene containing a polygon room,
|
|
thick wall envelope, zero wall, independent wall/draft, column and every
|
|
opening type; compare exact finite structural bounds and zero padding.
|
|
3. Pure unit: substitute closed/open/extreme opening amounts and prove the
|
|
state-independent envelope and resulting frame do not change.
|
|
4. Pure unit: add far backdrop/decor/marker/label items; prove ordinary content
|
|
widens while house bounds do not. Add a detached valid room and prove it is
|
|
retained rather than outlier-rejected.
|
|
5. Pure unit: empty/image-only/collinear/invalid inputs fall back to a finite
|
|
positive ordinary frame.
|
|
6. Extend `demo/smoke_space_card.mjs` with control, explicit `content`, `house`
|
|
and `house + title:""` cards. Assert numeric frames, no clipped structural
|
|
edge, common overlay coordinates, unchanged footer deep link and inert stage.
|
|
7. Repeat tight mode at narrow light and wide dark widths and change a door
|
|
sensor/device state; the viewBox remains stable and no overflow appears.
|
|
8. In the implementation cycle run `npm run typecheck`, `npm test`,
|
|
`npm run build`, `npm run bundle:sync`, `npm run bundle:budget`,
|
|
`node scripts/no-new-any.mjs --base origin/dev --head HEAD`,
|
|
`node scripts/check-docs.mjs`, smoke selection and the named target smoke.
|
|
Run full golden/smoke/performance and exact-SHA Linux Validate before beta,
|
|
following the process.
|
|
|
|
## Risks
|
|
|
|
- **Zero padding clips the outer half of a wall or opening stroke.** Mitigated
|
|
by structural visible-envelope math and AC3, not by reintroducing arbitrary
|
|
padding.
|
|
- **Backdrop/decor still votes through `spaceFrame()`.** Mitigated by a separate
|
|
explicit structural-item path and AC2; the ordinary helper remains untouched.
|
|
- **Stateful door movement resizes the card.** Mitigated by one conservative
|
|
state-independent opening envelope and AC3/AC4.
|
|
- **A detached building is mistaken for an outlier.** Mitigated by using all
|
|
sane structural items for the house union and the detached-room test.
|
|
- **The default changes accidentally.** Mitigated by an explicit resolver whose
|
|
fallback is `content`, numeric compatibility tests and no implicit opt-in.
|
|
- **Auxiliary content is intentionally clipped but undocumented.** Mitigated by
|
|
the GUI wording, guide warning and AC8.
|
|
- **Static and full cards drift.** The new path is static-only by contract; all
|
|
existing shared geometry primitives stay canonical, while the full card's
|
|
content frame remains unchanged and covered by existing canvas tests.
|
|
- **Docs screenshot fingerprint changes after `src/**`.** The canonical Docs
|
|
screenshots workflow/acceptance remains required by the repository process.
|
|
|
|
## Rollback
|
|
|
|
Remove the `fit` projection/editor option and always call the existing content
|
|
frame path. Existing cards without the option are already on that path; cards
|
|
with `fit: house` become ordinary content-framed cards when read by the rolled
|
|
back frontend. No House Plan config/layout data, migration or cleanup is needed.
|
|
|
|
## Release artifacts
|
|
|
|
- `docs/CHANGELOG.md` and `docs/CHANGELOG.ru.md`: opt-in tight house frame with
|
|
a link to #373;
|
|
- `docs/USER-GUIDE.md` and `docs/USER-GUIDE.ru.md`: YAML/table/default,
|
|
auxiliary crop and `title: ""` interaction;
|
|
- `docs/ARCHITECTURE.md` and `docs/CANVAS.md`: static-only structural frame and
|
|
its relation to the canonical content frame;
|
|
- targeted `demo/smoke_space_card.mjs` result with numeric and semantic visual
|
|
evidence for standard/tight, narrow/wide and light/dark;
|
|
- full existing golden verify to prove no default-path visual regressions; a
|
|
new baseline is added only if targeted semantic evidence cannot make AC2/AC3
|
|
unambiguous, and may be accepted only from the reviewed complete Linux
|
|
artifact;
|
|
- canonical Docs screenshots artifact accepted with
|
|
`npm run docs:accept -- --reviewed --from=<artifact>` because `src/**` changes;
|
|
- no backend, migration or security artifact; initial bundle budget and the
|
|
static-card performance profile remain mandatory before beta.
|
|
|
|
## Assumptions accepted provisionally; reviewer may change freely
|
|
|
|
- the public literals are `fit: content | house`; names of internal helpers and
|
|
exact file placement are technical choices;
|
|
- `content` is explicit current semantics rather than a newly invented mode;
|
|
- “house geometry” means the complete structural list in contract item 5, not
|
|
room polygons alone, because room-only bounds would cut thick outer walls and
|
|
opening leaves;
|
|
- objects deliberately excluded from bounds remain in DOM/SVG and may be
|
|
clipped; they are not hidden or deleted;
|
|
- `house` falls back to `content` when no structure exists rather than framing
|
|
an arbitrary stored square or rendering an empty card;
|
|
- a conservative analytic opening envelope is acceptable if it is tight to the
|
|
maximum rendered symbol and state-independent; DOM `getBBox()` is forbidden;
|
|
- exact synthetic fixture names and sub-pixel tolerances belong to the test
|
|
implementation, provided a one-pixel crop causes a deterministic failure.
|