mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
test: the Resize channel of the junction limits, driven by a real pointer (#329 AC7a)
Measured what Resize itself already forbids: a room cannot be squeezed below 30 cm (two 15 cm walls), so П3 and П5 are unreachable through shrinking and the gate merely fails closed there. П4 IS reachable on a fine grid, so the smoke drags a real handle on a 2 cm grid: two rooms 10 cm apart, a 6 cm pull would leave 4 cm between foreign nodes, the wall stops at 6 cm and exactly one toast names the 5 cm rule. Spec revision 6 records both the measurement and the two corrections it forces on AC7a: a dimmed handle cannot express a per-step limit, and the plan is NOT byte-unchanged — the allowed part of the gesture is a legitimate edit. Issue: #329 User-Visible: no
This commit is contained in:
@@ -119,5 +119,89 @@ const out = await page.evaluate(async () => {
|
||||
return result;
|
||||
});
|
||||
|
||||
checkAll(out);
|
||||
// AC7a (канал Resize): стена упирается в последнюю ДОПУСТИМУЮ позицию, а не
|
||||
// проезжает нарушение. Сетка 2 см: две комнаты в 10 см друг от друга, тяга
|
||||
// на 6 см вправо оставила бы 4 см между чужими узлами (П4 — минимум 5 см).
|
||||
// Жест настоящий: pointer по ручке, без вызовов внутренних методов Resize.
|
||||
const settle = () => page.evaluate(() => new Promise((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const card = window.__card;
|
||||
window.__toasts = [];
|
||||
card._showToast = (text) => { window.__toasts.push(String(text)); };
|
||||
const cm = (value) => (value / 2) * (1000 / 240);
|
||||
card._serverCfg = { spaces: [{
|
||||
id: 'limits', title: 'Limits', cell_cm: 2, view_box: [0, 0, 1, 1],
|
||||
rooms: [], openings: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
}], markers: [], settings: {} };
|
||||
card._space = 'limits'; card._layout = {};
|
||||
card._cfgEpoch++; card._modelCache = null; card._frame = null;
|
||||
card._setMode('plan'); card._tool = 'draw'; card._drawWallField = '15';
|
||||
card.requestUpdate(); await card.updateComplete;
|
||||
const draw = (points, name) => {
|
||||
card._path = [...points, points[0]];
|
||||
card._nameSel = name; card._areaSel = '';
|
||||
card._commitRoom();
|
||||
card._path = [];
|
||||
};
|
||||
draw([[200, 700], [200 + cm(200), 700], [200 + cm(200), 700 - cm(200)],
|
||||
[200, 700 - cm(200)]], 'A');
|
||||
draw([[200 + cm(210), 700], [200 + cm(400), 700],
|
||||
[200 + cm(400), 700 - cm(200)], [200 + cm(210), 700 - cm(200)]], 'B');
|
||||
card._tool = 'resize';
|
||||
card.requestUpdate(); await card.updateComplete;
|
||||
window.__toasts.length = 0;
|
||||
});
|
||||
await settle();
|
||||
await page.waitForTimeout(700);
|
||||
await page.evaluate(() => { window.__toasts.length = 0; });
|
||||
|
||||
// Просвет между правой гранью A и левой гранью B, в сантиметрах.
|
||||
const gapCm = () => page.evaluate(() => {
|
||||
const rooms = window.__card._curSpaceCfg.rooms;
|
||||
const a = Math.max(...rooms.find((room) => room.name === 'A').poly.map((p) => p[0]));
|
||||
const b = Math.min(...rooms.find((room) => room.name === 'B').poly.map((p) => p[0]));
|
||||
return Math.round(((b - a) * 1000) / (1000 / 240) * 2);
|
||||
});
|
||||
const handle = await page.evaluate(() => {
|
||||
const card = window.__card;
|
||||
const handles = [...card.renderRoot.querySelectorAll('.rszhandle[aria-disabled="false"]')];
|
||||
const rooms = card._curSpaceCfg.rooms;
|
||||
const aRight = Math.max(...rooms.find((room) => room.name === 'A').poly.map((p) => p[0])) * 1000;
|
||||
const found = handles.find((entry) => Math.abs(Number(entry.getAttribute('cx')) - aRight) < 1.5);
|
||||
if (!found) return null;
|
||||
const svg = found.ownerSVGElement;
|
||||
const map = (x, y) => {
|
||||
const point = svg.createSVGPoint();
|
||||
point.x = x; point.y = y;
|
||||
const mapped = point.matrixTransform(found.getScreenCTM());
|
||||
return [mapped.x, mapped.y];
|
||||
};
|
||||
const cx = Number(found.getAttribute('cx'));
|
||||
const cy = Number(found.getAttribute('cy'));
|
||||
return { start: map(cx, cy), end: map(cx + (6 / 2) * (1000 / 240), cy) };
|
||||
});
|
||||
|
||||
const resize = { resizeHandleFound: !!handle, resizeGapBefore: await gapCm() };
|
||||
if (handle) {
|
||||
await page.mouse.move(...handle.start);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(...handle.end, { steps: 10 });
|
||||
await settle();
|
||||
await page.mouse.up();
|
||||
await settle();
|
||||
await page.waitForTimeout(700);
|
||||
const toasts = await page.evaluate(() => window.__toasts.slice());
|
||||
// 6 см дало бы 4 см — отказ; стена стоит на 6 см, последней допустимой.
|
||||
resize.resizeStoppedAtLastAllowed = await gapCm();
|
||||
resize.resizeRefusalNamesRule = toasts.some((text) => text.includes('5'));
|
||||
resize.resizeRefusalOnce = toasts.length;
|
||||
}
|
||||
|
||||
checkAll({ ...out, ...resize }, {
|
||||
resizeGapBefore: 10,
|
||||
resizeStoppedAtLastAllowed: 6,
|
||||
resizeRefusalOnce: 1,
|
||||
});
|
||||
await finish(browser);
|
||||
|
||||
@@ -162,10 +162,29 @@ margin): правила П1–П5 применяются к элементам,
|
||||
ровно 3 различные вершины без ступенек и микро-сегментов; golden-сцена +
|
||||
юниты на контурах и на кольце.
|
||||
- **AC7a (Resize, канал §2).** Resize, приводящий к нарушению любого из
|
||||
П1–П5, упирается в последнюю допустимую позицию; при отсутствии
|
||||
безопасного шага ручка приглушена, и её объяснение (hover/focus/нажатие)
|
||||
называет нарушенное правило. План байт-неизменен; тост не показывается.
|
||||
Смок проверяет и байт-неизменность, и текст объяснения ручки.
|
||||
П1–П5, упирается в последнюю ДОПУСТИМУЮ позицию: нарушающий шаг не
|
||||
превьюится и не коммитится, а отказ ровно один раз за жест называет
|
||||
нарушенное правило тем же каналом, которым Resize уже объясняет
|
||||
отклонённый предпросмотр (`resize.preview_failed` → `resize.limit_stopped`).
|
||||
Смок ведёт настоящий pointer-жест по ручке: сетка 2 см, две комнаты в 10 см
|
||||
друг от друга, тяга на 6 см (оставила бы 4 см между чужими узлами, П4)
|
||||
останавливает стену на 6 см и показывает ровно один тост с «5 см».
|
||||
|
||||
> Ревизия 6, отступление от первоначальной формулировки («ручка приглушена,
|
||||
> тост не показывается, план байт-неизменен»). Приглушение ручки считается
|
||||
> статически в `_rszResolution`, до жеста, а нарушение стыка зависит от
|
||||
> КОНКРЕТНОГО шага — статически его не разрешить, не перебирая все позиции.
|
||||
> Молчаливый отказ при этом прямо противоречит уже действующему контракту
|
||||
> #293/#295 («reject не молчит»), закреплённому мутантом
|
||||
> `resize-preview-reject-silent`. И байт-неизменность плана неверна как
|
||||
> общее требование: разрешённая часть жеста — это нормальная правка,
|
||||
> которую пользователь и просил; неизменным план остаётся только когда
|
||||
> допустимого шага нет вовсе.
|
||||
>
|
||||
> Замер собственных ограничителей Resize (2026-08-27): комнату не дают
|
||||
> сузить ниже 30 см (две толщины по 15 см), поэтому П3 (20 см) и П5 через
|
||||
> сужение комнаты недостижимы — гейт там страхует, а не работает. Реально
|
||||
> достижимо П4 на мелкой сетке, этот случай и закреплён смоком.
|
||||
- **AC7b («Толщина», канал §2).** Значение толщины, нарушающее П3/П5, не
|
||||
применяется; показывается тост с названием правила (образец отказа нуля
|
||||
#313). План байт-неизменен. Смок проверяет текст тоста.
|
||||
|
||||
Reference in New Issue
Block a user