diff --git a/public/views/grid-setup.mjs b/public/views/grid-setup.mjs index 3a634cc..c3f3533 100644 --- a/public/views/grid-setup.mjs +++ b/public/views/grid-setup.mjs @@ -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)); - this.#corners[this.#drag_idx] = img_pos; + 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(); } }