mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
Одна и та же стена 15 см выглядела на планах с разным `cell_cm` по-разному: шаг паттерна был константой в юнитах, а толщина стены переводится в юниты через `cell_cm`, — значит число полос было пропорционально `1/cell_cm`. Разброс между крайними масштабами достигал 25 раз, а при `cell_cm ≥ 10` в стену не попадало и одной полосы: штриховка вырождалась в случайные штрихи или исчезала. Шаг стал физической величиной: `wallHatchStepUnits(cellCm)` возвращает `8 × (5 / cell_cm)` — это 9.6 см плана при любом масштабе сетки и ровно исторические 8 юнитов при эталонном `cell_cm: 5`, так что старые планы не двигаются. Толщина штриха следует за шагом, поэтому соотношение «штрих к просвету» тоже перестало зависеть от масштаба. Формула из описания issue (`cell_cm / 5`) не годилась: она увеличивает шаг там, где стена и без того тонкая в юнитах, и разброс не исчезает, а растёт — 39 полос против 0.06 на краях диапазона. Множитель обратный; на эту ошибку поставлен отдельный мутант `hatch-step-inverted`. Зумовая компенсация `1/zoom` убрана по решению владельца (§4.2 ТЗ): стена, которая меняет штриховку при зуме, — это тот же дефект, только по другой оси. От каши на дальнем конце зума защищает второй порог `wallHatchNeedsSolid` рядом с существующим `wallBodyNeedsSolid`; шаг клампится в [0.5, 80] юнитов, чтобы патологический `cell_cm` не выродил паттерн. Статический рендерер (`space-render.ts`) нёс собственную константу 8 и вообще не знал про зум — то есть уже сегодня расходился с картой при любом зуме, кроме единицы. Теперь оба читают шаг из одной функции; смок проверяет, что они согласны между собой, а не только каждый сам с собой. Два golden-эталона переснято осознанно (`golden:accept -- --reviewed`): `large-house-zoom-040-dark` (шаг был 20 юнитов, стал 8) и `large-house-zoom-250-dark` (был 3.2, стал 8). Расхождение осмотрено: меняется только плотность штриховки тел стен, колонн и перегородок, геометрия и цвета идентичны. Остальные 80 сцен не тронуты — `accept` переснимает весь набор, и пять сцен, разошедшихся на шуме рендера, возвращены к прежним байтам вместе с их хэшами в индексе. Issue: #230 User-Visible: yes
90 lines
4.0 KiB
JavaScript
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);
|