Let the sort knob win over the corner handle it sits on (#228)
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Successful in 9m57s

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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-27 13:36:40 +02:00
1 parent 30de4d35a8
commit 3a3401f8ac
3 files changed
+22 -10

No files matched your search

+2 -1
View File
@@ -730,8 +730,9 @@ export class TextureCanvas extends SheetCanvas<TextureAsset> {
}
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);
}
/**
+7 -4
View File
@@ -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;
}
+13 -5
View File
@@ -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();
});
});