From 3a3401f8ac4becbae0941b6310a4a886339d8ec0 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Sun, 27 Sep 2026 13:36:40 +0200 Subject: [PATCH] Let the sort knob win over the corner handle it sits on (#228) The CI run for 30de4d3 failed on e2e 37b-ii: dragging the sort knob of a nine-slice frame resized the frame instead (h 18, lifted null). The knob's middle sits exactly 8 px left of the frame, which is also the edge of the corner handle's 8 px tolerance. Until now only the rounding of the pointer decided which of the two won. The slimmer header bars of #245 moved that rounding to the corner handle's side. The bug itself has been there since #228. - sortLineAt bounds the knob where it is drawn, from a gap to the reach left of the frame, instead of by the handle tolerance. - TextureCanvas passes that gap in image pixels, scaled by the zoom. - The corner handle stays reachable at the corner itself and to the right of and below it. Tests: - ./check.sh: 178 files, 2576 tests, CHECK EXIT 0 - GM_STEPS=1,37: ALL PASSED, 26 checks (37b-ii: lifted 6, h 24) - New unit test: the knob's middle belongs to the knob even inside the corner handle's reach. It fails with the old bound. - Full e2e run not repeated. 37b-ii was the only failure in CI. Co-Authored-By: Claude Opus 5.5 --- src/editor/texture/TextureCanvas.ts | 3 ++- src/editor/texture/sheetView.ts | 11 +++++++---- tests/viewport/sort-by-y.test.ts | 18 +++++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/editor/texture/TextureCanvas.ts b/src/editor/texture/TextureCanvas.ts index 850fc43f..8441e518 100644 --- a/src/editor/texture/TextureCanvas.ts +++ b/src/editor/texture/TextureCanvas.ts @@ -730,8 +730,9 @@ export class TextureCanvas extends SheetCanvas { } private sortLineAt(frame: TextureFrame, point: Vec2, tolerance: number): 'knob' | 'line' | null { + const gap = SORT_KNOB_GAP / this.view.zoom; const reach = (SORT_KNOB_GAP + SORT_KNOB_SIZE + 2) / this.view.zoom; - return sortLineAt(frame, frameSortLine(frame), point, tolerance, reach); + return sortLineAt(frame, frameSortLine(frame), point, tolerance, gap, reach); } /** diff --git a/src/editor/texture/sheetView.ts b/src/editor/texture/sheetView.ts index f1091601..faab9f9e 100644 --- a/src/editor/texture/sheetView.ts +++ b/src/editor/texture/sheetView.ts @@ -174,19 +174,22 @@ export function sortLineY(rect: FrameRect, line: number): number { /** * What a press grabs of the sort line: its knob left of the frame, reachable wherever the line sits, - * or the line itself inside the frame. `reach` is how far left of the frame the knob goes, in image - * pixels. The press tests the knob before the frame's handles and the line after its border lines, - * which at 0 all lie on the bottom edge with it. + * or the line itself inside the frame. The knob spans from `gap` to `reach` left of the frame, in + * image pixels. The press tests the knob before the frame's handles and the line after its border + * lines, which at 0 all lie on the bottom edge with it. So the knob takes its whole drawn width, + * even where the corner handle's tolerance reaches over it – bounding it by that tolerance instead + * left its very middle to the corner handle. */ export function sortLineAt( rect: FrameRect, line: number, point: Vec2, tolerance: number, + gap: number, reach: number, ): 'knob' | 'line' | null { if (Math.abs(point.y - sortLineY(rect, line)) > tolerance) return null; - if (point.x >= rect.x - reach && point.x < rect.x - tolerance) return 'knob'; + if (point.x >= rect.x - reach && point.x < rect.x - gap) return 'knob'; return point.x >= rect.x && point.x <= rect.x + rect.w ? 'line' : null; } diff --git a/tests/viewport/sort-by-y.test.ts b/tests/viewport/sort-by-y.test.ts index 1bcc2caf..2e9a8316 100644 --- a/tests/viewport/sort-by-y.test.ts +++ b/tests/viewport/sort-by-y.test.ts @@ -297,11 +297,19 @@ describe('the sort line of a texture frame (#228)', () => { const rect = { x: 10, y: 20, w: 30, h: 40 }; expect(sortLineY(rect, 0)).toBe(60); expect(sortLineY(rect, 6)).toBe(54); - expect(sortLineAt(rect, 6, { x: 4, y: 54 }, 1, 8)).toBe('knob'); - expect(sortLineAt(rect, 6, { x: 25, y: 54.5 }, 1, 8)).toBe('line'); - expect(sortLineAt(rect, 6, { x: 25, y: 57 }, 1, 8)).toBeNull(); - expect(sortLineAt(rect, 6, { x: 0, y: 54 }, 1, 8)).toBeNull(); - expect(sortLineAt(rect, 6, { x: 45, y: 54 }, 1, 8)).toBeNull(); + expect(sortLineAt(rect, 6, { x: 4, y: 54 }, 1, 1, 8)).toBe('knob'); + expect(sortLineAt(rect, 6, { x: 25, y: 54.5 }, 1, 1, 8)).toBe('line'); + expect(sortLineAt(rect, 6, { x: 25, y: 57 }, 1, 1, 8)).toBeNull(); + expect(sortLineAt(rect, 6, { x: 0, y: 54 }, 1, 1, 8)).toBeNull(); + expect(sortLineAt(rect, 6, { x: 45, y: 54 }, 1, 1, 8)).toBeNull(); + }); + + it('takes the middle of its knob even where the corner handle reaches over it', () => { + // The editor's sizes at zoom 1: handles grab 8 px around the corner, the knob is drawn from + // 4 to 12 px left of the frame, so its middle sits exactly 8 px out – on the handle's edge. + const rect = { x: 100, y: 20, w: 24, h: 24 }; + expect(sortLineAt(rect, 0, { x: 92, y: 44 }, 8, 4, 14)).toBe('knob'); + expect(sortLineAt(rect, 0, { x: 97, y: 44 }, 8, 4, 14)).toBeNull(); }); });