From 2b1f03be2db7a1b8a5e4f68694f7d7ed7fa36da8 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 27 Aug 2026 21:45:21 +0300 Subject: [PATCH] test: the Resize channel of the junction limits, driven by a real pointer (#329 AC7a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- demo/smoke_junction_limits.mjs | 86 ++++++++++++++++++++++++++++++- docs/specs/329-junction-limits.md | 27 ++++++++-- 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/demo/smoke_junction_limits.mjs b/demo/smoke_junction_limits.mjs index d0259947..c1b4c1b7 100644 --- a/demo/smoke_junction_limits.mjs +++ b/demo/smoke_junction_limits.mjs @@ -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); diff --git a/docs/specs/329-junction-limits.md b/docs/specs/329-junction-limits.md index 50328b0e..d8fed085 100644 --- a/docs/specs/329-junction-limits.md +++ b/docs/specs/329-junction-limits.md @@ -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). План байт-неизменен. Смок проверяет текст тоста.