Add edge midpoint drag handles to Grid_Setup

Drag indices 4-7 correspond to top/right/bottom/left edge midpoints.
Dragging a midpoint applies the delta to both adjacent corners, making
it easier to align bins with rounded corners where corner handles may
be obscured.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 04:27:35 +00:00
parent 80a2fabf7d
commit 67369b56be

View File

@@ -14,13 +14,17 @@ export class Grid_Setup {
#cam_z = 1;
#corners = null; // in IMAGE coordinates
#drag_idx = -1; // index of corner being dragged, or -1
#drag_idx = -1; // 0-3: corners, 4-7: edge midpoints (top,right,bottom,left)
#drag_prev_img = null; // previous image-space position for midpoint delta tracking
#panning = false;
#pan_last = { x: 0, y: 0 };
#rows = 4;
#cols = 6;
// Edge pairs for midpoint handles: midpoint (idx-4) moves corners [a, b]
static #MIDPOINT_EDGES = [[0,1],[1,2],[2,3],[3,0]];
constructor(canvas_el) {
this.#canvas = canvas_el;
this.#ctx = canvas_el.getContext('2d');
@@ -126,12 +130,26 @@ export class Grid_Setup {
return { x: w.x * this.#cam_z + this.#cam_x, y: w.y * this.#cam_z + this.#cam_y };
}
#get_midpoints() {
return Grid_Setup.#MIDPOINT_EDGES.map(([a, b]) => ({
x: (this.#corners[a].x + this.#corners[b].x) / 2,
y: (this.#corners[a].y + this.#corners[b].y) / 2,
}));
}
#find_handle(sp, radius = 18) {
if (!this.#corners) return -1;
// Corners take priority
for (let i = 0; i < 4; i++) {
const s = this.#img_to_screen(this.#corners[i]);
if ((sp.x - s.x)**2 + (sp.y - s.y)**2 < radius**2) return i;
}
// Midpoints
const mids = this.#get_midpoints();
for (let i = 0; i < 4; i++) {
const s = this.#img_to_screen(mids[i]);
if ((sp.x - s.x)**2 + (sp.y - s.y)**2 < (radius * 0.85)**2) return i + 4;
}
return -1;
}
@@ -141,10 +159,13 @@ export class Grid_Setup {
const hit = this.#find_handle(sp);
if (hit !== -1) {
this.#drag_idx = hit;
if (hit >= 4) {
this.#drag_prev_img = this.#world_to_img(this.#to_world(sp));
}
} else {
this.#panning = true;
this.#pan_last = sp;
if (!is_touch) this.#canvas.style.cursor = 'grabbing';
if (!is_touch) { this.#canvas.style.cursor = 'grabbing'; }
}
}
@@ -152,7 +173,16 @@ export class Grid_Setup {
const sp = this.#screen_pos(e);
if (this.#drag_idx !== -1) {
const img_pos = this.#world_to_img(this.#to_world(sp));
if (this.#drag_idx < 4) {
this.#corners[this.#drag_idx] = img_pos;
} else {
const [a, b] = Grid_Setup.#MIDPOINT_EDGES[this.#drag_idx - 4];
const dx = img_pos.x - this.#drag_prev_img.x;
const dy = img_pos.y - this.#drag_prev_img.y;
this.#corners[a] = { x: this.#corners[a].x + dx, y: this.#corners[a].y + dy };
this.#corners[b] = { x: this.#corners[b].x + dx, y: this.#corners[b].y + dy };
this.#drag_prev_img = img_pos;
}
this.#draw();
} else if (this.#panning) {
this.#cam_x += sp.x - this.#pan_last.x;
@@ -167,6 +197,7 @@ export class Grid_Setup {
#on_up(e) {
this.#drag_idx = -1;
this.#drag_prev_img = null;
if (this.#panning) {
this.#panning = false;
const sp = this.#screen_pos(e);
@@ -255,6 +286,19 @@ export class Grid_Setup {
ctx.fillText(LABELS[i], pt.x, pt.y);
});
// Midpoint handles — smaller, white with blue stroke
const mid_r = 7 / this.#cam_z;
const mids = this.#get_midpoints().map(m => this.#img_to_world(m));
mids.forEach(pt => {
ctx.beginPath();
ctx.arc(pt.x, pt.y, mid_r, 0, Math.PI*2);
ctx.fillStyle = 'rgba(255,255,255,0.75)';
ctx.fill();
ctx.strokeStyle = 'rgba(91,156,246,0.9)';
ctx.lineWidth = 2 / this.#cam_z;
ctx.stroke();
});
ctx.restore();
}
}