Files
houseplan-card/demo/smoke_isometric_contract.mjs
2026-09-26 03:41:32 +03:00

576 lines
28 KiB
JavaScript

// #122/#160/#570 Stage 4 + #649: settings lifecycle, visual handoff and flat editor boundary.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 1000, height: 850 });
const out = await page.evaluate(async () => {
const card = window.__card;
const root = () => card.renderRoot;
const frame = () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
const isoRuntimeRequests = () => [...performance.getEntriesByType('resource')]
.filter((entry) => /\/iso-scene-render-[^/]+\.js(?:$|\?)/.test(entry.name)).length;
const ensureIsoRuntime = async () => {
if (typeof card._ensureIsoSceneRuntime !== 'function'
|| !(await card._ensureIsoSceneRuntime())) {
throw new Error('isometric smoke runtime did not load');
}
await card.updateComplete;
await frame();
};
const center = (element) => {
const rect = element.getBoundingClientRect();
return [rect.left + rect.width / 2, rect.top + rect.height / 2];
};
const inside = (outer, element, tolerance = 1) => {
const a = outer.getBoundingClientRect(), b = element.getBoundingClientRect();
return b.left >= a.left - tolerance && b.top >= a.top - tolerance
&& b.right <= a.right + tolerance && b.bottom <= a.bottom + tolerance;
};
const sceneToClient = (point) => {
const svg = root().querySelector('.iso-overlays-svg');
if (!svg) return [NaN, NaN];
const rect = svg.getBoundingClientRect(), vb = svg.viewBox.baseVal;
const scale = Math.min(rect.width / vb.width, rect.height / vb.height);
return [rect.left + (rect.width - vb.width * scale) / 2 + (point[0] - vb.x) * scale,
rect.top + (rect.height - vb.height * scale) / 2 + (point[1] - vb.y) * scale];
};
const owns44 = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect(), c = center(element);
const pseudo = getComputedStyle(element, '::before');
const width = Math.max(rect.width, Number.parseFloat(pseudo.width) || 0);
const height = Math.max(rect.height, Number.parseFloat(pseudo.height) || 0);
if (width < 44 || height < 44) return false;
return [[0, 0], [-21, 0], [21, 0], [0, -21], [0, 21]].every(([dx, dy]) => {
const hit = root().elementFromPoint(c[0] + dx, c[1] + dy);
return hit === element || !!hit && element.contains(hit);
});
};
// #649: 2.5D is the installation-wide General settings switch.
// The switch arrives as a server config push: re-bind the space the smoke edits.
const rebind = () => { configSpace = card._serverCfg.spaces.find((space) => space.id === 'f1'); };
const enable = async () => {
await window.__hpTest.setVolumetricView(true);
rebind();
await ensureIsoRuntime();
await frame();
};
const result = {};
result.cleanProfileStartsOff = window.__hpAlpha === false
&& JSON.stringify(window.__hpLabs) === '[]'
&& !root().querySelector('[data-hp="projection-toggle"]')
&& ![...root().querySelector('.stage').classList]
.some((name) => name.startsWith('projection-'));
result.cleanAlphaOffSkipsIsoRuntimeRequest = isoRuntimeRequests() === 0;
let configSpace = card._serverCfg.spaces.find((space) => space.id === 'f1');
configSpace.settings = {
...(configSpace.settings || {}), show_borders: true, show_names: true,
};
configSpace.partitions = [{
id: 'iso-smoke-wall', a: [0.15, 0.12], b: [0.85, 0.12], cm: 15,
}];
configSpace.openings = [
{ id: 'iso-centred-door', type: 'door', x: 0.325, y: 0.12, angle: 0, length: 0.08,
lock: 'lock.front_door',
host: { kind: 'partition', id: 'iso-smoke-wall', t: 0.25 } },
{ id: 'iso-flipped-window', type: 'window', x: 0.5, y: 0.12, angle: 0, length: 0.08,
flip_v: true, host: { kind: 'partition', id: 'iso-smoke-wall', t: 0.5 } },
{ id: 'iso-centred-gate', type: 'gate', x: 0.605, y: 0.12, angle: 0, length: 0.08,
flip_v: false, host: { kind: 'partition', id: 'iso-smoke-wall', t: 0.65 } },
{ id: 'iso-flipped-gate', type: 'gate', x: 0.745, y: 0.12, angle: 0, length: 0.08,
flip_v: true, host: { kind: 'partition', id: 'iso-smoke-wall', t: 0.85 } },
];
card._cfgEpoch++;
card.requestUpdate();
await card.updateComplete;
// #649 AC11: the alpha switch no longer enables 2.5D and loads nothing.
history.replaceState(null, '', '#space=f1&hp_alpha=1');
dispatchEvent(new HashChangeEvent('hashchange'));
await card.updateComplete;
await frame();
result.alphaDoesNotEnableIso = window.__hpAlpha === true && JSON.stringify(window.__hpLabs) === '[]'
&& !root().querySelector('[data-hp="iso-walls"]') && isoRuntimeRequests() === 0;
history.replaceState(null, '', '#space=f1&hp_alpha=0');
dispatchEvent(new HashChangeEvent('hashchange'));
await card.updateComplete;
await enable();
result.settingLoadsIsoRuntimeOnce = isoRuntimeRequests() === 1;
const openingBases = card._isoSource()?.build().openings || [];
const basis = (id) => openingBases.find((opening) => opening.id === id);
const centredDoor = basis('iso-centred-door');
const flippedWindow = basis('iso-flipped-window');
const centredGate = basis('iso-centred-gate');
const flippedGate = basis('iso-flipped-gate');
const wallAxisY = 0.12 * 1000;
const samePoint = (a, b) => !!a && !!b
&& Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
const leavesUseSelectedFace = (opening) => !!opening
&& opening.leaves.every((leaf) => samePoint(
leaf.hinge,
leaf.leaf === 0 ? opening.face.selectedStart : opening.face.selectedEnd,
));
result.isoDoorUsesSelectedHostFace = centredDoor?.leaves.length === 1
&& leavesUseSelectedFace(centredDoor)
&& Math.abs(centredDoor.leaves[0].hinge[1] - wallAxisY) > 1e-6;
result.isoWindowFlipStaysCentred = flippedWindow?.leaves.length === 2
&& flippedWindow.leaves.every(
(leaf) => Math.abs(leaf.hinge[1] - wallAxisY) < 1e-6,
);
result.isoGateFlipUsesSelectedHostFace = centredGate?.leaves.length === 2
&& leavesUseSelectedFace(centredGate)
&& flippedGate?.leaves.length === 2 && leavesUseSelectedFace(flippedGate)
&& Math.sign(centredGate.face.offset[1]) === -Math.sign(flippedGate.face.offset[1])
&& centredGate.leaves.every((leaf) => Math.abs(leaf.turnDeg) === 10)
&& flippedGate.leaves.every((leaf) => Math.abs(leaf.turnDeg) === 10);
result.isoGateFlipReversesTurn = centredGate?.leaves[0]?.turnDeg
=== -flippedGate?.leaves[0]?.turnDeg;
result.settingStored = card._serverCfg.settings?.volumetric_view === true;
result.noCardToggle = !root().querySelector('[data-hp="projection-toggle"]');
const device = root().querySelector('[data-hp="device"]');
const roomLabel = root().querySelector('[data-hp="room-label"]');
const openingLock = root().querySelector('.oplock');
const stage = root().querySelector('.stage');
result.isoRendered = !!root().querySelector('[data-hp="iso-underlay"] .iso-floor-side')
&& !!root().querySelector('[data-hp="iso-walls"] .iso-wall-top');
result.stage4RevisionAdvertised = stage?.getAttribute('data-hp-iso-stage') === '4';
result.sharedProjectionSnapshot = [
'.iso-underlay-svg', '.plan-svg', '.iso-shadows-svg', '.iso-walls-svg', '.iso-overlays-svg',
]
.map((selector) => root().querySelector(selector)?.getAttribute('viewBox'))
.every((value, _index, values) => !!value && value === values[0]);
const materialDefs = [...root().querySelectorAll('[data-hp-iso-material-def]')];
const requiredDefs = [
'hp-iso-wall-side', 'hp-iso-wall-top', 'hp-iso-wall-texture',
'hp-iso-floor-texture', 'hp-iso-ambient-shadow',
];
result.stage4DefinitionsBounded = materialDefs.length >= requiredDefs.length
&& materialDefs.length <= 16
&& new Set(materialDefs.map((node) => node.id)).size === materialDefs.length
&& requiredDefs.every((id) => materialDefs.some((node) => node.id === id));
const shadowNodes = [...root().querySelectorAll('.iso-ambient-shadow')];
const fixedLightTransform = card._isoSceneRuntime.isoFixedLightTransform(card._cellCm);
result.fixedLightVectorShared = shadowNodes.length === 1
&& shadowNodes[0].getAttribute('transform') === fixedLightTransform
&& !root().querySelector('.iso-contact-shadow, .iso-leaf-shadow,'
+ ' #hp-iso-contact-shadow, #hp-iso-leaf-shadow');
const hasOpeningSurface = (id, surface, material) => !!root().querySelector(
`[data-hp="iso-openings"] [data-id="${id}"][data-surface="${surface}"].iso-material-${material}`,
);
result.stage4OpeningMaterials = [
['iso-centred-door', 'jamb-reveal', 'reveal'],
['iso-centred-door', 'leaf-front', 'matte-leaf'],
['iso-centred-door', 'leaf-back', 'matte-leaf'],
['iso-centred-door', 'leaf-edge', 'matte-leaf'],
['iso-centred-door', 'leaf-top', 'matte-leaf'],
['iso-flipped-window', 'window-frame-side', 'light-frame'],
['iso-flipped-window', 'window-frame-top', 'light-frame'],
['iso-flipped-window', 'window-sill', 'light-sill'],
['iso-flipped-window', 'window-sash-side', 'light-frame'],
['iso-flipped-window', 'window-sash-bottom', 'light-frame'],
['iso-flipped-window', 'window-sash-top', 'light-frame'],
['iso-flipped-window', 'window-glass', 'glass-side'],
['iso-flipped-window', 'window-glass-top', 'glass-top'],
['iso-centred-gate', 'jamb-reveal', 'reveal'],
['iso-centred-gate', 'leaf-front', 'matte-leaf'],
].every(([id, surface, material]) => hasOpeningSurface(id, surface, material))
&& !root().querySelector('[data-hp="iso-openings"] .iso-material-dark-glass');
const raisedRoot = (node, kind) => node?.getAttribute('data-hp-iso-overlay-kind') === kind
&& node.getAttribute('data-hp-iso-raised') === 'true'
&& node.getAttribute('data-hp-iso-floor')
&& node.getAttribute('data-hp-iso-visual')
&& node.getAttribute('data-hp-iso-floor') !== node.getAttribute('data-hp-iso-visual');
result.stage4UsesExactLowInteractiveRoots = raisedRoot(device, 'device')
&& raisedRoot(roomLabel, 'room-label')
&& raisedRoot(openingLock, 'opening-lock')
&& !root().querySelector('.iso-overlay-tether, .iso-overlay-ground, #hp-iso-overlay-ground');
result.raisedFootprintsStayInvisible = !root().querySelector(
'.iso-overlay-plate, .iso-overlay-plate-texture, #hp-iso-overlay-texture',
);
result.noDevicePreference = localStorage.getItem('houseplan_card_view_v1') === null;
result.anchorsFinite = [device, roomLabel, openingLock].every((node) => node
&& center(node).every((value) => Number.isFinite(value)));
card._fitAll();
for (let guard = 0; card._cameraTransition.active && guard < 90; guard++) await frame();
const raisedNodes = [...root().querySelectorAll('[data-hp-iso-raised="true"]')]
.filter((node) => node.closest('.devlayer'));
const raisedGeometryMatches = (node) => {
const visual = (node.getAttribute('data-hp-iso-visual') || '').split(',').map(Number);
const floorPoint = (node.getAttribute('data-hp-iso-floor') || '').split(',').map(Number);
if (visual.length !== 2 || floorPoint.length !== 2) return false;
const expected = sceneToClient(visual), actual = center(node);
return Math.hypot(actual[0] - expected[0], actual[1] - expected[1]) <= 1
&& Math.hypot(floorPoint[0] - visual[0], floorPoint[1] - visual[1]) > 0;
};
result.globalFitContainsRaisedFootprints = raisedNodes.length >= 3
&& raisedNodes.every((node) => inside(root().querySelector('.stage'), node));
result.raisedGeometryTracksHtml = raisedNodes.every(raisedGeometryMatches);
result.raisedTargetsOwn44Pixels = ['device', 'room-label', 'opening-lock'].every((kind) =>
raisedNodes.filter((node) => node.getAttribute('data-hp-iso-overlay-kind') === kind).some(owns44));
const overlaySceneSnapshot = () => [...root().querySelectorAll(
'[data-hp-iso-raised="true"][data-hp-iso-visual]',
)].map((node) => [
node.getAttribute('data-hp-iso-overlay-kind'),
node.getAttribute('data-hp-iso-floor'),
node.getAttribute('data-hp-iso-visual'),
].join('|')).sort();
const overlaysBeforeZoom = overlaySceneSnapshot();
card._stepZoom(1);
for (let guard = 0; card._cameraTransition.active && guard < 90; guard++) await frame();
const overlaysAfterZoomIn = overlaySceneSnapshot();
card._stepZoom(-1);
for (let guard = 0; card._cameraTransition.active && guard < 90; guard++) await frame();
result.desktopZoomKeepsOverlayScene = overlaysBeforeZoom.length >= 3
&& JSON.stringify(overlaysAfterZoomIn) === JSON.stringify(overlaysBeforeZoom)
&& JSON.stringify(overlaySceneSnapshot()) === JSON.stringify(overlaysBeforeZoom);
const deviceBefore = device ? center(device) : null;
const fingerprintBeforeLive = root().querySelector('[data-hp="iso-walls"]')?.dataset.fingerprint;
const geometryBeforeLive = card._isoGeometryCache.get(fingerprintBeforeLive)?.geometry;
const buildsBeforeLive = Number(stage?.getAttribute('data-hp-iso-structural-builds'));
const sourceMethod = card._isoSource;
const sceneMethod = card._isoScene;
let sourceCalls = 0;
let sceneCalls = 0;
card._isoSource = (...args) => { sourceCalls++; return sourceMethod.apply(card, args); };
card._isoScene = (...args) => { sceneCalls++; return sceneMethod.apply(card, args); };
card.hass = { ...card.hass, states: { ...card.hass.states } };
card.requestUpdate();
await card.updateComplete;
result.oneStructuralResolvePerFrame = sourceCalls === 1 && sceneCalls === 1
&& Number(root().querySelector('.stage')?.getAttribute('data-hp-iso-structural-builds'))
=== buildsBeforeLive;
card._isoSource = sourceMethod;
card._isoScene = sceneMethod;
const deviceAfter = root().querySelector('[data-hp="device"]');
result.haUpdateKeepsGeometry = root().querySelector('[data-hp="iso-walls"]')?.dataset.fingerprint
&& card._isoGeometryCache.size === 1;
result.haUpdateKeepsAnchor = !!deviceBefore && !!deviceAfter
&& Math.hypot(...center(deviceAfter).map((value, index) => value - deviceBefore[index])) <= 1;
const room = card._spaceModel('f1')?.rooms?.[0];
card._hoverRoom = room ? { space: 'f1', room } : null;
card.requestUpdate();
await card.updateComplete;
await frame();
const fingerprintAfterHover = root().querySelector('[data-hp="iso-walls"]')?.dataset.fingerprint;
const buildsAfterHover = Number(root().querySelector('.stage')
?.getAttribute('data-hp-iso-structural-builds'));
result.haAndHoverReuseStructuralScene = !!fingerprintBeforeLive
&& fingerprintAfterHover === fingerprintBeforeLive
&& Number.isFinite(buildsBeforeLive) && buildsAfterHover === buildsBeforeLive
&& card._isoGeometryCache.get(fingerprintBeforeLive)?.geometry === geometryBeforeLive
&& card._isoGeometryCache.size === 1;
card._hoverRoom = null;
card.requestUpdate();
await card.updateComplete;
const originalRoomConfig = configSpace.rooms[0];
const roomSourceBeforePresentation = card._isoSource();
const roomGeometryBeforePresentation = card._isoGeometryCache
.get(roomSourceBeforePresentation.key)?.geometry;
const buildsBeforeRoomPresentation = Number(root().querySelector('.stage')
?.getAttribute('data-hp-iso-structural-builds'));
const presentationRoomConfig = {
...originalRoomConfig,
name: `${originalRoomConfig.name} presentation only`,
area: 'presentation_only_area',
settings: {
...(originalRoomConfig.settings || {}),
fill_mode: 'custom', custom_fill: { c: '#123456', a: 0.4 },
glow: false, name_scale: 1.7, label_scale: 0.8,
},
};
configSpace.rooms[0] = presentationRoomConfig;
card._cfgEpoch++;
card.requestUpdate();
await card.updateComplete;
await frame();
const roomSourceAfterPresentation = card._isoSource();
const buildsAfterRoomPresentation = Number(root().querySelector('.stage')
?.getAttribute('data-hp-iso-structural-builds'));
result.presentationRoomUpdateKeepsStructuralCache =
roomSourceAfterPresentation.key === roomSourceBeforePresentation.key
&& buildsAfterRoomPresentation === buildsBeforeRoomPresentation
&& card._isoGeometryCache.get(roomSourceAfterPresentation.key)?.geometry
=== roomGeometryBeforePresentation;
const geometryRoomConfig = {
...presentationRoomConfig,
poly: presentationRoomConfig.poly.map((point, index) =>
index === 1 ? [point[0] + 0.01, point[1]] : [...point]),
};
configSpace.rooms[0] = geometryRoomConfig;
card._cfgEpoch++;
card.requestUpdate();
await card.updateComplete;
await frame();
const roomSourceAfterGeometry = card._isoSource();
const buildsAfterRoomGeometry = Number(root().querySelector('.stage')
?.getAttribute('data-hp-iso-structural-builds'));
result.roomGeometryUpdateInvalidatesStructuralCache =
roomSourceAfterGeometry.key !== roomSourceAfterPresentation.key
&& buildsAfterRoomGeometry === buildsAfterRoomPresentation + 1
&& card._isoGeometryCache.get(roomSourceAfterGeometry.key)?.geometry
!== roomGeometryBeforePresentation;
configSpace.rooms[0] = originalRoomConfig;
card._cfgEpoch++;
card.requestUpdate();
await card.updateComplete;
await frame();
const roomFitLabel = root().querySelector('.roomlabel[data-id="r1"]');
roomFitLabel?.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Enter', bubbles: true, composed: true,
}));
for (let guard = 0; card._cameraTransition.active && guard < 90; guard++) await frame();
await card.updateComplete;
const ownedEntries = (card._renderIsoScene?.overlayFitEntries || [])
.filter((entry) => entry.placement.owner?.id === 'r1');
const ownedRoots = ownedEntries.map((entry) => entry.kind === 'device'
? root().querySelector(`.dev[data-id="${CSS.escape(entry.id)}"]`)
: entry.kind === 'room-label'
? root().querySelector(`.roomlabel[data-id="${CSS.escape(entry.id)}"]`)
: root().querySelector('.oplock')).filter(Boolean);
result.roomFitContainsOwnedRaisedFootprints = ownedEntries.length >= 2
&& ownedRoots.length === ownedEntries.length
&& ownedRoots.every((node) => inside(root().querySelector('.stage'), node));
card._fitAll();
for (let guard = 0; card._cameraTransition.active && guard < 90; guard++) await frame();
const originalMoreInfo = card._openMoreInfo;
let moreInfoCalls = 0;
card._openMoreInfo = () => { moreInfoCalls++; };
const sensorDevice = root().querySelector('.dev[data-id="d_temp"]');
sensorDevice?.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true, composed: true, button: 2,
}));
sensorDevice?.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Enter', bubbles: true, composed: true,
}));
const keyboardOpenedDeviceCard = card._infoCard?.id === 'd_temp';
card._closeInfoCard();
card._openMoreInfo = originalMoreInfo;
const areaLink = root().querySelector('.roomlabel[data-id="r1"] .rlgo');
areaLink?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
const areaPath = location.pathname;
history.replaceState(null, '', '/#space=f1');
const lockRoot = root().querySelector('.oplock');
lockRoot?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
result.raisedActionsRemainOwned = moreInfoCalls === 1 && keyboardOpenedDeviceCard
&& areaPath === '/config/areas/area/living_room'
&& card._openingInfo?.id === 'iso-centred-door';
card._openingInfo = null;
card._setMode('plan');
await card.updateComplete;
while (card._modeTransitionBusy) await frame();
result.editorIsFlat = card._mode === 'plan'
&& !root().querySelector('[data-hp="projection-toggle"]')
&& !root().querySelector('[data-hp="iso-walls"]')
&& ![...root().querySelector('.stage').classList]
.some((name) => name.startsWith('projection-'));
card._setMode('view');
await card.updateComplete;
while (card._modeTransitionBusy) await frame();
result.viewRestoresIso = !!root().querySelector('.stage.projection-iso')
&& !!root().querySelector('[data-hp="iso-walls"]');
const nativeCssSupports = CSS.supports;
const fallbackCountBeforeCapabilityProbe = card._isoFallback.size;
CSS.supports = () => { throw new Error('injected decoration capability failure'); };
card.requestUpdate();
await card.updateComplete;
await frame();
result.throwingCapabilityProbeStaysSolidIso = card._effectiveProjection() === 'iso'
&& card._isoFallback.size === fallbackCountBeforeCapabilityProbe
&& !!root().querySelector('[data-hp="iso-walls"] .iso-wall-top')
&& !!root().querySelector('[data-hp="iso-openings"] .iso-opening-panel')
&& !root().querySelector('.iso-overlay-plate, .iso-overlay-plate-texture')
&& !root().querySelector('.iso-overlay-tether, .iso-overlay-ground, #hp-iso-overlay-ground')
&& !root().querySelector('[data-hp-iso-material-def]')
&& !root().querySelector('.iso-ambient-shadow');
CSS.supports = nativeCssSupports;
card.requestUpdate();
await card.updateComplete;
await frame();
result.capabilityProbeRecoveryRestoresNuance = card._effectiveProjection() === 'iso'
&& !!root().querySelector('[data-hp-iso-material-def]')
&& card._isoFallback.size === fallbackCountBeforeCapabilityProbe;
const healthySource = card._isoSource;
let invalidAttempts = 0;
card._view = { x: 123456, y: 234567, w: 3456, h: 4567 };
card._viewModeSnap = { mode: 'view', viewBox: { ...card._view }, zoom: 1 };
card._isoSource = () => {
invalidAttempts++;
throw new Error('invalid source sentinel sensor.private_identifier https://secret.invalid');
};
const firstInvalidKey = card._isoInvalidKey();
card.requestUpdate();
await card.updateComplete;
card.hass = { ...card.hass, states: { ...card.hass.states } };
await card.updateComplete;
const flatView = (root().querySelector('[data-hp-live-viewbox="floor"]')
?.getAttribute('viewBox') || '').split(/\s+/).map(Number);
result.invalidSourceFallbackLatchedAndRefit = invalidAttempts === 1
&& card._isoFallback.has(firstInvalidKey) && card._viewModeSnap === null
&& flatView.length === 4 && flatView.every((value) => Number.isFinite(value) && Math.abs(value) < 10000);
const fallbackFlatCamera = { x: 80, y: 90, w: 700, h: 620 };
card._view = { ...fallbackFlatCamera };
card.requestUpdate();
await card.updateComplete;
result.latchedFallbackKeepsFlatCamera = JSON.stringify(card._view) === JSON.stringify(fallbackFlatCamera);
card._isoSource = healthySource;
await window.__hpTest.setVolumetricView(false);
await window.__hpTest.setVolumetricView(true);
rebind();
result.invalidSourceExplicitRetryRestoresIso = !card._isoFallback.has(firstInvalidKey)
&& !!root().querySelector('[data-hp="iso-walls"]');
const sourceKeyBeforeEpochRecovery = card._isoSource()?.key;
const originalSmokeWall = configSpace.partitions[0];
card._isoSource = () => {
invalidAttempts++;
throw new Error('second invalid source must recover after geometry changes');
};
const epochInvalidKey = card._isoInvalidKey();
card.requestUpdate();
await card.updateComplete;
card.hass = { ...card.hass, states: { ...card.hass.states } };
await card.updateComplete;
card._isoSource = healthySource;
configSpace.partitions[0] = { ...originalSmokeWall, cm: originalSmokeWall.cm + 1 };
card._cfgEpoch++;
const nextEpochInvalidKey = card._isoInvalidKey();
card.requestUpdate();
await card.updateComplete;
await frame();
const sourceKeyAfterEpochRecovery = card._isoSource()?.key;
result.invalidSourceNewFingerprintRestoresIso = invalidAttempts === 2
&& sourceKeyAfterEpochRecovery !== sourceKeyBeforeEpochRecovery
&& !card._isoFallback.has(epochInvalidKey)
&& !card._isoFallback.has(nextEpochInvalidKey)
&& card._effectiveProjection() === 'iso'
&& !!root().querySelector('[data-hp="iso-walls"]');
configSpace.partitions[0] = originalSmokeWall;
card._cfgEpoch++;
card.requestUpdate();
await card.updateComplete;
const source = card._isoSource;
let attempts = 0;
let shouldFail = true;
const fallbackKey = 'f1|injected-render-failure';
card._isoGeometryCache.clear();
card._isoSource = () => ({
key: fallbackKey,
build: () => {
attempts++;
if (shouldFail) throw new Error('injected isometric failure');
return { walls: [], floor: [], openings: [], openingSurfaces: [] };
},
});
card._view = { x: 123456, y: 234567, w: 3456, h: 4567 };
card._viewModeSnap = { mode: 'view', viewBox: { ...card._view }, zoom: 1 };
card.requestUpdate();
await card.updateComplete;
card.hass = { ...card.hass, states: { ...card.hass.states } };
await card.updateComplete;
result.fallbackLatched = attempts === 1
&& card._viewModeSnap === null
&& !root().querySelector('.stage.projection-iso')
&& !root().querySelector('[data-hp="iso-walls"]')
&& card._serverCfg.settings?.volumetric_view === true;
shouldFail = false;
await window.__hpTest.setVolumetricView(false);
await window.__hpTest.setVolumetricView(true);
rebind();
result.explicitRetryRestoresIso = attempts >= 2
&& !!root().querySelector('[data-hp="iso-walls"]')
&& !!root().querySelector('.stage.projection-iso');
card._isoSource = source;
card._isoGeometryCache.delete(fallbackKey);
card._isoFallback.delete(fallbackKey);
card.requestUpdate();
await card.updateComplete;
const runtime = card._isoSceneRuntime;
const warning = console.warn;
let lateAttempts = 0;
const fallbackWarnings = [];
const isoDiagnostics = [];
const diagnosticText = (args) => args.map((value) => {
if (value instanceof Error) return `${value.name}:${value.message}:${value.stack}`;
if (value && typeof value === 'object') return JSON.stringify(value, (_key, item) =>
item instanceof Error ? { name: item.name, message: item.message, stack: item.stack } : item);
return String(value);
}).join(' ');
console.warn = (...args) => {
if (/HOUSEPLAN ISO FALLBACK|hidden isometric runtime diagnostic/.test(String(args[0]))) {
const text = diagnosticText(args);
isoDiagnostics.push(text);
if (String(args[0]).includes('HOUSEPLAN ISO FALLBACK')) fallbackWarnings.push(text);
}
else warning(...args);
};
const privateSpace = 'space.private_identifier';
const privateAsset = 'https://private.invalid/iso.js?token=private-query';
const currentSpace = card._space;
card._space = privateSpace;
const privateKey = `${privateSpace}|${privateAsset}`;
card._latchIsoFallback(privateKey, new Error(`sensor.private_identifier ${privateAsset}`));
card._space = currentSpace;
card._isoFallback.delete(privateKey);
card._isoSceneRuntimeLoader.options.failed(
new Error(`${privateAsset}\nprivate stack sensor.private_identifier`), { terminal: false },
);
await card.updateComplete;
result.isoDiagnosticsAreRedacted = isoDiagnostics.length === 2
&& isoDiagnostics.every((text) => /"kind":|kind=/.test(text)
&& /"fingerprint":|fingerprint=/.test(text) && /"terminal":|terminal=/.test(text))
&& !isoDiagnostics.some((text) => /private_identifier|private\.invalid|sensor\.|token=|stack/i.test(text));
isoDiagnostics.length = 0;
fallbackWarnings.length = 0;
card._stepZoom(1);
const lateTransitionStarted = card._cameraTransition.active;
card._viewModeSnap = { mode: 'view', viewBox: { ...card._view }, zoom: card._zoom };
card._isoSceneRuntime = {
...runtime,
buildIsoOverlayRenderScene: () => {
lateAttempts++;
throw new Error('entity sensor.private_identifier must not reach diagnostics');
},
};
card.requestUpdate();
await card.updateComplete;
card.hass = { ...card.hass, states: { ...card.hass.states } };
await card.updateComplete;
result.lateFailureIsRedactedAndLatched = lateTransitionStarted && !card._cameraTransition.active
&& card._viewModeSnap === null && lateAttempts === 1 && fallbackWarnings.length === 1
&& /"kind":"structural"/.test(fallbackWarnings[0])
&& /"fingerprint":"[a-z0-9]+"/.test(fallbackWarnings[0])
&& /"terminal":true/.test(fallbackWarnings[0])
&& !/(private_identifier|sensor\.|https?:|token=|stack)/i.test(fallbackWarnings[0])
&& !root().querySelector('[data-hp="iso-walls"]')
&& card._serverCfg.settings?.volumetric_view === true;
card._isoSceneRuntime = runtime;
await window.__hpTest.setVolumetricView(false);
await window.__hpTest.setVolumetricView(true);
rebind();
result.lateFailureExplicitRetryRestoresIso = !!root().querySelector('[data-hp="iso-walls"]');
console.warn = warning;
await window.__hpTest.setVolumetricView(false);
await frame();
result.removalIsImmediateFlat = card._serverCfg.settings?.volumetric_view === false
&& !root().querySelector('[data-hp="projection-toggle"]')
&& !root().querySelector('[data-hp="iso-walls"]')
&& !root().querySelector('[id^="hp-iso-"]')
&& ![...root().querySelector('.stage').classList]
.some((name) => name.startsWith('projection-'));
await enable();
result.reenableRestoresIso = !!root().querySelector('.stage.projection-iso')
&& !!root().querySelector('[data-hp="iso-walls"]');
return result;
});
checkAll(out);
await finish(browser, out);