diff --git a/website/canvas.html b/website/canvas.html index d831bc9..fe67b22 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -1387,6 +1387,7 @@ } } + folk-shape, folk-markdown, folk-wrapper, folk-arrow, @@ -1428,6 +1429,15 @@ position: absolute; } + /* Whiteboard drawings rendered as folk-shapes with inline SVG */ + folk-shape[data-wb-drawing] > svg { + width: 100%; + height: 100%; + display: block; + overflow: visible; + pointer-events: none; + } + .connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map, folk-image-gen, folk-video-gen, folk-prompt, folk-zine-gen, folk-transcription, @@ -2907,10 +2917,6 @@ if (document.getElementById(data.id)) { return; } - // Check if wb-svg already exists in the overlay - if (data.type === "wb-svg" && wbOverlay.querySelector(`[data-wb-id="${data.id}"]`)) { - return; - } try { isProcessingRemote = true; @@ -3201,18 +3207,23 @@ if (data.maxItems) shape.maxItems = data.maxItems; if (data.refreshInterval) shape.refreshInterval = data.refreshInterval; break; - case "wb-svg": - // Whiteboard SVG drawing — recreate in SVG overlay, not as a folk-shape - if (data.svgMarkup) { - const temp = document.createElementNS("http://www.w3.org/2000/svg", "g"); - temp.innerHTML = data.svgMarkup; - const svgEl = temp.firstElementChild; - if (svgEl) { - svgEl.setAttribute("data-wb-id", data.id); - wbOverlay.appendChild(svgEl); - } + case "wb-svg": { + // Whiteboard SVG drawing — render as a folk-shape with inline SVG + if (!data.svgMarkup) return null; + let vb = data.svgViewBox; + // Old format: x/y/width/height are 0 — compute bounds from SVG + if (!data.width || !data.height || !vb) { + const bounds = computeWbBounds(data.svgMarkup); + if (!bounds) return null; + data.x = bounds.x; + data.y = bounds.y; + data.width = bounds.width; + data.height = bounds.height; + vb = bounds.viewBox; } - return null; // Not a folk-shape element + shape = createWbShapeElement(data.svgMarkup, vb); + break; + } case "folk-markdown": default: shape = document.createElement("folk-markdown"); @@ -3917,12 +3928,57 @@ const wbColor = "#1e293b"; const wbStrokeWidth = 3; - // SVG overlay for whiteboard drawing + // SVG overlay for whiteboard drawing (used during active drawing only) const wbOverlay = document.createElementNS("http://www.w3.org/2000/svg", "svg"); wbOverlay.id = "wb-overlay"; wbOverlay.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;overflow:visible;"; canvasContent.appendChild(wbOverlay); + // ── Helpers for converting SVG drawings into folk-shape elements ── + + // Compute bounding box of SVG markup by temporarily rendering it + function computeWbBounds(svgMarkup, padding) { + if (padding === undefined) padding = wbStrokeWidth + 2; + const temp = document.createElementNS("http://www.w3.org/2000/svg", "g"); + temp.innerHTML = svgMarkup; + const el = temp.firstElementChild; + if (!el) return null; + wbOverlay.appendChild(el); + const bbox = el.getBBox(); + el.remove(); + if (bbox.width < 1 && bbox.height < 1) return null; + return { + x: bbox.x - padding, + y: bbox.y - padding, + width: bbox.width + padding * 2, + height: bbox.height + padding * 2, + viewBox: `${bbox.x - padding} ${bbox.y - padding} ${bbox.width + padding * 2} ${bbox.height + padding * 2}`, + }; + } + + // Create a folk-shape element wrapping SVG drawing content + function createWbShapeElement(svgMarkup, viewBox) { + const shape = document.createElement("folk-shape"); + shape.dataset.wbDrawing = "true"; + + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", viewBox); + svg.setAttribute("preserveAspectRatio", "none"); + svg.innerHTML = svgMarkup; + shape.appendChild(svg); + + // Monkey-patch toJSON so CommunitySync persists svgMarkup + shape.toJSON = function() { + return { + type: "wb-svg", + svgMarkup: svgMarkup, + svgViewBox: viewBox, + }; + }; + + return shape; + } + function setWbTool(tool) { wbTool = wbTool === tool ? null : tool; @@ -3932,9 +3988,15 @@ canvas.style.cursor = ""; } - // Disable shape interaction when whiteboard tool is active - canvasContent.style.pointerEvents = wbTool ? "none" : ""; - wbOverlay.style.pointerEvents = wbTool ? "all" : "none"; + // Disable shape interaction when drawing tools are active. + // Eraser keeps canvasContent interactive so it can delete wb-drawing shapes. + if (wbTool && wbTool !== "eraser") { + canvasContent.style.pointerEvents = "none"; + wbOverlay.style.pointerEvents = "all"; + } else { + canvasContent.style.pointerEvents = ""; + wbOverlay.style.pointerEvents = wbTool === "eraser" ? "all" : "none"; + } syncBottomToolbar(); } @@ -4237,18 +4299,27 @@ if (!wbDrawing) return; wbDrawing = false; - // Persist the completed SVG element to Automerge + // Convert the completed SVG drawing into a folk-shape if (wbPreviewEl) { - const wbId = `wb-${Date.now()}-${++shapeCounter}`; - wbPreviewEl.setAttribute("data-wb-id", wbId); const svgMarkup = wbPreviewEl.outerHTML; + const bounds = computeWbBounds(svgMarkup); - sync.addShapeData({ - type: "wb-svg", - id: wbId, - svgMarkup, - x: 0, y: 0, width: 0, height: 0, rotation: 0, - }); + // Remove the temporary preview from the overlay + wbPreviewEl.remove(); + + if (bounds) { + const wbId = `wb-${Date.now()}-${++shapeCounter}`; + const shape = createWbShapeElement(svgMarkup, bounds.viewBox); + shape.id = wbId; + shape.x = bounds.x; + shape.y = bounds.y; + shape.width = bounds.width; + shape.height = bounds.height; + + setupShapeEventListeners(shape); + canvasContent.appendChild(shape); + sync.registerShape(shape); + } } wbPreviewEl = null; @@ -4260,8 +4331,7 @@ } }); - // Eraser click fallback — deletion is handled in pointerdown above, - // but catch any clicks that slip through (e.g. keyboard-triggered) + // Eraser click fallback for old SVG overlay elements wbOverlay.addEventListener("click", (e) => { if (wbTool !== "eraser") return; const hit = e.target; @@ -4272,6 +4342,24 @@ } }); + // Eraser for wb-drawing folk-shapes — capturing phase intercepts + // before normal shape interaction (drag/select) kicks in + canvasContent.addEventListener("pointerdown", (e) => { + if (wbTool !== "eraser") return; + const target = e.target.closest?.("[data-wb-drawing]"); + if (!target) return; + e.stopPropagation(); + e.preventDefault(); + const state = sync.getShapeVisualState(target.id); + if (state === "forgotten") { + sync.hardDeleteShape(target.id); + target.remove(); + } else { + sync.forgetShape(target.id, getLocalDID()); + target.forgotten = true; + } + }, true); + // ── Helper: get local user DID ── function getLocalDID() { try {