diff --git a/web/app.js b/web/app.js index 497332d..25f911d 100644 --- a/web/app.js +++ b/web/app.js @@ -254,14 +254,20 @@ function makeNotePin(note, idx) { g.dataset.tooltip = note.text || "(empty)"; // drag-to-move + right-click delete + double-click to edit text + // Click on a pin must NOT bubble to the SVG (which would add a waypoint + // / pull / etc. at the same spot, depending on the active tool). + g.addEventListener("click", (e) => e.stopPropagation()); g.addEventListener("pointerdown", (e) => { e.stopPropagation(); - state.drag = { kind: "note", idx }; + state.drag = { kind: "note", idx, moved: false, downX: e.clientX, downY: e.clientY }; g.classList.add("dragging"); g.setPointerCapture(e.pointerId); }); g.addEventListener("pointermove", (e) => { if (!state.drag || state.drag.kind !== "note" || state.drag.idx !== idx) return; + if (Math.hypot(e.clientX - state.drag.downX, e.clientY - state.drag.downY) > 3) { + state.drag.moved = true; + } const pt = svgPointFromEvent(e); const arr = state.notes[currentKey()]; arr[idx].x = pt.x; arr[idx].y = pt.y; @@ -269,29 +275,40 @@ function makeNotePin(note, idx) { }); g.addEventListener("pointerup", () => { if (!state.drag) return; + const moved = state.drag.moved; state.drag = null; g.classList.remove("dragging"); - pushHistory(); - updateHash(); - }); - g.addEventListener("contextmenu", (e) => { - e.preventDefault(); - removeNote(idx); - }); - g.addEventListener("dblclick", (e) => { - e.preventDefault(); - e.stopPropagation(); - const txt = prompt("Edit note text", note.text || ""); - if (txt !== null) { - state.notes[currentKey()][idx].text = txt; + if (moved) { pushHistory(); - renderOverlay(); updateHash(); } }); + g.addEventListener("contextmenu", (e) => { + e.preventDefault(); + e.stopPropagation(); + removeNote(idx); + }); + // Double-click → edit. Works regardless of active tool because the + // pin captures click + dblclick before the canvas click handler runs. + g.addEventListener("dblclick", (e) => { + e.preventDefault(); + e.stopPropagation(); + editNote(idx); + }); return g; } +function editNote(idx) { + const cur = state.notes[currentKey()]?.[idx]; + if (!cur) return; + const txt = prompt("Edit note text:", cur.text || ""); + if (txt === null) return; + cur.text = txt; + pushHistory(); + renderOverlay(); + updateHash(); +} + function removeNote(idx) { const key = currentKey(); const arr = state.notes[key]; @@ -338,15 +355,21 @@ function makeTextLabel(item, idx) { hit.setAttribute("fill", "transparent"); g.appendChild(hit); - // Drag / right-click delete / dbl-click edit + // Stop click propagating so the canvas's tool handler doesn't fire when + // the user is interacting with this label (otherwise editing in Route + // mode would also drop a waypoint underneath). + g.addEventListener("click", (e) => e.stopPropagation()); g.addEventListener("pointerdown", (e) => { e.stopPropagation(); - state.drag = { kind: "text", idx }; + state.drag = { kind: "text", idx, moved: false, downX: e.clientX, downY: e.clientY }; g.classList.add("dragging"); g.setPointerCapture(e.pointerId); }); g.addEventListener("pointermove", (e) => { if (!state.drag || state.drag.kind !== "text" || state.drag.idx !== idx) return; + if (Math.hypot(e.clientX - state.drag.downX, e.clientY - state.drag.downY) > 3) { + state.drag.moved = true; + } const pt = svgPointFromEvent(e); const arr = state.texts[currentKey()]; arr[idx].x = pt.x; arr[idx].y = pt.y; @@ -354,25 +377,23 @@ function makeTextLabel(item, idx) { }); g.addEventListener("pointerup", () => { if (!state.drag) return; + const moved = state.drag.moved; state.drag = null; g.classList.remove("dragging"); - pushHistory(); - updateHash(); + if (moved) { + pushHistory(); + updateHash(); + } }); g.addEventListener("contextmenu", (e) => { e.preventDefault(); + e.stopPropagation(); removeText(idx); }); g.addEventListener("dblclick", (e) => { e.preventDefault(); e.stopPropagation(); - const txt = prompt("Edit label", item.text || ""); - if (txt !== null) { - state.texts[currentKey()][idx].text = txt; - pushHistory(); - renderOverlay(); - updateHash(); - } + editText(idx); }); return g; } @@ -387,6 +408,17 @@ function removeText(idx) { updateHash(); } +function editText(idx) { + const cur = state.texts[currentKey()]?.[idx]; + if (!cur) return; + const txt = prompt("Edit label:", cur.text || ""); + if (txt === null) return; + cur.text = txt; + pushHistory(); + renderOverlay(); + updateHash(); +} + // kg classification IDs: // 1 = minor (NPCs, ambient mobs) // 2 = standard trash @@ -555,14 +587,18 @@ function makeUserPin(x, y, label, cssClass, kind, idx) { t.textContent = label; g.appendChild(t); + g.addEventListener("click", (e) => e.stopPropagation()); g.addEventListener("pointerdown", (e) => { e.stopPropagation(); - state.drag = { kind, idx }; + state.drag = { kind, idx, moved: false, downX: e.clientX, downY: e.clientY }; g.classList.add("dragging"); g.setPointerCapture(e.pointerId); }); g.addEventListener("pointermove", (e) => { if (!state.drag || state.drag.idx !== idx || state.drag.kind !== kind) return; + if (Math.hypot(e.clientX - state.drag.downX, e.clientY - state.drag.downY) > 3) { + state.drag.moved = true; + } const pt = svgPointFromEvent(e); const arr = kind === "route" ? state.routes[currentKey()] : state.pulls[currentKey()]; arr[idx] = { x: pt.x, y: pt.y }; @@ -579,14 +615,18 @@ function makeUserPin(x, y, label, cssClass, kind, idx) { }); g.addEventListener("pointerup", () => { if (!state.drag) return; + const moved = state.drag.moved; state.drag = null; g.classList.remove("dragging"); - pushHistory(); - renderInfoPane(); - updateHash(); + if (moved) { + pushHistory(); + renderInfoPane(); + updateHash(); + } }); g.addEventListener("contextmenu", (e) => { e.preventDefault(); + e.stopPropagation(); removePoint(kind, idx); }); return g;