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
116 lines
5.2 KiB
JavaScript
116 lines
5.2 KiB
JavaScript
/** #278: degraded canonical components render everywhere; strict writes do not persist. */
|
|
import { readFileSync } from 'node:fs';
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
|
|
const fixture = JSON.parse(readFileSync(
|
|
new URL('../test/fixtures/278-wall-union-isolation.json', import.meta.url), 'utf8',
|
|
));
|
|
const { page, browser } = await launch({ width: 1000, height: 780 }, 1);
|
|
|
|
const result = await page.evaluate(async (fixtureConfig) => {
|
|
const out = {};
|
|
const card = window.__card;
|
|
const root = () => card.renderRoot || card.shadowRoot;
|
|
const frame = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
const settle = async () => { await card.updateComplete; await frame(); };
|
|
const cfg = structuredClone(fixtureConfig);
|
|
cfg.spaces[0].settings = {
|
|
...(cfg.spaces[0].settings || {}), show_borders: true, fill_mode: 'none',
|
|
};
|
|
card._serverCfg = cfg;
|
|
card._layout = {};
|
|
card._space = cfg.spaces[0].id;
|
|
card._cfgEpoch++;
|
|
card._modelCache = null;
|
|
card._frame = null;
|
|
card._wallUnionCache = null;
|
|
card._physicalBodiesCache = null;
|
|
card._lightBarrierCache = null;
|
|
card._setMode('plan');
|
|
card._tool = 'select';
|
|
card.requestUpdate();
|
|
await settle();
|
|
|
|
const sourceBeforeRender = JSON.stringify(card._serverCfg.spaces[0]);
|
|
const canonical = card._wallUnionGeometry();
|
|
out.typedDegradedResult = canonical?.status === 'degraded-extra';
|
|
out.planRendersBothComponents = root().querySelectorAll('.wallbody[data-component]').length === 2
|
|
&& root().querySelectorAll('.wallbody-fill[data-component]').length === 2;
|
|
const planPaths = [...root().querySelectorAll('.wallbody[data-component]')]
|
|
.map((path) => path.getAttribute('d')).sort();
|
|
const model = card._spaceModel();
|
|
const polys = model.rooms.map((room) => ({ r: room, poly: room.poly }));
|
|
const barriers = card._lightBarriers(model, polys, card._physicalBodiesR(model));
|
|
out.lightUsesBothComponents = barriers.masonryGeometry.length
|
|
=== canonical.components.reduce((sum, component) => sum + component.geom.length, 0)
|
|
&& barriers.occluders.length > 0;
|
|
|
|
card._setMode('view');
|
|
card.requestUpdate();
|
|
await settle();
|
|
out.viewMatchesPlan = JSON.stringify([...root().querySelectorAll('.wallbody[data-component]')]
|
|
.map((path) => path.getAttribute('d')).sort()) === JSON.stringify(planPaths);
|
|
|
|
await customElements.whenDefined('houseplan-space-card');
|
|
const staticCard = document.createElement('houseplan-space-card');
|
|
staticCard.setConfig({
|
|
type: 'custom:houseplan-space-card', space: cfg.spaces[0].id, show_button: false,
|
|
});
|
|
const baseCall = card.hass.callWS.bind(card.hass);
|
|
staticCard.hass = { ...card.hass, callWS: async (message) => {
|
|
if (message.type === 'houseplan/config/get') return { config: structuredClone(cfg), rev: 1 };
|
|
if (message.type === 'houseplan/layout/get') return { layout: {}, rev: 1 };
|
|
return baseCall(message);
|
|
} };
|
|
document.body.appendChild(staticCard);
|
|
const started = Date.now();
|
|
while (staticCard.renderRoot?.querySelectorAll('.wallbody[data-component]').length !== 2
|
|
&& Date.now() - started < 6000) await new Promise((resolve) => setTimeout(resolve, 50));
|
|
await staticCard.updateComplete;
|
|
out.staticMatchesPlan = JSON.stringify([...staticCard.renderRoot.querySelectorAll(
|
|
'.wallbody[data-component]',
|
|
)].map((path) => path.getAttribute('d')).sort()) === JSON.stringify(planPaths);
|
|
staticCard.remove();
|
|
await window.__hpHarnessProjection(card, 'iso');
|
|
await window.__hpEnsureHarnessIsoRuntime(card);
|
|
card.requestUpdate();
|
|
await settle();
|
|
const iso = card._isoSource().build();
|
|
out.hiddenIsoUsesBothComponents = iso.walls.length
|
|
=== canonical.components.reduce((sum, component) => sum + component.geom.length, 0)
|
|
&& !!root().querySelector('[data-hp="iso-walls"]');
|
|
out.renderNeverWrites = JSON.stringify(card._serverCfg.spaces[0]) === sourceBeforeRender;
|
|
|
|
let writes = 0;
|
|
card.hass = { ...card.hass, callWS: async (message) => {
|
|
if (message.type === 'houseplan/config/set') { writes++; return { rev: 9 }; }
|
|
return baseCall(message);
|
|
} };
|
|
await window.__hpHarnessProjection(card, 'flat');
|
|
card._setMode('plan');
|
|
await settle();
|
|
card._geometryHistory.clear();
|
|
const before = card._geometrySnapshot();
|
|
const beforeJson = JSON.stringify(before);
|
|
card._curSpaceCfg.walls[0].cm += 1;
|
|
const committed = card._commitPhysicalGeometry('unsafe test', before);
|
|
await settle();
|
|
out.degradedPhysicalEditRejected = committed === false
|
|
&& JSON.stringify(card._geometrySnapshot()) === beforeJson;
|
|
out.rejectedEditHasNoHistoryOrWrite = card._geometryHistory.size === 0 && writes === 0;
|
|
out.rejectedEditHasLocalizedToast = card._toast === card._t('toast.geometry_unsafe');
|
|
|
|
let strictChecks = 0;
|
|
const strictCheck = card._checkSpacePhysicalGeometry.bind(card);
|
|
card._checkSpacePhysicalGeometry = (...args) => { strictChecks++; return strictCheck(...args); };
|
|
card._curSpaceCfg.title = 'Non geometry edit';
|
|
card._saveConfig();
|
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
out.nonGeometryEditBypassesStrictBarrier = strictChecks === 0 && writes === 1;
|
|
card._checkSpacePhysicalGeometry = strictCheck;
|
|
return out;
|
|
}, fixture.config);
|
|
|
|
checkAll(result);
|
|
await finish(browser, result);
|