mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
BG: drawn-plan paper follows the room contours, not their bounding box
Owner: the white backing must hug the ROOMS — an L-shaped house or detached buildings grew a white square around the plan. Drawn plans now paper one opaque shape per room (paperRoomShapes in logic.ts) in exactly the room's own geometry — polygon points / rounded rect verbatim — so the union of the stack is the paper: islands paint over their parent, open (virtual) boundaries change nothing, and the scene bg_color / daynight sky reaches the exterior walls, shows in the L's pocket and between buildings. Image plans keep the backdrop-image rect (the canvas IS the paper). A live resize preview (_rszPreview) feeds _renderCfg, so the paper moves WITH a dragged wall. Static space-card follows the same contract. Paper is fill-only (stroke:none). smoke_bg_color §11–13 rewritten: L-shaped + detached test rooms, paper-per- room DOM checks, resize-preview wiring, pixel probes (acid in the pocket and between buildings, none inside rooms); §13 injects the snapshot directly — the module-level config-store cache made the old WS mock a no-op. Was 8 red on the previous build, green now. docs/SUN.md + docs/TESTING.md contract updated; unit test for paperRoomShapes.
This commit is contained in:
File diff suppressed because one or more lines are too long
+93
-56
@@ -143,11 +143,12 @@ const res = await page.evaluate(async () => {
|
||||
checkAll(res);
|
||||
|
||||
// ---- 11) opaque plan paper (owner 2026-08-03) --------------------------
|
||||
// The scene bg_color must never bleed through the plan itself: an opaque
|
||||
// rect.hp-paper sits under everything the plan draws and hugs the plan's
|
||||
// extents (backdrop image rect / drawn-content bounds). The scene colour is
|
||||
// visible ONLY around it. Verified computed AND by pixels against an acid
|
||||
// background; the 'daynight' night dims the paper via brightness only.
|
||||
// The scene bg_color must never bleed through the plan itself: opaque
|
||||
// .hp-paper shapes sit under everything the plan draws. An IMAGE plan papers
|
||||
// the backdrop image rect (the canvas is the paper); a DRAWN plan papers the
|
||||
// ROOM CONTOURS — one shape per room, never their bounding box (section 12).
|
||||
// The scene colour is visible ONLY around the paper; the 'daynight' night
|
||||
// dims the paper via brightness only.
|
||||
const paper = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
@@ -193,41 +194,79 @@ const paper = await page.evaluate(async () => {
|
||||
});
|
||||
Object.assign(res, paper);
|
||||
|
||||
// ---- 12) drawn plan (no backdrop image): white paper + pixel proof ------
|
||||
// ---- 12) drawn plan: the paper follows the ROOM CONTOURS ---------------
|
||||
// Owner: «белая подложка должна быть по границам комнат» — an L-shaped house
|
||||
// plus a detached building must NOT grow a white bounding rectangle. One
|
||||
// paper shape per room, in exactly the room's own geometry; the acid scene
|
||||
// colour shows in the L's pocket and between the buildings, right up to the
|
||||
// exterior walls; inside the rooms it never shows. A live resize preview
|
||||
// (_rszPreview) moves the paper together with the dragged wall.
|
||||
const drawn = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const upd = async () => { c.requestUpdate(); await c.updateComplete; };
|
||||
const sp = c._serverCfg.spaces.find((s) => s.id === c._space);
|
||||
sp.plan_url = ''; // hand-drawn plan now
|
||||
// binary-exact fractions -> exact render coords (0.125*1000 = 125 etc.)
|
||||
sp.rooms = [
|
||||
{ id: 'rL', name: 'L', poly: [[0.125, 0.125], [0.625, 0.125], [0.625, 0.375], [0.375, 0.375], [0.375, 0.625], [0.125, 0.625]] },
|
||||
{ id: 'rQ', name: 'Q', x: 0.75, y: 0.75, w: 0.1875, h: 0.1875 },
|
||||
];
|
||||
sp.openings = [];
|
||||
sp.decor = [];
|
||||
c._devices = []; // no icons over the pixel probes below
|
||||
c._cfgRev = (c._cfgRev || 0) + 1;
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
const p = sr().querySelector('.stage.noplan svg rect.hp-paper');
|
||||
out.drawnPaperExists = !!p;
|
||||
out.drawnPaperWhite = !!p && getComputedStyle(p).fill === 'rgb(255, 255, 255)';
|
||||
if (p) {
|
||||
const pr = p.getBoundingClientRect();
|
||||
const st = sr().querySelector('.stage').getBoundingClientRect();
|
||||
out.rects = {
|
||||
paper: [pr.left, pr.top, pr.width, pr.height],
|
||||
stage: [st.left, st.top, st.width, st.height],
|
||||
};
|
||||
// the paper hugs the CONTENT — the scene colour stays visible around it
|
||||
out.bgVisibleAround = pr.left - st.left > 8 || (st.left + st.width) - (pr.left + pr.width) > 8
|
||||
|| pr.top - st.top > 8 || (st.top + st.height) - (pr.top + pr.height) > 8;
|
||||
}
|
||||
c._cfgEpoch++;
|
||||
await upd();
|
||||
const papers = () => [...sr().querySelectorAll('.stage.noplan svg .hp-paper')];
|
||||
let ps = papers();
|
||||
out.paperPerRoom = ps.length === sp.rooms.length;
|
||||
out.paperShapesMatchRooms = ps.length === 2
|
||||
&& ps[0].tagName.toLowerCase() === 'polygon'
|
||||
&& ps[0].getAttribute('points') === sp.rooms[0].poly.map((p) => (p[0] * 1000) + ',' + (p[1] * 1000)).join(' ')
|
||||
&& ps[1].tagName.toLowerCase() === 'rect';
|
||||
out.drawnPaperWhite = ps.length > 0 && ps.every((p) => getComputedStyle(p).fill === 'rgb(255, 255, 255)');
|
||||
out.paperNoStroke = ps.length > 0 && ps.every((p) => {
|
||||
const cs = getComputedStyle(p);
|
||||
return cs.stroke === 'none' || cs.strokeOpacity === '0' || cs.strokeWidth === '0px';
|
||||
});
|
||||
// paper first, everything else on top of it
|
||||
const first = sr().querySelector('.stage.noplan svg').firstElementChild;
|
||||
out.paperUnderneath = !!first && first.classList.contains('hp-paper');
|
||||
// live resize preview: drag the L's right wall 0.625 -> 0.6875 — the paper
|
||||
// must move WITH the room, not lag behind until the drop
|
||||
const pv = JSON.parse(JSON.stringify(sp));
|
||||
pv.rooms[0].poly = pv.rooms[0].poly.map(([x, y]) => [x === 0.625 ? 0.6875 : x, y]);
|
||||
c._rszPreview = { space: c._space, sp: pv };
|
||||
c._cfgEpoch++;
|
||||
await upd();
|
||||
out.previewMovesPaper = (papers()[0]?.getAttribute('points') || '').includes('687.5,125');
|
||||
c._rszPreview = null;
|
||||
c._cfgEpoch++;
|
||||
await upd();
|
||||
out.previewRestores = (papers()[0]?.getAttribute('points') || '').includes('625,125');
|
||||
// screen coords for the pixel probes (svg user units -> viewport px)
|
||||
const svgEl = sr().querySelector('.stage svg');
|
||||
const ctm = svgEl.getScreenCTM();
|
||||
const toScr = (x, y) => { const q = svgEl.createSVGPoint(); q.x = x; q.y = y; const r = q.matrixTransform(ctm); return [r.x, r.y]; };
|
||||
out.probes = {
|
||||
pocket: toScr(500, 500), // the L's notch — bg must show here
|
||||
gap: toScr(687, 300), // between the two buildings
|
||||
inside: [toScr(250, 250), toScr(500, 250), toScr(250, 500), toScr(843.75, 843.75)],
|
||||
};
|
||||
return out;
|
||||
});
|
||||
const { rects } = drawn;
|
||||
delete drawn.rects;
|
||||
const { probes } = drawn;
|
||||
delete drawn.probes;
|
||||
Object.assign(res, drawn);
|
||||
|
||||
// pixel proof: screenshot → canvas → not a single probe inside the paper
|
||||
// carries the acid scene colour; right outside the paper it IS the acid colour
|
||||
if (rects) {
|
||||
// pixel proof: the acid scene colour IS in the L's pocket and between the
|
||||
// buildings (where the old bounding-box paper drew a white square), and is
|
||||
// NOT inside any room
|
||||
if (probes) {
|
||||
const shot = (await page.screenshot()).toString('base64');
|
||||
const px = await page.evaluate(async ({ shot, rects }) => {
|
||||
const px = await page.evaluate(async ({ shot, probes }) => {
|
||||
const img = new Image();
|
||||
img.src = 'data:image/png;base64,' + shot;
|
||||
await img.decode();
|
||||
@@ -236,29 +275,19 @@ if (rects) {
|
||||
cv.height = img.height;
|
||||
const ctx = cv.getContext('2d', { willReadFrequently: true });
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const at = (x, y) => [...ctx.getImageData(Math.round(x), Math.round(y), 1, 1).data];
|
||||
const at = ([x, y]) => [...ctx.getImageData(Math.round(x), Math.round(y), 1, 1).data];
|
||||
const acid = ([r, g, b]) => r > 200 && b > 200 && g < 160; // #ff00ff admixture
|
||||
const [pl, pt, pw, ph] = rects.paper;
|
||||
const [sl, st, sw, sh] = rects.stage;
|
||||
const probes = [];
|
||||
for (const fx of [0.1, 0.3, 0.5, 0.7, 0.9])
|
||||
for (const fy of [0.1, 0.3, 0.5, 0.7, 0.9]) probes.push(at(pl + pw * fx, pt + ph * fy));
|
||||
// a point in the stage but outside the paper (the widest gap side)
|
||||
const gaps = [
|
||||
[pl - sl, [(sl + pl) / 2, pt + ph / 2]],
|
||||
[sl + sw - pl - pw, [(pl + pw + sl + sw) / 2, pt + ph / 2]],
|
||||
[pt - st, [pl + pw / 2, (st + pt) / 2]],
|
||||
[st + sh - pt - ph, [pl + pw / 2, (pt + ph + st + sh) / 2]],
|
||||
].sort((a, b) => b[0] - a[0]);
|
||||
return {
|
||||
noBleedInsidePlan: probes.every((q) => !acid(q)),
|
||||
acidAroundPlan: gaps[0][0] > 8 && acid(at(gaps[0][1][0], gaps[0][1][1])),
|
||||
acidInPocket: acid(at(probes.pocket)),
|
||||
acidBetweenBuildings: acid(at(probes.gap)),
|
||||
noAcidInsideRooms: probes.inside.every((q) => !acid(at(q))),
|
||||
};
|
||||
}, { shot, rects });
|
||||
}, { shot, probes });
|
||||
Object.assign(res, px);
|
||||
} else {
|
||||
res.noBleedInsidePlan = false;
|
||||
res.acidAroundPlan = false;
|
||||
res.acidInPocket = false;
|
||||
res.acidBetweenBuildings = false;
|
||||
res.noAcidInsideRooms = false;
|
||||
}
|
||||
|
||||
// ---- 13) the static space-card follows the same paper contract ----------
|
||||
@@ -266,17 +295,17 @@ const stat = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
await customElements.whenDefined('houseplan-space-card');
|
||||
// NOTE: the config-store cache is module-level and already primed by the
|
||||
// earlier static-card sections (with the PRE-§12 rooms), so a mocked
|
||||
// callWS would never reach the card — inject the snapshot directly.
|
||||
const mk = async (cfg) => {
|
||||
const hass2 = { ...c.hass, callWS: async (m) => {
|
||||
if (m.type === 'houseplan/config/get') return { config: cfg, rev: 1 };
|
||||
if (m.type === 'houseplan/layout/get') return { layout: {} };
|
||||
return c.hass.callWS(m);
|
||||
} };
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const el = document.createElement('houseplan-space-card');
|
||||
el.setConfig({ type: 'custom:houseplan-space-card', space: c._space });
|
||||
el.hass = hass2;
|
||||
el._snap = { config: cfg, rev: 1, layout: {} };
|
||||
el._loadedOnce = true;
|
||||
el.hass = c.hass;
|
||||
host.appendChild(el);
|
||||
const t0 = Date.now();
|
||||
while (!el.renderRoot?.querySelector('.hp-static-stage') && Date.now() - t0 < 6000)
|
||||
@@ -293,14 +322,22 @@ const stat = await page.evaluate(async () => {
|
||||
out.staticPaperOpaque = !!pa && getComputedStyle(pa).fillOpacity === '1'
|
||||
&& getComputedStyle(pa).fill !== 'none';
|
||||
a.host.remove();
|
||||
// drawn plan: per-room paper shapes (the L + the detached rect from §12)
|
||||
const drawnCfg = JSON.parse(JSON.stringify(base));
|
||||
drawnCfg.spaces.find((s) => s.id === c._space).plan_url = '';
|
||||
const dsp = drawnCfg.spaces.find((s) => s.id === c._space);
|
||||
dsp.plan_url = '';
|
||||
const b = await mk(drawnCfg);
|
||||
const pb = b.el.renderRoot.querySelector('.hp-static-stage svg rect.hp-paper');
|
||||
out.staticDrawnPaperExists = !!pb;
|
||||
const pbs = [...b.el.renderRoot.querySelectorAll('.hp-static-stage svg .hp-paper')];
|
||||
out.staticDrawnPaperPerRoom = pbs.length === dsp.rooms.length;
|
||||
out.staticDrawnPaperShapes = pbs.length === 2
|
||||
&& pbs[0].tagName.toLowerCase() === 'polygon'
|
||||
&& pbs[1].tagName.toLowerCase() === 'rect';
|
||||
b.host.remove();
|
||||
return out;
|
||||
});
|
||||
Object.assign(res, stat);
|
||||
checkAll({ ...paper, ...drawn, ...stat, noBleedInsidePlan: res.noBleedInsidePlan, acidAroundPlan: res.acidAroundPlan });
|
||||
checkAll({ ...paper, ...drawn, ...stat,
|
||||
acidInPocket: res.acidInPocket,
|
||||
acidBetweenBuildings: res.acidBetweenBuildings,
|
||||
noAcidInsideRooms: res.noAcidInsideRooms });
|
||||
await finish(browser, res);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+60
-53
File diff suppressed because one or more lines are too long
+14
-9
@@ -63,15 +63,20 @@ only — no entities are created, no services are called.
|
||||
without `sun.sun`) it behaves as `'static'`.
|
||||
- **The scene background never bleeds through the plan** (owner,
|
||||
2026-08-03). In BOTH modes the background — `bg_color` or the
|
||||
daynight sky — is visible only AROUND the plan: an opaque
|
||||
`rect.hp-paper` sits under everything the plan draws and hugs the
|
||||
plan's extents (the backdrop image rect, or the drawn content bounds
|
||||
the opening view fits). Its colour is the pre-bg_color canvas —
|
||||
white for hand-drawn plans, the theme card background under an
|
||||
image. The night dimming above is the `brightness` filter on the
|
||||
zoomwrap ONLY; the paper's alpha never changes. Applies to
|
||||
view/kiosk/editors and the static space-card alike
|
||||
(smoke_bg_color).
|
||||
daynight sky — is visible only AROUND the plan: opaque `.hp-paper`
|
||||
shapes sit under everything the plan draws. An image plan papers the
|
||||
backdrop image rect (the canvas IS the paper); a hand-drawn plan
|
||||
papers the ROOM CONTOURS — one shape per room in exactly the room's
|
||||
own geometry (fill only, no stroke), never their bounding box, so
|
||||
the background reaches the exterior walls of an L-shaped house and
|
||||
fills the gaps between detached buildings (an empty drawn space has
|
||||
no paper). Open (virtual) boundaries do not affect the paper; a live
|
||||
resize preview moves it together with the rooms. Its colour is the
|
||||
pre-bg_color canvas — white for hand-drawn plans, the theme card
|
||||
background under an image. The night dimming above is the
|
||||
`brightness` filter on the zoomwrap ONLY; the paper's alpha never
|
||||
changes. Applies to view/kiosk/editors and the static space-card
|
||||
alike (smoke_bg_color).
|
||||
|
||||
## Window light wedges — `settings.sun_rays`
|
||||
|
||||
|
||||
+12
-7
@@ -882,13 +882,18 @@ require hands on real hardware — they remain for the human pass.
|
||||
- [ ] Opaque plan paper (2026-08-03, owner): the scene background —
|
||||
`bg_color` or the daynight sky — is visible ONLY around the plan and
|
||||
NEVER bleeds through it, in view/kiosk/editors and on the static
|
||||
space-card. An opaque `rect.hp-paper` under everything the plan draws
|
||||
hugs the plan's extents (the backdrop image rect, or the drawn
|
||||
content bounds the opening view fits); a hand-drawn plan keeps its
|
||||
historical white canvas, an image plan the theme card background.
|
||||
At night (`daynight`) the paper dims via the zoomwrap `brightness`
|
||||
filter only — its alpha stays 1. Pixel-proofed against an acid
|
||||
`#ff00ff` background [auto: smoke_bg_color]
|
||||
space-card. An image plan papers the backdrop image rect
|
||||
(`rect.hp-paper`). A hand-drawn plan papers the ROOM CONTOURS: one
|
||||
`.hp-paper` shape per room in exactly the room's own geometry (fill
|
||||
only, no stroke), so an L-shaped house or detached buildings never
|
||||
grow a white bounding rectangle — the scene colour reaches the
|
||||
exterior walls, shows in the L's pocket and between buildings, and an
|
||||
empty drawn space has no paper at all. A live resize preview
|
||||
(`_rszPreview`) moves the paper together with the dragged wall.
|
||||
Colours: historical white for drawn plans, the theme card background
|
||||
under an image. At night (`daynight`) the paper dims via the zoomwrap
|
||||
`brightness` filter only — its alpha stays 1. Pixel-proofed against
|
||||
an acid `#ff00ff` background [auto: smoke_bg_color]
|
||||
- [ ] Per-space overrides (background mode, north, sun-in-windows) inherit
|
||||
when empty, exactly like show_lqi/fill_mode
|
||||
- [ ] «Sunlight through windows» (default OFF): wedges appear only from
|
||||
|
||||
+28
-15
@@ -13,7 +13,7 @@ import {
|
||||
} from './rules';
|
||||
import {
|
||||
lqiColor, snapToGrid, samePoint, pointInPolygon, markerIdForBinding,
|
||||
segmentCm, formatLength, roomEdges, roomPoly, pointStrictlyInside, roomsOverlap,
|
||||
segmentCm, formatLength, roomEdges, roomPoly, paperRoomShapes, pointStrictlyInside, roomsOverlap,
|
||||
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, openZoneOf, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, contentUrl,
|
||||
snapToWall, openingAmount, interiorPoint, poleOfInaccessibility, subst,
|
||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||
@@ -1598,19 +1598,20 @@ class HouseplanCard extends LitElement {
|
||||
|
||||
/**
|
||||
* Opaque plan "paper" (owner 2026-08-03): the scene background — bg_color or
|
||||
* the 'daynight' sky — must never bleed through the plan itself, so an
|
||||
* opaque rect sits under everything the plan draws. It hugs the plan's
|
||||
* extents: the backdrop image when there is one, otherwise the same content
|
||||
* bounds the opening view fits (an empty drawn space papers its whole
|
||||
* canvas — exactly the pre-bg_color look). Its colour is the pre-bg_color
|
||||
* canvas (styles.ts .hp-paper): white for drawn plans, the theme card
|
||||
* background under an image. At night 'daynight' dims it via the zoomwrap
|
||||
* brightness filter ONLY — its alpha stays 1 (docs/SUN.md).
|
||||
* the 'daynight' sky — must never bleed through the plan itself, so opaque
|
||||
* shapes sit under everything the plan draws. With a backdrop image the
|
||||
* paper is the image rect (the canvas IS the paper) — this helper returns
|
||||
* it. For a hand-drawn plan the paper follows the ROOM CONTOURS instead
|
||||
* (paperRoomShapes in logic.ts): one shape per room in exactly the room's
|
||||
* own geometry, so an L-shaped house or detached buildings never grow a
|
||||
* white bounding rectangle — the scene colour is visible right up to the
|
||||
* exterior walls, and an empty drawn space has no paper at all. Its colour
|
||||
* is the pre-bg_color canvas (styles.ts .hp-paper): white for drawn plans,
|
||||
* the theme card background under an image. At night 'daynight' dims it via
|
||||
* the zoomwrap brightness filter ONLY — its alpha stays 1 (docs/SUN.md).
|
||||
*/
|
||||
private _paperRect(m: SpaceModel): { x: number; y: number; w: number; h: number } {
|
||||
if (m.bg) return { x: m.bg.x, y: m.bg.y, w: m.bg.w, h: m.bg.h };
|
||||
const b = this._baseVb();
|
||||
return { x: b[0], y: b[1], w: b[2], h: b[3] };
|
||||
private _paperRect(m: SpaceModel): { x: number; y: number; w: number; h: number } | null {
|
||||
return m.bg ? { x: m.bg.x, y: m.bg.y, w: m.bg.w, h: m.bg.h } : null;
|
||||
}
|
||||
|
||||
/** Aspect ratio of the scene (width/height, px). */
|
||||
@@ -5065,8 +5066,20 @@ class HouseplanCard extends LitElement {
|
||||
style="${dayNight ? `filter:brightness(${(1 - planDim).toFixed(3)})` : ''}">
|
||||
<svg viewBox="${view.x} ${view.y} ${view.w} ${view.h}" preserveAspectRatio="xMidYMid meet">
|
||||
${(() => {
|
||||
const pp = this._paperRect(space);
|
||||
return svg`<rect class="hp-paper" x="${pp.x}" y="${pp.y}" width="${pp.w}" height="${pp.h}" pointer-events="none"></rect>`;
|
||||
// opaque paper: the image rect for image plans, the room
|
||||
// contours for drawn ones (owner: no white square around an
|
||||
// L-shaped house — the scene bg reaches the exterior walls).
|
||||
// `space` comes from _renderCfg, so a live resize preview
|
||||
// (_rszPreview) moves the paper together with the rooms.
|
||||
if (space.bg) {
|
||||
const pp = this._paperRect(space)!;
|
||||
return svg`<rect class="hp-paper" x="${pp.x}" y="${pp.y}" width="${pp.w}" height="${pp.h}" pointer-events="none"></rect>`;
|
||||
}
|
||||
return paperRoomShapes(space.rooms).map((sh) =>
|
||||
'poly' in sh
|
||||
? svg`<polygon class="hp-paper" points="${sh.poly}" pointer-events="none"></polygon>`
|
||||
: svg`<rect class="hp-paper" x="${sh.rect.x}" y="${sh.rect.y}" width="${sh.rect.w}" height="${sh.rect.h}" rx="${sh.rect.rx}" pointer-events="none"></rect>`,
|
||||
);
|
||||
})()}
|
||||
${this._editing ? this._renderMarkupDefs(vb) : nothing}
|
||||
${this._editing && !this._markup
|
||||
|
||||
@@ -62,6 +62,30 @@ export function roomPoly(r: any): number[][] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paper shapes for a hand-drawn plan (owner 2026-08-03): the opaque paper
|
||||
* follows the ROOM CONTOURS, not their bounding box — an L-shaped house or a
|
||||
* detached building must not grow a white rectangle around itself. One shape
|
||||
* per room, in EXACTLY the geometry the room itself renders (same polygon
|
||||
* points string, same rounded rect), so the paper never peeks past a wall;
|
||||
* the union of the stack is the paper. Islands simply paint over their
|
||||
* parent; open (virtual) boundaries do not affect the paper at all.
|
||||
*/
|
||||
export function paperRoomShapes(rooms: any[]): Array<
|
||||
| { poly: string }
|
||||
| { rect: { x: number; y: number; w: number; h: number; rx: number } }
|
||||
> {
|
||||
const out: Array<{ poly: string } | { rect: { x: number; y: number; w: number; h: number; rx: number } }> = [];
|
||||
for (const r of rooms || []) {
|
||||
if (r?.poly?.length >= 3) {
|
||||
out.push({ poly: r.poly.map((p: number[]) => p.join(',')).join(' ') });
|
||||
} else if (r && r.x != null && r.y != null && r.w != null && r.h != null) {
|
||||
out.push({ rect: { x: r.x, y: r.y, w: r.w, h: r.h, rx: Math.min(r.w, r.h) * 0.03 } });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function roomEdges(rooms: any[]): number[][] {
|
||||
const out: number[][] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
+4
-1
@@ -207,9 +207,12 @@ class HouseplanSpaceCard extends LitElement {
|
||||
}
|
||||
/* Opaque plan paper — the scene bg_color/daynight sky shows only AROUND
|
||||
the plan (owner 2026-08-03). The static card keeps its historical
|
||||
canvas colour: the theme card background. */
|
||||
canvas colour: the theme card background. Drawn plans paper the room
|
||||
contours (per-room shapes, fill only, no stroke) — same contract as
|
||||
the full card. */
|
||||
.hp-paper {
|
||||
fill: var(--ha-card-background, var(--card-background-color, #111));
|
||||
stroke: none;
|
||||
}
|
||||
.hp-static-stage .devlayer {
|
||||
position: absolute;
|
||||
|
||||
+15
-11
@@ -8,13 +8,13 @@
|
||||
*/
|
||||
import { html, svg, nothing, type TemplateResult } from 'lit';
|
||||
import { buildDevices, areaLqi, areaLights, areaTemp } from './devices';
|
||||
import { spaceDisplayOf, roomFillStyle, fillColorsOf, roomFillModeOf, stageBgOf } from './logic';
|
||||
import { spaceDisplayOf, roomFillStyle, fillColorsOf, roomFillModeOf, stageBgOf, paperRoomShapes } from './logic';
|
||||
import { DEFAULT_ICON_RULES, compileIconRules, EXCLUDED_DOMAINS } from './rules';
|
||||
import { t, type Lang } from './i18n';
|
||||
import { bgModeOf, northDegOf, sunStateOf, dayPhase } from './sun';
|
||||
import type { ServerConfig } from './types';
|
||||
import {
|
||||
spaceModels, roomCenter, defaultPositions, markerPos, labelPos, contentBounds, type Layout,
|
||||
spaceModels, roomCenter, defaultPositions, markerPos, labelPos, type Layout,
|
||||
} from './space-geometry';
|
||||
|
||||
export { spaceModels } from './space-geometry';
|
||||
@@ -159,21 +159,25 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
const stageBg = sunBg || stageBgOf(o.cfg?.settings, disp);
|
||||
|
||||
// Opaque plan paper (owner 2026-08-03), same contract as the full card: the
|
||||
// scene background never bleeds through the plan. The rect hugs the backdrop
|
||||
// image, or the drawn content's bounds (marker positions included, as on the
|
||||
// full card); an empty space papers its whole canvas.
|
||||
const paperPts = spaceDevs.map((d) => {
|
||||
const p = markerPos(d, o.layout, o.cfg, defPos, space);
|
||||
return [p.x, p.y] as const;
|
||||
});
|
||||
// scene background never bleeds through the plan. With a backdrop image the
|
||||
// paper is the image rect; a hand-drawn plan papers the ROOM CONTOURS
|
||||
// (paperRoomShapes) — never their bounding box, so the scene colour reaches
|
||||
// the exterior walls of an L-shaped house and fills the gaps between
|
||||
// detached buildings. An empty drawn space has no paper at all.
|
||||
const paper = space.bg
|
||||
? { x: space.bg.x, y: space.bg.y, w: space.bg.w, h: space.bg.h }
|
||||
: contentBounds(space, 0.05, paperPts) || { x: vb[0], y: vb[1], w: vb[2], h: vb[3] };
|
||||
: null;
|
||||
|
||||
return html`
|
||||
<div class="hp-static-stage" style="aspect-ratio:${vb[2]}/${vb[3]}${stageBg ? ';background:' + stageBg : ''}">
|
||||
<svg viewBox="${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}" preserveAspectRatio="xMidYMid meet">
|
||||
${svg`<rect class="hp-paper" x="${paper.x}" y="${paper.y}" width="${paper.w}" height="${paper.h}"></rect>`}
|
||||
${paper
|
||||
? svg`<rect class="hp-paper" x="${paper.x}" y="${paper.y}" width="${paper.w}" height="${paper.h}"></rect>`
|
||||
: paperRoomShapes(space.rooms).map((sh) =>
|
||||
'poly' in sh
|
||||
? svg`<polygon class="hp-paper" points="${sh.poly}"></polygon>`
|
||||
: svg`<rect class="hp-paper" x="${sh.rect.x}" y="${sh.rect.y}" width="${sh.rect.w}" height="${sh.rect.h}" rx="${sh.rect.rx}"></rect>`,
|
||||
)}
|
||||
${bgHref
|
||||
? svg`<image href="${bgHref}" x="${space.bg!.x}" y="${space.bg!.y}" width="${space.bg!.w}" height="${space.bg!.h}" preserveAspectRatio="none" />`
|
||||
: nothing}
|
||||
|
||||
+6
-2
@@ -316,10 +316,14 @@ export const cardStyles = css`
|
||||
/* Opaque plan paper (owner 2026-08-03): the scene bg_color / daynight sky
|
||||
shows ONLY around the plan, never through it. The colour is the
|
||||
pre-bg_color canvas — the theme card background under an image plan,
|
||||
plain white for a hand-drawn one (.stage.noplan). Night dimming comes
|
||||
from the .zoomwrap brightness filter, never from alpha (docs/SUN.md). */
|
||||
plain white for a hand-drawn one (.stage.noplan). On drawn plans the
|
||||
paper is per-room shapes following the room contours — fill only,
|
||||
never a stroke, so the paper cannot poke past a wall. Night dimming
|
||||
comes from the .zoomwrap brightness filter, never from alpha
|
||||
(docs/SUN.md). */
|
||||
.hp-paper {
|
||||
fill: var(--ha-card-background, var(--card-background-color, #111));
|
||||
stroke: none;
|
||||
}
|
||||
.stage.noplan .hp-paper {
|
||||
fill: #ffffff;
|
||||
|
||||
+22
-1
@@ -14,7 +14,7 @@ import {
|
||||
roomFillModeOf,
|
||||
contentUrl, chunk, referencedContentUrls, MAX_SIGN_PATHS,
|
||||
interiorPoint,
|
||||
segmentCm, formatLength, roomEdges, roomPoly, pointOnBoundary, pointStrictlyInside, roomsOverlap,
|
||||
segmentCm, formatLength, roomEdges, roomPoly, paperRoomShapes, pointOnBoundary, pointStrictlyInside, roomsOverlap,
|
||||
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, poleOfInaccessibility, runServiceFor, TOGGLE_SAFE_DOMAINS } from '../test-build/logic.js';
|
||||
import {
|
||||
iconFor, compileIconRules, isValidPattern, iconFromDeviceClasses,
|
||||
@@ -959,3 +959,24 @@ test('tap action run: explicit-only, with runnable targets (owner spec 2026-07-2
|
||||
// locks and alarms stay forbidden, run or not
|
||||
assert.equal(resolveTapAction('toggle', undefined, 'lock'), 'info');
|
||||
});
|
||||
|
||||
|
||||
test('paperRoomShapes: paper follows room contours, one shape per room (owner 2026-08-03)', () => {
|
||||
const L = [[100, 100], [600, 100], [600, 350], [350, 350], [350, 600], [100, 600]];
|
||||
const rooms = [
|
||||
{ id: 'L', poly: L }, // L-shaped: polygon, verbatim
|
||||
{ id: 'q', x: 650, y: 650, w: 200, h: 100 }, // legacy rect: same rounded rect the room draws
|
||||
{ id: 'bad' }, // no geometry -> no paper
|
||||
];
|
||||
const shapes = paperRoomShapes(rooms);
|
||||
assert.equal(shapes.length, 2, 'one shape per room WITH geometry');
|
||||
// the polygon uses exactly the room's own points string — same coordinates,
|
||||
// so the paper can never peek past a wall
|
||||
assert.deepEqual(shapes[0], { poly: L.map((p) => p.join(',')).join(' ') });
|
||||
// the rect mirrors the room shape incl. its corner rounding rx
|
||||
assert.deepEqual(shapes[1], { rect: { x: 650, y: 650, w: 200, h: 100, rx: 100 * 0.03 } });
|
||||
// degenerate inputs never throw and produce no paper
|
||||
assert.deepEqual(paperRoomShapes([]), []);
|
||||
assert.deepEqual(paperRoomShapes(null), []);
|
||||
assert.deepEqual(paperRoomShapes([{ poly: [[0, 0], [1, 1]] }]), [], 'a 2-point poly is not a surface');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user