Files
houseplan-card/demo/smoke_wall_hatch_density.mjs
Claude 600330e187 fix(daycycle): stage-sized plan layers without a frozen raster; revert #685 hatch
With the day/night background the plan went blurry after zooming from 100 %
(sharp when the page was opened at 800 %) and navigating a strongly zoomed
plan flashed the page white. Both came from #582's composition, not from the
wall hatch that #685 replaced:

- `.stage.daycycle.hp-safe-daycycle-outline .plan-svg` promoted the scene
  with `will-change: transform`; Chromium freezes the raster scale of such a
  layer, so the 100 % raster was shown stretched. The explicit layer #582
  needs is now `will-change: opacity` (re-rasters at the current scale).
- The filtered outline had `overflow: visible` and a gesture exposed every
  scene (#544) without bound, so the promoted layers grew with zoom squared
  (CDP LayerTree, ~460 %: plan-svg 15.9x, outline 13.2x the stage; 39x after
  navigating at 800 %). The full card clips its outline to its box and marks
  it data-hp-live-overflow="clip" (never exposed); the live viewport bounds
  every other exposure with an inline clip-path: inset(-25%) that leaves
  with it, so idle DOM stays byte-identical (#531).

Owner-verified in Chrome 152 (built-in browser, DPR 2): sharp after 100 ->
800 %, no white flashes after reloading at 800 %.

Owner decision: #685's analytic gradient is reverted (13af1d5e), the single
<pattern> is back at every zoom; its close-up golden scenes stay and check
the pattern, its terminal-frame smoke checks the pattern.

Witnesses: demo/smoke_daycycle_zoom_layers.mjs (800 % x DPR 2: layers vs
stage, the hint, reload path, button/wheel/pinch); #582/#532 smokes now pin
the opacity hint; test/live-viewport.test.mjs (bounded exposure, clipped
scene); test/daycycle-layers.test.mjs (cascade). Four Node-guarded mutants.

Issue: #689
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:51:06 +03:00

90 lines
4.0 KiB
JavaScript

// Issue #230: the hatch step is a physical distance, not a coordinate one.
//
// The units own the arithmetic; this smoke owns the wiring — that both renderers
// actually put the computed step into the pattern, that neither of them scales
// it back by zoom, and that the two agree with each other.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 1000, height: 900 }, 1);
const res = await page.evaluate(async () => {
const out = {};
const c = window.__card;
const settle = async () => {
for (let i = 0; i < 3; i++) await new Promise((r) => requestAnimationFrame(r));
await c.updateComplete;
};
const space = () => c._serverCfg.spaces.find((s) => s.id === c._space);
const pattern = (root) => root.querySelector('#hp-wall-hatch');
const read = (p) => p && ({
width: Number(p.getAttribute('width')),
height: Number(p.getAttribute('height')),
transform: p.getAttribute('patternTransform') || '',
stroke: Number(p.querySelector('path')?.getAttribute('stroke-width')),
d: p.querySelector('path')?.getAttribute('d') || '',
});
c._mode = 'plan'; c.requestUpdate(); await settle();
await new Promise((r) => setTimeout(r, 400));
// The demo house has no thick walls, and without a wall body neither renderer
// emits the pattern. Set 15 cm on one wall the way a person would — through
// the thickness tool, so the walls end up keyed exactly as the card expects.
space().settings = { ...(space().settings || {}), show_borders: true };
c._tool = 'wallthick';
c.requestUpdate(); await settle();
c._wallThickClick([50, 250]);
await settle();
c._wallDialog = { ...c._wallDialog, value: '15' };
c._wallThickApply(true);
await new Promise((r) => setTimeout(r, 500));
c._tool = null;
c.requestUpdate(); await settle();
out.wallBodyIsRendered = !!c.shadowRoot.querySelector('.wallbody');
// Reference scale: exactly the historical numbers, so old plans do not move.
const atFive = read(pattern(c.shadowRoot));
out.referenceStepIsEight = atFive?.width === 8 && atFive?.height === 8;
out.referenceStrokeIsTwo = atFive?.stroke === 2;
out.noZoomScaleAtReference = !!atFive && !/scale/.test(atFive.transform);
// Zoom must not touch the pattern any more — that is the whole point.
c._applyView(3); await settle();
const zoomed = read(pattern(c.shadowRoot));
out.zoomDoesNotChangeThePattern = JSON.stringify(zoomed) === JSON.stringify(atFive);
c._applyView(1); await settle();
// A coarse grid: the step follows the centimetres, so it shrinks in units.
space().cell_cm = 25;
// Saved, not just poked locally: the static card reads the config from the
// server, so a local mutation would leave it on the old scale.
c._saveConfig();
await new Promise((r) => setTimeout(r, 500));
c.requestUpdate(); await settle();
const atTwentyFive = read(pattern(c.shadowRoot));
out.coarseGridShrinksTheStep = Math.abs(atTwentyFive.width - 1.6) < 1e-9;
out.coarseGridScalesTheStroke = Math.abs(atTwentyFive.stroke - 0.4) < 1e-9;
out.coarseGridStripeSpansTheCell = atTwentyFive.d === `M0 0 L0 ${atTwentyFive.width}`;
// The static renderer is the second path that draws a wall body, and it used
// to carry its own hard-coded 8 (spec §8.2, AC12).
await customElements.whenDefined('houseplan-space-card');
const host = document.createElement('div');
document.body.appendChild(host);
const card = document.createElement('houseplan-space-card');
card.setConfig({ type: 'custom:houseplan-space-card', space: c._space });
card.hass = c.hass;
host.appendChild(card);
const t0 = Date.now();
while (!card.renderRoot?.querySelector('.hp-static-stage') && Date.now() - t0 < 6000) {
await new Promise((r) => setTimeout(r, 80));
}
await card.updateComplete;
const staticPattern = read(pattern(card.renderRoot));
out.staticRendererFollowsTheCell = !!staticPattern
&& Math.abs(staticPattern.width - 1.6) < 1e-9;
out.bothRenderersAgree = JSON.stringify(staticPattern) === JSON.stringify(atTwentyFive);
return out;
});
checkAll(res);
await finish(browser, res);