diff --git a/demo/smoke_furniture.mjs b/demo/smoke_furniture.mjs index 4fd975ad..6841515a 100644 --- a/demo/smoke_furniture.mjs +++ b/demo/smoke_furniture.mjs @@ -357,7 +357,13 @@ const res = await page.evaluate(async () => { const before = { w: sofaNow().w, h: sofaNow().h }; const b = c._dtBox || { x: 0, y: 0, w: 0, h: 0 }; const handles = [...(frame()?.querySelectorAll('.dthandle') || [])]; - const se = handles[3] || stageEl(); // SE — четвёртый угол + // #400: угловая ручка ищется по РОЛИ, а не по индексу в DOM. Индекс держался + // на порядке отрисовки, а порядок — это решение о приоритете хита (углы + // рисуются последними, чтобы выигрывать нажатие у осевых на мелкой мебели), + // и оно имеет право меняться. Юго-восточный угол — второй `nwse`. + const cornerHandles = handles.filter((el) => !el.classList.contains('dtrot') + && !el.classList.contains('dtedge')); + const se = cornerHandles[2] || stageEl(); // SE — [1, 1, 'nwse'] ev('pointerdown', se, b.x + b.w, b.y + b.h); out.cornerDragStarted = c._dtDrag?.kind === 'scale' && !!c._dtDrag?.orig; ev('pointermove', stageEl(), b.x + b.w + 100, b.y + b.h + 20, { shiftKey: true }); @@ -405,8 +411,9 @@ const res = await page.evaluate(async () => { // must restore the exact pre-gesture object and create no persisted flip. const crossBefore = JSON.parse(JSON.stringify(sofaNow())); const cb = c._dtBox; - const crossHandles = [...frame().querySelectorAll('.dthandle')]; - const crossSe = crossHandles[3]; + const crossHandles = [...frame().querySelectorAll('.dthandle')] + .filter((el) => !el.classList.contains('dtrot') && !el.classList.contains('dtedge')); + const crossSe = crossHandles[2]; // #400: по роли, не по позиции в DOM ev('pointerdown', crossSe, cb.x + cb.w, cb.y + cb.h); ev('pointermove', stageEl(), cb.x - 25, cb.y + cb.h + 10, { shiftKey: true }); await c.updateComplete; diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index 4f46c527..a2a82a9d 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -754,8 +754,11 @@ const MUTANT_DEFINITIONS = [ + 'the one that cannot be reached any other way (#400 AC1)', patches: [{ file: 'src/houseplan-card.ts', - find: ' ${/* #400: corners LAST.', - replace: ' ${/* mutant: corners no longer last.', + // Порядок — именованное решение, поэтому мутант меняет ЕГО, а не текст + // рядом с ним: прежний патч правил комментарий и регрессию не + // воспроизводил (находка CODE-REVIEW-400-r1). + find: "const HANDLE_PAINT_ORDER = ['edges', 'corners'] as const;", + replace: "const HANDLE_PAINT_ORDER = ['corners', 'edges'] as const;", }], }, { diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index 0e066f1c..f61c3448 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -811,6 +811,19 @@ interface DeviceInboxDialogState { type FixedFloorState = FixedFloorSelection | { kind: 'pending'; value: unknown }; +/** + * #400: the paint order of the selection handles IS the hit priority, so it is + * a named decision rather than a consequence of where the blocks happen to sit + * in the template. + * + * Corner and edge handles carry the same hit radius (1.8 % of the view). On + * furniture narrower than 4·hr — a 40 cm cabinet — their circles overlap, and + * whichever is painted last takes the tap. The corner must be last: an edge + * handle scales one axis, a corner scales both, and the object is small + * exactly when proportional resize matters most. + */ +const HANDLE_PAINT_ORDER = ['edges', 'corners'] as const; + interface DeviceDragState { id: string; spaceId: string; @@ -8464,25 +8477,19 @@ export class HouseplanCard extends LitElement { this._dtStart(e, 'rotate')}> - ${sides.map(([sx, sy, cur]) => { - const x = sx < 0 ? b.x : sx > 0 ? b.x + b.w : b.x + b.w / 2; - const y = sy < 0 ? b.y : sy > 0 ? b.y + b.h : b.y + b.h / 2; - return svg` - this._dtStart(e, 'scale', [sx, sy])}> - `; - })} - ${/* #400: corners LAST. Both handles carry the same hit radius (1.8 % of - the view), so on furniture narrower than 4·hr — a 40 cm cabinet — the - two circles overlap and whichever is painted later takes the hit. - The corner is the one that cannot be reached any other way: an edge - handle scales one axis, a corner scales both, and the object is small - precisely when proportional resize matters most. The bead order is - unchanged visually — the knobs are drawn with their own handles. */ - corners.map(([sx, sy, cur]) => svg` this._dtStart(e, 'scale', [sx, sy])}>`)} + ${HANDLE_PAINT_ORDER.map((role) => (role === 'edges' + ? sides.map(([sx, sy, cur]) => { + const x = sx < 0 ? b.x : sx > 0 ? b.x + b.w : b.x + b.w / 2; + const y = sy < 0 ? b.y : sy > 0 ? b.y + b.h : b.y + b.h / 2; + return svg` + this._dtStart(e, 'scale', [sx, sy])}> + `; + }) + : corners.map(([sx, sy, cur]) => svg` this._dtStart(e, 'scale', [sx, sy])}>`)))} ` as unknown as TemplateResult; }