Files
houseplan-card/demo/smoke_resize_labels.mjs
Sergey Matyunin f293c6e9dc fix(editor): сохранять позицию кнопки при отмене Resize (#645)
Транзитная геометрия Resize теперь только вычисляет центр для текущего кадра и не удаляет сессионную позицию. Окончательная геометрия по-прежнему очищает точку, если она действительно вышла за комнату.

Issue: #645
User-Visible: yes
2026-09-25 10:15:34 +03:00

179 lines
7.8 KiB
JavaScript

// #300: the production Resize gesture renders two side-wall measurements,
// matching highlights and one area badge per owner beside the moving wall.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const result = await page.evaluate(async () => {
const card = window.__card;
const out = {};
const update = async () => {
card._cfgEpoch++;
card.requestUpdate();
await card.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
};
const rectRoom = (id, x0, y0, x1, y1) => ({
id, name: id, area: null,
poly: [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
.map(([x, y]) => [x / 1000, y / 1000]),
});
// This smoke exercises Resize labels, not the View -> editor animation.
// Enter atomically so the transition controller cannot settle its saved
// camera over the explicit non-default zoom later in this scenario.
card._setMode('plan', false);
card._tool = 'resize';
const space = card._serverCfg.spaces.find((candidate) => candidate.id === card._space);
space.rooms = [
rectRoom('label-left', 200, 100, 300, 500),
rectRoom('label-right', 300, 100, 400, 500),
];
space.openings = [];
delete space.walls;
delete space.open_spans;
delete space.partitions;
delete space.room_drafts;
delete space.wall_columns;
card._resize.reset();
await update();
// #645: move the left room's session-only gear away from the live area
// label. The old automatic centre still collides and would add a tangent
// offset, so this distinguishes the rendered centre from the stale one.
card._editorRuntime.roomGear.positions.set(`${space.id}\u0000label-left`, [292, 140]);
await update();
const resizeHandle = () => [...card.renderRoot.querySelectorAll('.rszhandle')]
.find((node) => Math.abs(Number(node.getAttribute('cx')) - 300) < 1
&& Math.abs(Number(node.getAttribute('cy')) - 300) < 1
&& node.getAttribute('aria-disabled') === 'false');
let handle = resizeHandle();
const stage = card.renderRoot.querySelector('.stage');
const stageRect = stage.getBoundingClientRect();
const svg = stage.querySelector('svg');
const [vx, vy, vw, vh] = svg.getAttribute('viewBox').split(' ').map(Number);
const screen = (x, y) => [
stageRect.left + ((x - vx) / vw) * stageRect.width,
stageRect.top + ((y - vy) / vh) * stageRect.height,
];
const [sx, sy] = screen(300, 300);
const [tx] = screen(325, 300);
const [shrinkX] = screen(275, 300);
const dispatch = (type, x, y) => handle?.dispatchEvent(new PointerEvent(type, {
bubbles: true, cancelable: true, pointerId: 300, pointerType: 'mouse',
clientX: x, clientY: y, buttons: type === 'pointerup' ? 0 : 1,
}));
// Review r1 M1: this preview temporarily excludes the moved button from the
// left room. It may use an automatic centre for label placement, but it must
// not erase the session position because pointercancel restores the room.
const movedGearKey = `${space.id}\u0000label-left`;
dispatch('pointerdown', sx, sy);
dispatch('pointermove', shrinkX, sy);
await update();
out.previewKeepsMovedGear = JSON.stringify(
card._editorRuntime.roomGear.positions.get(movedGearKey),
) === JSON.stringify([292, 140]);
dispatch('pointercancel', shrinkX, sy);
await update();
out.cancelRestoresMovedGear = JSON.stringify(
card._editorRuntime.roomGear.positions.get(movedGearKey),
) === JSON.stringify([292, 140]);
handle = resizeHandle();
dispatch('pointerdown', sx, sy);
dispatch('pointermove', tx, sy);
await update();
const live = card._resize?.liveLabels || [];
const lengths = live.filter((label) => label.kind === 'length');
const areas = live.filter((label) => label.kind === 'area');
out.dragStarted = card._resize?.dragging === true;
out.twoLengths = lengths.length === 2;
out.movingWallLengthAbsent = lengths.every((label) =>
Math.abs(label.edge.a[0] - label.edge.b[0]) > 1);
out.twoMeasuredEdges = card.renderRoot.querySelectorAll(
'[data-hp="resize-measured-edge"]',
).length === 2;
out.twoAreas = card.renderRoot.querySelectorAll('[data-hp="resize-area-label"]').length === 2;
out.twoLeaders = card.renderRoot.querySelectorAll('[data-hp="resize-area-leader"]').length === 2;
out.oppositeSides = new Set(areas.map((label) => label.placement.side)).size === 2;
out.gearVisible = card.renderRoot.querySelectorAll('[data-hp="room-settings"]').length === 2;
const areaRects = [...card.renderRoot.querySelectorAll('[data-hp="resize-area-label"]')]
.map((label) => label.getBoundingClientRect());
out.areasDoNotOverlap = areaRects.length === 2 && (
areaRects[0].right <= areaRects[1].left || areaRects[0].left >= areaRects[1].right
|| areaRects[0].bottom <= areaRects[1].top || areaRects[0].top >= areaRects[1].bottom
);
out.gearAvoided = areas.every((area) => {
const label = card.renderRoot.querySelector(
`[data-hp="resize-area-label"][data-room="${area.roomId}"]`,
);
const gear = card.renderRoot.querySelector(
`[data-hp="room-settings"][data-room="${area.roomId}"]`,
);
if (!label || !gear) return false;
const a = label.getBoundingClientRect();
const b = gear.getBoundingClientRect();
return a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom;
});
out.tangentAvoidanceUsed = areas.some((label) => Math.abs(label.placement.tangentOffsetPx) > 0);
out.movedGearDrivesResizePlacement = (
areas.find((label) => label.roomId === 'label-left')?.placement.tangentOffsetPx
) === 0;
dispatch('pointerup', tx, sy);
await update();
out.cleanedAfterCommit = card._resize?.liveLabels === null
&& card.renderRoot.querySelectorAll('[data-hp^="resize-"]').length === 0;
// Repeat the actual browser gesture at a non-default zoom. The footprint is
// recomputed from current view.w/iconCqw rather than reusing the first frame.
space.rooms = [
rectRoom('label-left', 200, 100, 300, 500),
rectRoom('label-right', 300, 100, 400, 500),
];
card._resize.reset();
card._zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, 2);
await update();
const zoomHandle = [...card.renderRoot.querySelectorAll('.rszhandle')]
.find((node) => Math.abs(Number(node.getAttribute('cx')) - 300) < 1
&& Math.abs(Number(node.getAttribute('cy')) - 300) < 1
&& node.getAttribute('aria-disabled') === 'false');
const zoomStageRect = stage.getBoundingClientRect();
const zoomSvg = stage.querySelector('svg');
const [zvx, zvy, zvw, zvh] = zoomSvg.getAttribute('viewBox').split(' ').map(Number);
const zoomScreen = (x, y) => [
zoomStageRect.left + ((x - zvx) / zvw) * zoomStageRect.width,
zoomStageRect.top + ((y - zvy) / zvh) * zoomStageRect.height,
];
const [zsx, zsy] = zoomScreen(300, 300);
const [ztx] = zoomScreen(325, 300);
const zoomDispatch = (type, x, y) => zoomHandle?.dispatchEvent(new PointerEvent(type, {
bubbles: true, cancelable: true, pointerId: 301, pointerType: 'mouse',
clientX: x, clientY: y, buttons: type === 'pointerup' ? 0 : 1,
}));
zoomDispatch('pointerdown', zsx, zsy);
zoomDispatch('pointermove', ztx, zsy);
await update();
const zoomAreas = [...card.renderRoot.querySelectorAll('[data-hp="resize-area-label"]')];
out.nonDefaultZoomActive = card._zoom > 1 && zoomAreas.length === 2;
out.gearAvoidedAtNonDefaultZoom = zoomAreas.every((label) => {
const gear = card.renderRoot.querySelector(
`[data-hp="room-settings"][data-room="${label.getAttribute('data-room')}"]`,
);
if (!gear) return false;
const a = label.getBoundingClientRect();
const b = gear.getBoundingClientRect();
return a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom;
});
zoomDispatch('pointercancel', ztx, zsy);
await update();
out.cleanedAfterZoomCancel = card._resize?.liveLabels === null;
return out;
});
checkAll(result);
await finish(browser);