Files
houseplan-card/demo/smoke_wall_union_isolation.mjs
T
Claude e5ab4659f6 feat: 2.5D stage 6 — public setting, raised tiles, soft sun, theme-free walls (#649)
- 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
2026-09-25 18:55:14 +03:00

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);