mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
- settings.volumetric_view (General settings › Display, third switch) replaces the alpha entry, the header projection toggle and the phone-menu item; one rule for card, sidebar page and kiosk; editors stay Flat; backend accepts only a boolean, support package copies it. - Raised tiles in 2.5D View: no ring, rounded body min(0.275 D, 0.3 h), edge 0.1 D with the lab filters, ×1.12 size also in layout/collision, lift 0.075 D, one floor-shadow layer under all markers (theme × floor table), frames hugging tile and edge (Alert > Focus > Selected > Hover), forced colours without edge and shadow (src/iso-tiles.ts, styles/iso-tiles). - Soft sun wash instead of projected Flat wedges (src/iso-sun.ts): same gates and windows as sun_rays, length from elevation, parallelogram along the sun, tone and streaks by floor lightness, sill line. - Walls take the user's wall colour (top opaque, sides × .77/.68/.60); theme rules for walls/openings/labels removed; furniture uses the Flat stroke rule. - Smokes smoke_iso_tiles/iso_sun/iso_theme_walls/volumetric_setting, 16 new mutants, acceptance scenes STAGE6_ACCEPTANCE_SCENARIOS (golden with their Linux CI baselines), ACCEPTANCE.md side by side. Issue: #649 User-Visible: yes
492 lines
23 KiB
JavaScript
492 lines
23 KiB
JavaScript
// Issue #239: a finer coordinate grid changes precision, never the visible plan.
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
import { coherentGridScaleStaticPatch } from './grid-scale-static-fixture.mjs';
|
|
|
|
const { page, browser } = await launch({ width: 1000, height: 860 }, 1, [], {
|
|
reducedMotion: 'reduce',
|
|
});
|
|
|
|
await page.evaluate(async () => {
|
|
const card = window.__card;
|
|
const base = {
|
|
id: 'f1', title: 'Grid invariant', plan_url: '/assets/f1.svg', plan_aspect: 1.25,
|
|
view_box: [0, 0, 1, 1], cell_cm: 5,
|
|
settings: {
|
|
show_borders: true, show_names: true, fill_mode: 'custom',
|
|
custom_fill: { c: '#d8e2e8', a: 0.22 }, room_color: '#2d8fce', room_opacity: 0.8,
|
|
hide_decor: false, hide_openings: false, glow_enabled: true, sun_rays: false,
|
|
},
|
|
rooms: [{
|
|
id: 'r1', name: 'Living room', area: 'living_room',
|
|
poly: [[0.10, 0.12], [0.90, 0.12], [0.90, 0.88], [0.10, 0.88]],
|
|
}],
|
|
openings: [
|
|
{ id: 'grid-door', type: 'door', x: 0.50, y: 0.12, angle: 0, length: 0.16 },
|
|
{ id: 'grid-window', type: 'window', x: 0.90, y: 0.48, angle: 90, length: 0.14 },
|
|
{ id: 'grid-gate', type: 'gate', x: 0.38, y: 0.88, angle: 0, length: 0.22 },
|
|
],
|
|
partitions: [{ id: 'grid-partition', a: [0.28, 0.34], b: [0.72, 0.34], cm: 15 }],
|
|
wall_columns: [{ id: 'grid-column', shape: 'circle', center: [0.72, 0.68], cm: 32 }],
|
|
decor: [{
|
|
id: 'grid-decor', kind: 'line', x1: 0.20, y1: 0.72, x2: 0.58, y2: 0.72,
|
|
color: '#6b4d32', opacity: 0.9, width_cm: 1.2, line_style: 'dashed',
|
|
}],
|
|
};
|
|
const baseLayout = {
|
|
d_light1: { s: 'f1', x: 0.24, y: 0.24 },
|
|
d_lamp: { s: 'f1', x: 0.42, y: 0.54 },
|
|
d_tv: { s: 'f1', x: 0.70, y: 0.54 },
|
|
d_temp: { s: 'f1', x: 0.72, y: 0.24 },
|
|
};
|
|
const sharedLayout = card._layout;
|
|
await card._ensureEditorRuntime();
|
|
const runtime = card._editorRuntime;
|
|
const originalGridLevels = runtime._gridLevels.bind(runtime);
|
|
const scalePoint = (point, factor) => point.map((value) => value * factor);
|
|
const scaledFixture = (cellCm) => {
|
|
const factor = 5 / cellCm;
|
|
const space = structuredClone(base);
|
|
space.cell_cm = cellCm;
|
|
space.view_box = [0, 0, factor, factor];
|
|
space.plan_scale_x = factor;
|
|
space.plan_scale_y = factor;
|
|
// Keep an intentionally transformed backdrop in both fixtures. The 1.25
|
|
// image starts at y=.1; both its offset and that origin scale physically.
|
|
space.plan_x = 0.02 * factor;
|
|
space.plan_y = 0.11 * factor - 0.1;
|
|
space.rooms.forEach((room) => { room.poly = room.poly.map((point) => scalePoint(point, factor)); });
|
|
space.openings.forEach((opening) => {
|
|
opening.x *= factor; opening.y *= factor; opening.length *= factor;
|
|
});
|
|
space.partitions.forEach((partition) => {
|
|
partition.a = scalePoint(partition.a, factor);
|
|
partition.b = scalePoint(partition.b, factor);
|
|
});
|
|
space.wall_columns.forEach((column) => { column.center = scalePoint(column.center, factor); });
|
|
space.decor.forEach((shape) => {
|
|
for (const key of ['x1', 'y1', 'x2', 'y2']) shape[key] *= factor;
|
|
});
|
|
return { space, factor };
|
|
};
|
|
const settle = async () => {
|
|
card.requestUpdate();
|
|
await card.updateComplete;
|
|
for (let index = 0; index < 3; index++) {
|
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
}
|
|
// Room paths deliberately animate between View and Plan styles for 120 ms.
|
|
// Compare settled render states, not two different interpolation frames.
|
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
};
|
|
window.__applyGridScaleFixture = async (cellCm, mode = 'view', projection = 'flat') => {
|
|
const { space, factor } = scaledFixture(cellCm);
|
|
card._serverCfg.spaces.splice(0, card._serverCfg.spaces.length, space);
|
|
card._space = 'f1';
|
|
for (const key of Object.keys(sharedLayout)) delete sharedLayout[key];
|
|
Object.assign(sharedLayout, Object.fromEntries(Object.entries(baseLayout).map(([id, pos]) => [id, {
|
|
...pos, x: pos.x * factor, y: pos.y * factor,
|
|
}])));
|
|
card._layout = sharedLayout;
|
|
card._cfgEpoch++;
|
|
card._modelCache = null;
|
|
card._frame = null;
|
|
card._view = null;
|
|
card._showFar = false;
|
|
card._regSignature = '';
|
|
card._maybeRebuildDevices();
|
|
card._mode = mode;
|
|
card._tool = mode === 'plan' ? 'draw' : null;
|
|
card._decorTool = mode === 'decor' ? 'backdrop' : 'select';
|
|
// Grid density is the one allowed difference. Mask it in every editor
|
|
// raster pair and prove the precision ratio separately below.
|
|
runtime._gridLevels = mode === 'view' ? originalGridLevels : () => null;
|
|
card._gridLevels = () => runtime._gridLevels();
|
|
await window.__hpHarnessProjection(card, projection);
|
|
card._isoFallback.clear();
|
|
card._isoGeometryCache.clear();
|
|
await settle();
|
|
card._fitAll();
|
|
await settle();
|
|
};
|
|
window.__gridScaleMetrics = () => {
|
|
const root = card.renderRoot;
|
|
const number = (value) => Number.parseFloat(value || '0');
|
|
const style = (selector) => {
|
|
const node = root.querySelector(selector);
|
|
return node ? getComputedStyle(node) : null;
|
|
};
|
|
const rect = (selector) => {
|
|
const node = root.querySelector(selector);
|
|
if (!node) return null;
|
|
const box = node.getBoundingClientRect();
|
|
return [box.width, box.height];
|
|
};
|
|
const opening = root.querySelector('[data-id="grid-door"]');
|
|
const outline = opening?.querySelector('.op-outline');
|
|
const hit = opening?.querySelector('.op-hit');
|
|
const leaf = opening?.querySelector('.op-leaf rect');
|
|
return {
|
|
factor: number(style('.stage')?.getPropertyValue('--hp-cell-visual-scale')),
|
|
roomStroke: number(style('.room.styled')?.strokeWidth),
|
|
wallStroke: number(style('.wallbody')?.strokeWidth),
|
|
arcStroke: number(style('.op-arc')?.strokeWidth),
|
|
openingLeafHeight: number(leaf?.getAttribute('height')),
|
|
openingOutlineWidth: number(outline?.getAttribute('width')),
|
|
openingHitWidth: number(hit?.getAttribute('width')),
|
|
openingBox: rect('[data-id="grid-door"]'),
|
|
wallBox: rect('.wallbody'),
|
|
deviceBox: rect('[data-hp="device"]'),
|
|
roomLabelBox: rect('[data-hp="room-label"]'),
|
|
decorBox: rect('[data-hp="decor"][data-id="grid-decor"]'),
|
|
glowBox: rect('.glow-spot'),
|
|
snapNodeBox: rect('.plan-snap-node[data-kind="endpoint"]'),
|
|
backdropHandleBox: rect('.bdhandle'),
|
|
backdropKnobBox: rect('.bdknob'),
|
|
backdropBox: rect('.bdbox'),
|
|
backdropBoxStroke: number(style('.bdbox')?.strokeWidth),
|
|
backdropBoxDash: style('.bdbox')?.strokeDasharray ?? '',
|
|
isoWallBox: rect('[data-hp="iso-walls"]'),
|
|
isoFloorBox: rect('[data-hp="iso-underlay"]'),
|
|
planSnapLineStroke: number(style('.plan-snap-line')?.strokeWidth),
|
|
snapIntervalsPerMeter: 100 / card._cellCm,
|
|
projection: card._effectiveProjection(),
|
|
projectionToggleCount: root.querySelectorAll('[data-hp="projection-toggle"]').length,
|
|
visualScaleNodes: root.querySelectorAll('.room, .wallbody, .opening').length,
|
|
};
|
|
};
|
|
window.__makeStaticGridCard = async () => {
|
|
document.querySelector('#grid-static-host')?.remove();
|
|
const host = document.createElement('div');
|
|
host.id = 'grid-static-host';
|
|
host.style.width = '780px';
|
|
document.body.appendChild(host);
|
|
const compact = document.createElement('houseplan-space-card');
|
|
compact.setConfig({ type: 'custom:houseplan-space-card', space: 'f1', show_button: false });
|
|
compact.hass = card.hass;
|
|
host.appendChild(compact);
|
|
const deadline = Date.now() + 6000;
|
|
while (!compact.renderRoot?.querySelector('.hp-static-stage') && Date.now() < deadline) {
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
}
|
|
while ((compact._loading || !compact._snap) && Date.now() < deadline) {
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
}
|
|
await compact.updateComplete;
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
return {
|
|
config: card._serverCfg,
|
|
layout: sharedLayout,
|
|
revision: card._cfgEpoch,
|
|
};
|
|
};
|
|
});
|
|
|
|
const stage = page.locator('houseplan-card').first().locator('.stage');
|
|
const staticStage = () => page.locator('#grid-static-host houseplan-space-card .hp-static-stage');
|
|
const themeVars = {
|
|
light: {
|
|
'--primary-color': '#0b73b8', '--primary-text-color': '#202124',
|
|
'--secondary-text-color': '#5f6368', '--card-background-color': '#ffffff',
|
|
'--ha-card-background': '#ffffff', '--divider-color': '#d7d9de',
|
|
},
|
|
dark: {
|
|
'--primary-color': '#3ea6ff', '--primary-text-color': '#e6e7eb',
|
|
'--secondary-text-color': '#9aa4ad', '--card-background-color': '#202126',
|
|
'--ha-card-background': '#202126', '--divider-color': '#3a3d45',
|
|
},
|
|
};
|
|
const setTheme = async (theme) => {
|
|
await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: theme });
|
|
await page.evaluate(async ({ variables, dark }) => {
|
|
for (const [name, value] of Object.entries(variables)) {
|
|
document.documentElement.style.setProperty(name, value);
|
|
}
|
|
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
|
|
const card = window.__card;
|
|
card.hass = { ...card.hass, themes: { ...(card.hass.themes || {}), darkMode: dark } };
|
|
await card.updateComplete;
|
|
await document.fonts.ready;
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
}, { variables: themeVars[theme], dark: theme === 'dark' });
|
|
};
|
|
const stableScreenshot = async (locator) => {
|
|
let previous = null;
|
|
for (let attempt = 0; attempt < 6; attempt++) {
|
|
await page.evaluate(async () => {
|
|
await window.__card.updateComplete;
|
|
await document.fonts.ready;
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
});
|
|
const current = await locator.screenshot({ animations: 'disabled' });
|
|
if (previous?.equals(current)) return current;
|
|
previous = current;
|
|
}
|
|
throw new Error('grid scale fixture did not reach two identical consecutive paints');
|
|
};
|
|
const capture = async (cellCm, mode, {
|
|
theme = 'light', projection = 'flat', staticCard = false, hide = [],
|
|
} = {}) => {
|
|
await setTheme(theme);
|
|
await page.evaluate(([cell, nextMode, nextProjection]) =>
|
|
window.__applyGridScaleFixture(cell, nextMode, nextProjection), [cellCm, mode, projection]);
|
|
const metrics = await page.evaluate(() => window.__gridScaleMetrics());
|
|
if (mode === 'decor') {
|
|
// The browser resamples the same SVG backdrop at two user-space sizes with
|
|
// small raster differences. This pair is about Background chrome; the
|
|
// backdrop geometry itself is proved by its shared frame and handle boxes.
|
|
await page.evaluate(() => {
|
|
const backdrop = window.__card.renderRoot.querySelector('.hp-backdrop');
|
|
if (backdrop) backdrop.style.visibility = 'hidden';
|
|
});
|
|
}
|
|
await page.evaluate((selectors) => {
|
|
for (const selector of selectors) {
|
|
for (const node of window.__card.renderRoot.querySelectorAll(selector)) node.style.visibility = 'hidden';
|
|
}
|
|
}, hide);
|
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
const pixels = await stableScreenshot(stage);
|
|
let staticPixels = null;
|
|
if (staticCard) {
|
|
const source = await page.evaluate(() => window.__makeStaticGridCard());
|
|
const patch = coherentGridScaleStaticPatch(source);
|
|
await page.evaluate(async (nextPatch) => {
|
|
const compact = document.querySelector('#grid-static-host houseplan-space-card');
|
|
if (!compact) throw new Error('grid-scale static fixture card disappeared');
|
|
if (!compact._snap) throw new Error('grid-scale static fixture did not load its base snapshot');
|
|
// Preserve runtime-only Set/Map values from the in-page snapshot. Only
|
|
// config+layout cross the Playwright boundary; both come from one scale.
|
|
compact._snap = { ...compact._snap, ...nextPatch };
|
|
compact._loadedOnce = true;
|
|
compact._refreshDevices();
|
|
compact.requestUpdate();
|
|
await compact.updateComplete;
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
}, patch);
|
|
staticPixels = await stableScreenshot(staticStage());
|
|
}
|
|
return { metrics, pixels, staticPixels };
|
|
};
|
|
const near = (left, right, tolerance = 0.35) => Math.abs(left - right) <= tolerance;
|
|
const worldUnitKeys = new Set([
|
|
'roomStroke', 'wallStroke', 'arcStroke', 'openingLeafHeight',
|
|
'openingOutlineWidth', 'openingHitWidth',
|
|
]);
|
|
const metricPairsNear = (left, right) => Object.keys(left).every((key) => {
|
|
if (key === 'factor' || key === 'snapIntervalsPerMeter') return true;
|
|
const a = left[key], b = right[key];
|
|
if (Array.isArray(a) && Array.isArray(b)) return a.every((value, index) => near(value, b[index]));
|
|
if (worldUnitKeys.has(key)) return near(a * (right.factor / left.factor), b, 0.01);
|
|
return typeof a === 'number' && typeof b === 'number' ? near(a, b) : a === b;
|
|
});
|
|
|
|
const pixelDiff = async (left, right) => page.evaluate(async ([a, b]) => {
|
|
const decode = async (base64) => createImageBitmap(await (await fetch(`data:image/png;base64,${base64}`)).blob());
|
|
const [first, second] = await Promise.all([decode(a), decode(b)]);
|
|
if (first.width !== second.width || first.height !== second.height) {
|
|
return {
|
|
sameSize: false,
|
|
firstSize: [first.width, first.height],
|
|
secondSize: [second.width, second.height],
|
|
changed: Infinity,
|
|
maxDelta: Infinity,
|
|
meanDelta: Infinity,
|
|
};
|
|
}
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = first.width; canvas.height = first.height;
|
|
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
context.drawImage(first, 0, 0);
|
|
const firstPixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
context.drawImage(second, 0, 0);
|
|
const secondPixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
let changed = 0, maxDelta = 0, totalDelta = 0;
|
|
let minX = first.width, minY = first.height, maxX = -1, maxY = -1;
|
|
for (let index = 0; index < firstPixels.length; index += 4) {
|
|
let delta = 0;
|
|
for (let channel = 0; channel < 4; channel++) {
|
|
delta = Math.max(delta, Math.abs(firstPixels[index + channel] - secondPixels[index + channel]));
|
|
}
|
|
if (delta > 2) {
|
|
changed++;
|
|
const pixel = index / 4;
|
|
const x = pixel % first.width, y = Math.floor(pixel / first.width);
|
|
minX = Math.min(minX, x); minY = Math.min(minY, y);
|
|
maxX = Math.max(maxX, x); maxY = Math.max(maxY, y);
|
|
}
|
|
maxDelta = Math.max(maxDelta, delta);
|
|
totalDelta += delta;
|
|
}
|
|
return {
|
|
sameSize: true,
|
|
changed,
|
|
maxDelta,
|
|
meanDelta: totalDelta / (firstPixels.length / 4),
|
|
pixels: firstPixels.length / 4,
|
|
changedBounds: changed ? [minX, minY, maxX, maxY] : null,
|
|
};
|
|
}, [left.toString('base64'), right.toString('base64')]);
|
|
|
|
const pair = async (mode, options) => ({
|
|
reference: await capture(5, mode, options),
|
|
detailed: await capture(1, mode, options),
|
|
});
|
|
const referenceView = await capture(5, 'view', { staticCard: true });
|
|
const detailedView = await capture(1, 'view', { staticCard: true });
|
|
const referencePlan = await capture(5, 'plan');
|
|
const detailedPlan = await capture(1, 'plan');
|
|
const darkView = await pair('view', { theme: 'dark' });
|
|
const devices = await pair('devices');
|
|
// Chromium rasterises non-scaling dashed SVG rectangles differently under
|
|
// equivalent user-space transforms. Compare the rest of Background pixel for
|
|
// pixel; the masked box and openings are covered by critical metrics and by
|
|
// the View/Plan raster pairs respectively.
|
|
const background = await pair('decor', { hide: ['.bdbox', '.opening'] });
|
|
const isoLight = await pair('view', { projection: 'iso' });
|
|
const isoDark = await pair('view', { projection: 'iso', theme: 'dark' });
|
|
|
|
const openingEdgeAction = async (cellCm) => {
|
|
await setTheme('light');
|
|
await page.evaluate((cell) => window.__applyGridScaleFixture(cell, 'plan', 'flat'), cellCm);
|
|
const point = await page.evaluate(() => {
|
|
const card = window.__card;
|
|
const hit = card.renderRoot.querySelector('[data-id="grid-door"] .op-hit');
|
|
const box = hit.getBoundingClientRect();
|
|
const x = box.right - 0.75;
|
|
const y = box.top + box.height / 2;
|
|
const stack = card.renderRoot.elementsFromPoint(x, y).slice(0, 5).map((node) => ({
|
|
tag: node.tagName.toLowerCase(),
|
|
class: node.getAttribute('class') || '',
|
|
id: node.closest?.('[data-id]')?.getAttribute('data-id') || '',
|
|
}));
|
|
return { x, y, stack };
|
|
});
|
|
const before = await page.evaluate(() => JSON.stringify(window.__card._serverCfg));
|
|
await page.mouse.click(point.x, point.y);
|
|
await page.evaluate(() => window.__card.updateComplete);
|
|
const ok = await page.evaluate((snapshot) => {
|
|
const card = window.__card;
|
|
const opened = card._openingDialog?.id === 'grid-door';
|
|
card._openingDialog = null;
|
|
card.requestUpdate();
|
|
return opened && JSON.stringify(card._serverCfg) === snapshot;
|
|
}, before);
|
|
return { ok, cellCm, stack: point.stack };
|
|
};
|
|
|
|
const backgroundCancel = async (cellCm) => {
|
|
await setTheme('light');
|
|
await page.evaluate((cell) => window.__applyGridScaleFixture(cell, 'decor', 'flat'), cellCm);
|
|
return page.evaluate(async () => {
|
|
const card = window.__card;
|
|
const root = card.renderRoot;
|
|
const handle = root.querySelector('.bdhandle[data-corner="1,1"]');
|
|
const stageNode = root.querySelector('.stage');
|
|
const box = handle?.getBoundingClientRect();
|
|
if (!handle || !stageNode || !box) return false;
|
|
const before = structuredClone(card._serverCfg);
|
|
handle.dispatchEvent(new PointerEvent('pointerdown', {
|
|
bubbles: true, composed: true, cancelable: true, pointerId: 239,
|
|
pointerType: 'touch', clientX: box.left + box.width / 2, clientY: box.top + box.height / 2,
|
|
}));
|
|
const startedPid = card._bdDrag?.pid ?? null;
|
|
const cancelEvent = new PointerEvent('pointercancel', {
|
|
pointerId: 239, pointerType: 'touch',
|
|
});
|
|
card._stagePointerCancel(cancelEvent);
|
|
await card.updateComplete;
|
|
const after = structuredClone(card._serverCfg);
|
|
const canonical = (value) => {
|
|
if (Array.isArray(value)) return value.map(canonical);
|
|
if (value && typeof value === 'object') return Object.fromEntries(
|
|
Object.keys(value).sort().map((key) => [key, canonical(value[key])]),
|
|
);
|
|
return value;
|
|
};
|
|
const changedKeys = [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
.filter((key) => JSON.stringify(canonical(before[key])) !== JSON.stringify(canonical(after[key])));
|
|
return {
|
|
ok: startedPid === 239 && cancelEvent.pointerId === 239
|
|
&& !card._bdDrag && changedKeys.length === 0,
|
|
startedPid, cancelPointerId: cancelEvent.pointerId,
|
|
dragEnded: !card._bdDrag,
|
|
configSame: changedKeys.length === 0,
|
|
changedKeys,
|
|
};
|
|
});
|
|
};
|
|
|
|
const referenceOpeningEdgeActs = await openingEdgeAction(5);
|
|
const detailedOpeningEdgeActs = await openingEdgeAction(1);
|
|
const referenceBackgroundCancel = await backgroundCancel(5);
|
|
const detailedBackgroundCancel = await backgroundCancel(1);
|
|
const flatDiff = await pixelDiff(referenceView.pixels, detailedView.pixels);
|
|
const staticDiff = await pixelDiff(referenceView.staticPixels, detailedView.staticPixels);
|
|
const planDiff = await pixelDiff(referencePlan.pixels, detailedPlan.pixels);
|
|
const darkViewDiff = await pixelDiff(darkView.reference.pixels, darkView.detailed.pixels);
|
|
const devicesDiff = await pixelDiff(devices.reference.pixels, devices.detailed.pixels);
|
|
const backgroundDiff = await pixelDiff(background.reference.pixels, background.detailed.pixels);
|
|
const isoLightDiff = await pixelDiff(isoLight.reference.pixels, isoLight.detailed.pixels);
|
|
const isoDarkDiff = await pixelDiff(isoDark.reference.pixels, isoDark.detailed.pixels);
|
|
const pixelEquivalent = (diff) => diff.sameSize
|
|
&& diff.changed <= 150 && diff.maxDelta <= 40 && diff.meanDelta <= 0.05;
|
|
|
|
// Diagnostic only: the raw diff numbers behind every pixel verdict, so a CI
|
|
// failure reports HOW FAR a pair drifted instead of a bare boolean (#302).
|
|
console.error('pixel-diffs', JSON.stringify({
|
|
flat: flatDiff, static: staticDiff, plan: planDiff, darkView: darkViewDiff,
|
|
devices: devicesDiff, background: backgroundDiff,
|
|
isoLight: isoLightDiff, isoDark: isoDarkDiff,
|
|
}));
|
|
const out = {
|
|
referenceFactorIsOne: referenceView.metrics.factor === 1,
|
|
detailedFactorIsFive: detailedView.metrics.factor === 5,
|
|
flatCriticalMetricsMatch: metricPairsNear(referenceView.metrics, detailedView.metrics),
|
|
flatPixelsMatch: pixelEquivalent(flatDiff),
|
|
staticPixelsMatch: pixelEquivalent(staticDiff),
|
|
planCriticalMetricsMatch: metricPairsNear(referencePlan.metrics, detailedPlan.metrics),
|
|
planPixelsMatchWithEquivalentGridHidden: pixelEquivalent(planDiff),
|
|
snapPrecisionIsFiveTimesFiner: detailedPlan.metrics.snapIntervalsPerMeter
|
|
=== referencePlan.metrics.snapIntervalsPerMeter * 5,
|
|
screenFixedPlanSnapStrokeIsNotDoubleScaled: referencePlan.metrics.planSnapLineStroke === 1
|
|
&& detailedPlan.metrics.planSnapLineStroke === 1,
|
|
openingEdgeHitAndActionMatch: referenceOpeningEdgeActs.ok && detailedOpeningEdgeActs.ok,
|
|
darkViewCriticalMetricsMatch: metricPairsNear(darkView.reference.metrics, darkView.detailed.metrics),
|
|
darkViewPixelsMatch: pixelEquivalent(darkViewDiff),
|
|
devicesCriticalMetricsMatch: metricPairsNear(devices.reference.metrics, devices.detailed.metrics),
|
|
devicesPixelsMatch: pixelEquivalent(devicesDiff),
|
|
backgroundCriticalMetricsMatch: metricPairsNear(background.reference.metrics, background.detailed.metrics),
|
|
backgroundPixelsMatch: pixelEquivalent(backgroundDiff),
|
|
backgroundPointerCancelIsMutationFree: referenceBackgroundCancel.ok && detailedBackgroundCancel.ok,
|
|
isoLightCriticalMetricsMatch: metricPairsNear(isoLight.reference.metrics, isoLight.detailed.metrics),
|
|
isoLightPixelsMatch: pixelEquivalent(isoLightDiff),
|
|
isoDarkCriticalMetricsMatch: metricPairsNear(isoDark.reference.metrics, isoDark.detailed.metrics),
|
|
isoDarkPixelsMatch: pixelEquivalent(isoDarkDiff),
|
|
// #649: 2.5D is the General settings switch; the card never shows a toggle.
|
|
isoHasNoCardToggle: referenceView.metrics.projectionToggleCount === 0
|
|
&& isoLight.reference.metrics.projection === 'iso'
|
|
&& isoLight.reference.metrics.projectionToggleCount === 0,
|
|
};
|
|
|
|
if (Object.values(out).some((value) => !value)) {
|
|
console.error('grid scale evidence:', JSON.stringify({
|
|
referenceView: referenceView.metrics,
|
|
detailedView: detailedView.metrics,
|
|
referencePlan: referencePlan.metrics,
|
|
detailedPlan: detailedPlan.metrics,
|
|
darkView: { reference: darkView.reference.metrics, detailed: darkView.detailed.metrics },
|
|
devices: { reference: devices.reference.metrics, detailed: devices.detailed.metrics },
|
|
background: { reference: background.reference.metrics, detailed: background.detailed.metrics },
|
|
isoLight: { reference: isoLight.reference.metrics, detailed: isoLight.detailed.metrics },
|
|
isoDark: { reference: isoDark.reference.metrics, detailed: isoDark.detailed.metrics },
|
|
flatDiff, staticDiff, planDiff, darkViewDiff, devicesDiff, backgroundDiff,
|
|
isoLightDiff, isoDarkDiff,
|
|
referenceBackgroundCancel, detailedBackgroundCancel,
|
|
referenceOpeningEdgeActs, detailedOpeningEdgeActs,
|
|
}, null, 2));
|
|
}
|
|
|
|
checkAll(out);
|
|
await finish(browser, out);
|