mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Дефект High-1 был одинаковым в двух местах — и в живом рисовании, и в
«Оптимизировать планы», — потому что каждый вызывающий собирал геометрию
примыканий сам. Ревью r2 справедливо заметило, что и защита получилась
однобокой: юнит и мутант сторожили только оптимизатор, а путь карты — тот, где
дефект и был виден пользователю, — не сторожил никто. Заплатка в виде второго
мутанта-близнеца оставила бы причину на месте: два списка координат, которые
обязаны совпадать, но ничем не связаны.
Поэтому геометрия переехала в `spaceMergeGeometry(space, { excludeDraftId })`:
один источник комнат, колонн и концов черновиков, одни координаты, одно место,
где можно ошибиться. Оба вызывающих теперь строчка вызова.
Покрытие идёт за причиной, а не за симптомом: три юнита в
`test/wall-merge.test.mjs` проверяют масштаб полигонов (включая комнаты в форме
x/y/w/h и комнату без геометрии), исключение активного черновика и сам T-стык к
середине стороны комнаты. Мутанты `partition-merge-rescales-rooms` и
`chain-merge-sees-own-draft` перенацелены на общий модуль и теперь краснеют для
обоих путей сразу: 2 и 1 падение, проверено применением патча.
Сценарий с комнатой в смоке пробовал — не взлетел: рисование в комнату
поднимает `_offerWallFaces`, и цепочка не завершается штатно. Ломать смок под
тест не стал, юниты общего модуля покрывают оба пути честнее.
Issue: #229
User-Visible: no
81 lines
3.4 KiB
JavaScript
81 lines
3.4 KiB
JavaScript
// Issue #229: a straight wall drawn in several clicks is stored as one record.
|
|
//
|
|
// The unit tests own the rules; this smoke owns the wiring — that finishing a
|
|
// chain in the real editor actually calls the merge, that the seam disappears
|
|
// from the saved config, and that a chain drawn round a corner keeps its node.
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
const { page, browser } = await launch({ width: 1000, height: 900 }, 1);
|
|
const res = await page.evaluate(async () => {
|
|
const out = {};
|
|
const c = window.__card;
|
|
const settle = async () => {
|
|
for (let i = 0; i < 3; i++) await new Promise((r) => requestAnimationFrame(r));
|
|
await c.updateComplete;
|
|
};
|
|
const space = () => c._serverCfg.spaces.find((s) => s.id === c._space);
|
|
const partitionCount = () => (space().partitions || []).length;
|
|
|
|
c._mode = 'plan'; c.requestUpdate(); await settle();
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
c._tool = 'draw'; c._drawWallCm = 15; c.requestUpdate(); await settle();
|
|
|
|
const before = partitionCount();
|
|
|
|
// Four clicks along one straight line, then finish the chain.
|
|
c._path = [[200, 500], [300, 500], [420, 500], [560, 500]];
|
|
c._draftSegmentCms = [];
|
|
out.chainHasThreeSegments = c._path.length - 1 === 3;
|
|
c._finishWallChain();
|
|
await settle();
|
|
|
|
const straight = (space().partitions || []).slice(before);
|
|
out.straightRunIsOneRecord = straight.length === 1;
|
|
out.straightRunSpansTheChain = !!straight[0]
|
|
&& Math.abs(straight[0].a[0] * 1000 - 200) < 0.01
|
|
&& Math.abs(straight[0].b[0] * 1000 - 560) < 0.01;
|
|
out.straightRunKeepsThickness = !!straight[0] && straight[0].cm === 15;
|
|
|
|
// A corner is not a straight run: two records, one node between them.
|
|
const beforeCorner = partitionCount();
|
|
c._path = [[200, 700], [400, 700], [400, 820]];
|
|
c._draftSegmentCms = [];
|
|
c._finishWallChain();
|
|
await settle();
|
|
out.cornerKeepsBothRecords = partitionCount() - beforeCorner === 2;
|
|
|
|
// Drawing onto an existing wall merges with it — the seam belongs to the chain.
|
|
// Real clicks, not an assigned `_path`: each click persists the chain into
|
|
// `room_drafts`, and the merge used to mistake the chain's own ends for a
|
|
// foreign junction (review CODE-REVIEW-229-r1, High-2).
|
|
const stage = c.shadowRoot.querySelector('svg');
|
|
// Clicks are given in plan coordinates and converted through the live view
|
|
// box, so the smoke aims at the wall it just drew and not at a screen guess.
|
|
const click = async (px, py) => {
|
|
const rect = stage.getBoundingClientRect();
|
|
const view = c._viewOr(c._baseVb());
|
|
stage.dispatchEvent(new MouseEvent('click', {
|
|
clientX: rect.left + ((px - view.x) / view.w) * rect.width,
|
|
clientY: rect.top + ((py - view.y) / view.h) * rect.height,
|
|
bubbles: true, composed: true,
|
|
}));
|
|
await settle();
|
|
};
|
|
const beforeTouch = partitionCount();
|
|
await click(560, 500);
|
|
await click(700, 500);
|
|
out.clicksPersistTheDraft = ((space().room_drafts || []).length) === 1;
|
|
c._finishWallChain();
|
|
await settle();
|
|
out.chainMergesIntoTheWallItTouches = partitionCount() === beforeTouch;
|
|
const extended = (space().partitions || []).find(
|
|
(p) => Math.abs(p.a[1] * 1000 - 500) < 0.01 && Math.abs(p.b[1] * 1000 - 500) < 0.01,
|
|
);
|
|
out.touchedWallGrew = !!extended && Math.abs(extended.b[0] * 1000 - 700) < 0.01;
|
|
out.draftIsGone = !(space().room_drafts || []).length;
|
|
|
|
|
|
return out;
|
|
});
|
|
checkAll(res);
|
|
await finish(browser, res);
|