From 8d77c6eee8bed58b058cf457dc872c41e422e4c9 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 28 Feb 2026 22:42:33 -0800 Subject: [PATCH] fix: canvas tools place shapes exactly where the ghost placeholder shows - newShape() with explicit position now places directly at click point instead of routing through findFreePosition spiral which could nudge the shape away from the cursor - Sticky note converted to setPendingTool so it shows dotted placeholder before placement instead of instantly spawning at viewport center - Feed tool converted to setPendingTool with __postCreate for the same click-to-place UX - Removed wb-sticky from mobile keepOpen list since it's now a placement tool that should close the menu Co-Authored-By: Claude Opus 4.6 --- website/canvas.html | 108 +++++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/website/canvas.html b/website/canvas.html index 9fecdb5a..cd129fe0 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -2053,8 +2053,9 @@ } }); - // Create a shape, position it without overlapping others, add to canvas, and register for sync - // atPosition: optional { x, y } in canvas coordinates to place near + // Create a shape, add to canvas, and register for sync. + // atPosition: optional { x, y } in canvas coordinates — places shape centered there exactly. + // Without atPosition, uses findFreePosition to auto-place without overlapping. function newShape(tagName, props = {}, atPosition) { const id = `shape-${Date.now()}-${++shapeCounter}`; const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 }; @@ -2062,9 +2063,14 @@ const shape = document.createElement(tagName); shape.id = id; - const pos = atPosition - ? findFreePosition(defaults.width, defaults.height, atPosition.x, atPosition.y) - : findFreePosition(defaults.width, defaults.height); + let pos; + if (atPosition) { + // Explicit click-to-place: honor exact position (centered on click) + pos = { x: atPosition.x - defaults.width / 2, y: atPosition.y - defaults.height / 2 }; + } else { + // Auto-place: find free spot near viewport center + pos = findFreePosition(defaults.width, defaults.height); + } shape.x = pos.x; shape.y = pos.y; shape.width = defaults.width; @@ -2280,48 +2286,49 @@ }; const flowKind = moduleFlowKinds[sourceModule] || "data"; - const shape = newShape("folk-feed", { + setPendingTool("folk-feed", { sourceModule, sourceLayer: "layer-" + sourceModule, feedId: "", flowKind, maxItems: 10, refreshInterval: 30000, + __postCreate: (shape) => { + // Auto-register a LayerFlow in Automerge if layers exist + if (sync.getLayers) { + const layers = sync.getLayers(); + const currentLayer = layers.find(l => l.moduleId === "rspace") || layers[0]; + const sourceLayer = layers.find(l => l.moduleId === sourceModule); + + if (currentLayer && sourceLayer) { + const flowId = `flow-auto-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + sync.addFlow({ + id: flowId, + kind: flowKind, + sourceLayerId: sourceLayer.id, + targetLayerId: currentLayer.id, + targetShapeId: shape.id, + label: sourceModule + " feed", + strength: 0.5, + active: true, + }); + } + + // Also ensure source module has a layer (add if missing) + if (!sourceLayer) { + sync.addLayer({ + id: "layer-" + sourceModule, + moduleId: sourceModule, + label: sourceModule, + order: layers.length, + color: "", + visible: true, + createdAt: Date.now(), + }); + } + } + }, }); - - // Auto-register a LayerFlow in Automerge if layers exist - if (shape && sync.getLayers) { - const layers = sync.getLayers(); - const currentLayer = layers.find(l => l.moduleId === "rspace") || layers[0]; - const sourceLayer = layers.find(l => l.moduleId === sourceModule); - - if (currentLayer && sourceLayer) { - const flowId = `flow-auto-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; - sync.addFlow({ - id: flowId, - kind: flowKind, - sourceLayerId: sourceLayer.id, - targetLayerId: currentLayer.id, - targetShapeId: shape.id, - label: sourceModule + " feed", - strength: 0.5, - active: true, - }); - } - - // Also ensure source module has a layer (add if missing) - if (!sourceLayer) { - sync.addLayer({ - id: "layer-" + sourceModule, - moduleId: sourceModule, - label: sourceModule, - order: layers.length, - color: "", - visible: true, - createdAt: Date.now(), - }); - } - } }); // Arrow connection mode @@ -2411,18 +2418,17 @@ document.getElementById("wb-pencil")?.addEventListener("click", () => setWbTool("pencil")); document.getElementById("wb-sticky")?.addEventListener("click", () => { - // Create a sticky note as a markdown shape with yellow background setWbTool(null); - const shape = newShape("folk-markdown", { - content: "# Sticky Note\n\nClick to edit..." + setPendingTool("folk-markdown", { + content: "# Sticky Note\n\nClick to edit...", + __postCreate: (shape) => { + shape.width = 200; + shape.height = 200; + shape.style.background = "#fef08a"; + shape.style.borderRadius = "4px"; + shape.style.boxShadow = "2px 2px 8px rgba(0,0,0,0.15)"; + }, }); - if (shape) { - shape.width = 200; - shape.height = 200; - shape.style.background = "#fef08a"; - shape.style.borderRadius = "4px"; - shape.style.boxShadow = "2px 2px 8px rgba(0,0,0,0.15)"; - } }); document.getElementById("wb-rect")?.addEventListener("click", () => setWbTool("rect")); document.getElementById("wb-circle")?.addEventListener("click", () => setWbTool("circle")); @@ -2694,7 +2700,7 @@ if (!btn) return; // Keep open for connect, memory, group toggles, collapse, whiteboard tools const keepOpen = ["new-arrow", "toggle-memory", "toggle-theme", "zoom-in", "zoom-out", "reset-view", "toolbar-collapse", - "wb-pencil", "wb-sticky", "wb-rect", "wb-circle", "wb-line", "wb-eraser"]; + "wb-pencil", "wb-rect", "wb-circle", "wb-line", "wb-eraser"]; if (btn.classList.contains("toolbar-group-toggle")) return; if (!keepOpen.includes(btn.id)) { toolbarEl.classList.remove("mobile-open");