mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
fix: make the handle paint order a named decision and mutate it (#400)
CODE-REVIEW-400-r1 Medium: the registered mutant edited a comment, not the order — it could not reproduce the regression AC1 exists to catch. That is the same defect class this issue is fixing elsewhere, in my own guard. The order is now HANDLE_PAINT_ORDER, a named constant, because it IS the hit priority rather than an accident of where the blocks sit in the template. The mutant flips that constant, so it reproduces exactly the behaviour the audit found. Also: smoke_furniture picked the SE corner as handles[3], an index that silently depended on the old paint order — CI shard 3 went red on four checks. It now selects by role (corner handles, third of four), which is what the test actually means. User-Visible: no Issue: #400
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;",
|
||||
}],
|
||||
},
|
||||
{
|
||||
|
||||
+26
-19
@@ -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 {
|
||||
<circle class="dthandle dtrot" cx="${b.x + b.w / 2}" cy="${b.y - arm}" r="${hr.toFixed(1)}"
|
||||
@pointerdown=${(e: PointerEvent) => this._dtStart(e, 'rotate')}></circle>
|
||||
<circle class="dtknob" cx="${b.x + b.w / 2}" cy="${b.y - arm}" r="${kr.toFixed(2)}"></circle>
|
||||
${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`<circle class="dthandle dtedge dt-${cur}" cx="${x}" cy="${y}"
|
||||
r="${hr.toFixed(1)}" @pointerdown=${(e: PointerEvent) =>
|
||||
this._dtStart(e, 'scale', [sx, sy])}></circle>
|
||||
<circle class="dtknob dtedgeknob" cx="${x}" cy="${y}" r="${kr.toFixed(2)}"></circle>`;
|
||||
})}
|
||||
${/* #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`<circle class="dthandle dt-${cur}"
|
||||
cx="${sx < 0 ? b.x : b.x + b.w}" cy="${sy < 0 ? b.y : b.y + b.h}" r="${hr.toFixed(1)}"
|
||||
@pointerdown=${(e: PointerEvent) => this._dtStart(e, 'scale', [sx, sy])}></circle><circle class="dtknob"
|
||||
cx="${sx < 0 ? b.x : b.x + b.w}" cy="${sy < 0 ? b.y : b.y + b.h}" r="${kr.toFixed(2)}"></circle>`)}
|
||||
${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`<circle class="dthandle dtedge dt-${cur}" cx="${x}" cy="${y}"
|
||||
r="${hr.toFixed(1)}" @pointerdown=${(e: PointerEvent) =>
|
||||
this._dtStart(e, 'scale', [sx, sy])}></circle>
|
||||
<circle class="dtknob dtedgeknob" cx="${x}" cy="${y}" r="${kr.toFixed(2)}"></circle>`;
|
||||
})
|
||||
: corners.map(([sx, sy, cur]) => svg`<circle class="dthandle dt-${cur}"
|
||||
cx="${sx < 0 ? b.x : b.x + b.w}" cy="${sy < 0 ? b.y : b.y + b.h}" r="${hr.toFixed(1)}"
|
||||
@pointerdown=${(e: PointerEvent) => this._dtStart(e, 'scale', [sx, sy])}></circle><circle class="dtknob"
|
||||
cx="${sx < 0 ? b.x : b.x + b.w}" cy="${sy < 0 ? b.y : b.y + b.h}" r="${kr.toFixed(2)}"></circle>`)))}
|
||||
</g>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user