mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
A hidden device no longer stretches the plan's frame
Audit dev@2c947f4, DEV-2C947-01 (P2). One visible room and one marker with a saved position 90 canvases out, then the marker is hidden: the auditor's probe measured a frame 112.375x wider than the room it drew — the house opened as a dot in the corner of empty canvas. The same on `houseplan-space-card`. Both cards filtered the devices for RENDERING and framed the unfiltered list. The full card's `_contentItems` walked `_devices` without looking at `hidden`, while the renderer a few lines later drew `!d.hidden`; `space-render.ts` said it out loud — `devs = spaceDevs.filter(d => !d.hidden)` for the markers, `spaceDevs` for the frame. The frame is PRESENTATION (docs/CANVAS.md §4), so it follows what is drawn. Hidden devices keep everything the filtering contract gives them: they are still built, still counted by room LQI, still hold their cell in the auto-grid roster (so hiding one does not move a visible neighbour) — they are simply not content items. The device editor's ghosts are not items either: reaching a ghost is what the §5 pan slack is for, and making the frame follow a local, ephemeral editor toggle would have made the opening view depend on which tab had it switched on. demo/smoke_canvas_frame.mjs is the auditor's probe, both cards: with the marker visible the frame holds it (2 items is below MIN_VOTERS, so the outlier vote cannot quietly rescue the test); hidden, the marker is gone from the DOM, the frame is exactly the room's 60..940 and the room fills the stage. Three of its checks are red on the parent commit.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,118 @@
|
||||
// The content frame is PRESENTATION (docs/CANVAS.md §4) — audit dev@2c947f4.
|
||||
//
|
||||
// DEV-2C947-01: a hidden device is not drawn, but it still stretched the
|
||||
// frame — hiding a marker that had wandered into the yard left the visible
|
||||
// house a dot in the corner (the full card AND the static
|
||||
// houseplan-space-card, which drew `devs` but framed `spaceDevs`).
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 900, height: 820 }, 1);
|
||||
const out = {};
|
||||
const raf = () => page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))));
|
||||
|
||||
// Load a config/layout into the card AND into the demo's WS stub, so the
|
||||
// static card (which fetches its own copy) sees exactly the same house.
|
||||
const load = (cfg, layout, space) => page.evaluate(async ([c2, l2, sp]) => {
|
||||
const c = window.__card;
|
||||
const srvCfg = (await c.hass.callWS({ type: 'houseplan/config/get' })).config;
|
||||
const srvLay = (await c.hass.callWS({ type: 'houseplan/layout/get' })).layout;
|
||||
for (const k of Object.keys(srvCfg)) delete srvCfg[k];
|
||||
Object.assign(srvCfg, JSON.parse(JSON.stringify(c2)));
|
||||
for (const k of Object.keys(srvLay)) delete srvLay[k];
|
||||
Object.assign(srvLay, JSON.parse(JSON.stringify(l2)));
|
||||
c._serverCfg = srvCfg;
|
||||
c._layout = JSON.parse(JSON.stringify(l2));
|
||||
c._modelCache = null;
|
||||
c._frame = null;
|
||||
c._showFar = false;
|
||||
c._space = sp;
|
||||
c._cfgEpoch++;
|
||||
c._regSignature = '';
|
||||
c._maybeRebuildDevices?.();
|
||||
c._defPos = c._defaultPositions();
|
||||
c._view = null;
|
||||
c._zoom = 1;
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
c._resetZoom();
|
||||
await c.updateComplete;
|
||||
}, [cfg, layout, space]);
|
||||
|
||||
// ---------------------------------------------------------------- 01: hidden
|
||||
// The auditor's probe, verbatim: ONE visible room and ONE marker parked 90
|
||||
// canvases away. Two content items is below MIN_VOTERS, so the outlier vote
|
||||
// never runs and the far position is guaranteed to stay in the core — the
|
||||
// frame either sees the marker or it does not.
|
||||
const oneRoom = (hidden) => ({
|
||||
spaces: [{
|
||||
id: 'h1', title: 'Hidden', view_box: [0, 0, 1, 1], plan_url: null, plan_aspect: null,
|
||||
rooms: [{ id: 'r1', name: 'Room', area: 'zx_room',
|
||||
poly: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]] }],
|
||||
}],
|
||||
// a made-up area: no auto device of the demo house lands in this space, so
|
||||
// the only content is the room and the one marker below
|
||||
markers: [{ id: 'm_far', binding: 'device:d_motion', area: 'zx_room', hidden }],
|
||||
settings: { filter_seeded: true },
|
||||
});
|
||||
const FAR_POS = { m_far: { s: 'h1', x: 90, y: 0.5 } };
|
||||
|
||||
await load(oneRoom(false), FAR_POS, 'h1');
|
||||
await raf();
|
||||
Object.assign(out, await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const sr = c.shadowRoot || c.renderRoot;
|
||||
const b = c._baseVb();
|
||||
return {
|
||||
// premise: while it IS drawn, the marker is content and the frame holds it
|
||||
visibleMarkerIsDrawn: sr.querySelectorAll('.devlayer .dev').length === 1,
|
||||
visibleMarkerWidensTheFrame: b[2] > 50000,
|
||||
};
|
||||
}));
|
||||
|
||||
await load(oneRoom(true), FAR_POS, 'h1');
|
||||
await raf();
|
||||
Object.assign(out, await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const sr = c.shadowRoot || c.renderRoot;
|
||||
const o = {};
|
||||
const b = c._baseVb();
|
||||
o.hiddenMarkerNotDrawn = sr.querySelectorAll('.devlayer .dev').length === 0;
|
||||
o.hiddenMarkerStillBuilt = !!c._devices.find((d) => d.id === 'm_far' && d.hidden === true);
|
||||
// the room is 100..900 render units, padded by 5 % of the longer side
|
||||
o.frameIsTheVisibleRoom = Math.round(b[0]) === 60 && Math.round(b[2]) === 880
|
||||
&& Math.round(b[1]) === 60 && Math.round(b[3]) === 880;
|
||||
// and the room fills the screen instead of being a dot 112x too small
|
||||
const st = sr.querySelector('.stage').getBoundingClientRect();
|
||||
const poly = sr.querySelector('polygon.room').getBoundingClientRect();
|
||||
o.roomFillsTheStage = poly.width > st.width * 0.7;
|
||||
return o;
|
||||
}));
|
||||
|
||||
// the same house on the STATIC card (space-render.ts drew `devs` but framed
|
||||
// `spaceDevs` — the identical split)
|
||||
Object.assign(out, await page.evaluate(async () => {
|
||||
await customElements.whenDefined('houseplan-space-card');
|
||||
const o = {};
|
||||
const host = document.createElement('div');
|
||||
host.style.cssText = 'width:400px;height:400px';
|
||||
document.body.appendChild(host);
|
||||
const el = document.createElement('houseplan-space-card');
|
||||
el.setConfig({ type: 'custom:houseplan-space-card', space: 'h1' });
|
||||
el.hass = window.__card.hass;
|
||||
host.appendChild(el);
|
||||
const t0 = Date.now();
|
||||
while (!el.renderRoot?.querySelector('.hp-static-stage') && Date.now() - t0 < 6000) {
|
||||
await new Promise((r) => setTimeout(r, 80));
|
||||
}
|
||||
await el.updateComplete;
|
||||
const svg = el.renderRoot.querySelector('.hp-static-stage svg');
|
||||
const vb = (svg?.getAttribute('viewBox') || '').split(/\s+/).map(Number);
|
||||
o.staticCardHidesTheMarker = el.renderRoot.querySelectorAll('.devlayer .dev').length === 0;
|
||||
o.staticFrameIsTheVisibleRoom = vb.length === 4
|
||||
&& Math.round(vb[0]) === 60 && Math.round(vb[2]) === 880;
|
||||
host.remove();
|
||||
return o;
|
||||
}));
|
||||
|
||||
checkAll(out);
|
||||
await finish(browser, out);
|
||||
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+6
-1
@@ -81,7 +81,12 @@ tested. Input is a list of **items**, one per drawn/placed object:
|
||||
* the backdrop image rectangle, when the space has one;
|
||||
* every opening (door/window) end-to-end segment;
|
||||
* every decor shape;
|
||||
* every device the layout actually places in this space.
|
||||
* every device the layout actually places in this space **and that the card
|
||||
actually draws** — a HIDDEN device (docs/FILTERING.md) is not content: the
|
||||
frame is presentation, and an object nobody can see must not decide what the
|
||||
plan opens on. It keeps its auto-grid cell and every aggregation it feeds;
|
||||
it simply is not an item here. Ghosts in the device editor are not items
|
||||
either — reaching them is §5's job, not the opening view's.
|
||||
|
||||
Output:
|
||||
|
||||
|
||||
@@ -51,6 +51,15 @@ the old behaviour until an editing client materialises it.
|
||||
- "Show hidden devices" (rename of "Show all") is LOCAL, ephemeral state of
|
||||
the current tab.
|
||||
- Room LQI counts hidden devices (owner's decision).
|
||||
- Hidden devices are NOT content for the CONTENT FRAME (docs/CANVAS.md §4,
|
||||
audit DEV-2C947-01). The frame is presentation: an object the plan does not
|
||||
draw may not decide what the plan opens on. Hiding a marker that had once
|
||||
been dragged into the yard used to leave the visible house a dot in the
|
||||
corner of a frame 112x too wide — on the full card and on
|
||||
`houseplan-space-card` alike. They keep their place in the auto-grid roster
|
||||
(so a visible neighbour does not move when one is hidden) and in every
|
||||
aggregation listed here; only the frame stops seeing them, ghosts in the
|
||||
device editor included — reaching a ghost is the pan slack's job (§5).
|
||||
- Light fill and glow do NOT count hidden devices — an invisible device casts
|
||||
no visible light (owner's decision). Room climate is registry-wide and
|
||||
unaffected, as before.
|
||||
|
||||
@@ -992,6 +992,12 @@ require hands on real hardware — they remain for the human pass.
|
||||
drawn plus a small margin, on every floor, with and without a backdrop
|
||||
image (with one the IMAGE sets the extent — it must not be cropped to
|
||||
the outlined rooms)
|
||||
- [ ] **What is not drawn does not frame** (audit DEV-2C947-01): tick «hide
|
||||
from plan» on a marker standing far from the house and the view snaps
|
||||
back to the house — the hidden marker neither renders nor stretches the
|
||||
frame, on the full card and on `houseplan-space-card`. Untick it and the
|
||||
frame takes it in again; room LQI counted it the whole time
|
||||
[auto: smoke_canvas_frame]
|
||||
- [ ] **A far stray does not break the view** (§4.1): a marker dragged an
|
||||
order of magnitude away leaves the opening view alone and raises the
|
||||
inline chip «Объектов далеко от плана: N» with «Показать». No modal.
|
||||
|
||||
@@ -1796,11 +1796,19 @@ class HouseplanCard extends LitElement {
|
||||
* model, openings/decor/devices are added here because the model does not
|
||||
* carry them. Devices use their RESOLVED position, so a marker parked in
|
||||
* the far corner of the yard frames with the rest.
|
||||
*
|
||||
* HIDDEN devices are NOT content (DEV-2C947-01): the frame is presentation,
|
||||
* and a device the plan does not draw must not decide what the plan opens
|
||||
* on — hiding a marker that once wandered into the yard used to leave the
|
||||
* house a dot in the corner of an empty frame. They keep counting for room
|
||||
* LQI/climate and they keep their auto-grid cell (docs/FILTERING.md); only
|
||||
* the frame stops seeing them, including the ghosts of the device editor,
|
||||
* whose reach is the pan slack's job, not the opening view's.
|
||||
*/
|
||||
private _contentItems(m: SpaceModel): ContentItem[] {
|
||||
const extra: ContentItem[] = [];
|
||||
for (const d of this._devices) {
|
||||
if (d.space !== m.id) continue;
|
||||
if (d.space !== m.id || d.hidden) continue;
|
||||
const p = this._pos(d);
|
||||
extra.push({ minX: p.x, minY: p.y, maxX: p.x, maxY: p.y });
|
||||
}
|
||||
|
||||
+5
-2
@@ -83,9 +83,12 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
// docs/CANVAS.md §4: the static card frames the CONTENT, exactly like the
|
||||
// full one — `space.vb` is only the stored hint now, and on a plan drawn
|
||||
// past the old unit square it framed empty canvas with the house off-screen.
|
||||
// Markers placed outside every room count too (a gate sensor by the fence).
|
||||
// Markers placed outside every room count too (a gate sensor by the fence)
|
||||
// — but only the ones this card DRAWS: a hidden device is never painted
|
||||
// here, so it must not stretch the frame either (DEV-2C947-01). It keeps
|
||||
// its grid cell above; the frame is presentation, the roster is not.
|
||||
const placed: ContentItem[] = [];
|
||||
for (const d of spaceDevs) {
|
||||
for (const d of devs) {
|
||||
const sv = o.layout[d.id];
|
||||
if (sv && sv.s === o.spaceId) {
|
||||
const x = sv.x * NORM_W, y = sv.y * NORM_W;
|
||||
|
||||
Reference in New Issue
Block a user