From cc45fcae54ce7f1951df766c2ef636d631c11edb Mon Sep 17 00:00:00 2001 From: SplinterSword Date: Thu, 20 Nov 2025 17:43:36 +0530 Subject: [PATCH 01/19] Added curve Support Signed-off-by: SplinterSword --- site/src/components/ShapeBuilder/index.js | 395 +++++++++++------- .../ShapeBuilder/shapeBuilder.styles.js | 1 + 2 files changed, 252 insertions(+), 144 deletions(-) diff --git a/site/src/components/ShapeBuilder/index.js b/site/src/components/ShapeBuilder/index.js index 4bd3832..a260369 100644 --- a/site/src/components/ShapeBuilder/index.js +++ b/site/src/components/ShapeBuilder/index.js @@ -1,197 +1,304 @@ -// /* global window */ +// Updated ShapeBuilder with Curved Drawing Support (Figma-like) +// Style preserved from your original component + import React, { useEffect, useRef, useState } from "react"; import { Wrapper, CanvasContainer, OutputBox, StyledSVG } from "./shapeBuilder.styles"; import { Button, Typography, Box } from "@layer5/sistent"; -import { SVG, extend as SVGextend } from "@svgdotjs/svg.js"; -import draw from "@svgdotjs/svg.draw.js"; -SVGextend(SVG.Polygon, draw); +const defaultStroke = "#00B39F"; + +function getSvgPoint(svg, clientX, clientY) { + if (!svg) return { x: clientX, y: clientY }; + const pt = svg.createSVGPoint(); + pt.x = clientX; + pt.y = clientY; + return pt.matrixTransform(svg.getScreenCTM().inverse()); +} const ShapeBuilder = () => { const boardRef = useRef(null); - const polyRef = useRef(null); - const keyHandlersRef = useRef({}); + const [mousePoint, setMousePoint] = useState(null); + const [anchors, setAnchors] = useState([]); // {x,y, handleIn:{x,y}, handleOut:{x,y}} + const [isClosed, setIsClosed] = useState(false); + const [dragState, setDragState] = useState(null); const [result, setResult] = useState(""); - const [error, setError] = useState(null); - const getPlottedPoints = (poly) => { - if (!poly) return null; - const plotted = poly.plot(); - const points = Array.isArray(plotted) ? plotted : plotted?.value; - return Array.isArray(points) ? points : null; - }; + // deep clone anchors helper + const cloneAnchors = (arr) => arr.map(a => ({ + x: a.x, y: a.y, + handleIn: { x: a.handleIn.x, y: a.handleIn.y }, + handleOut: { x: a.handleOut.x, y: a.handleOut.y } + })); - const showCytoArray = () => { - const poly = polyRef.current; - if (!poly) return; - - try { - const points = getPlottedPoints(poly); - if (!points) throw new Error("Invalid or empty polygon points"); - - const normalized = points - .map(([x, y]) => [(x - 260) / 260, (y - 260) / 260]) - .flat() - .join(" "); - setResult(normalized); - setError(null); - } catch (err) { - setError("Failed to extract and normalize polygon points."); - console.error("showCytoArray error:", err); + // Add a new anchor and optionally begin placing (dragging handle) + const addAnchor = (x, y, placing = true) => { + const newAnchor = { x, y, handleIn: { x, y }, handleOut: { x, y } }; + setAnchors(prev => { + const next = cloneAnchors(prev); + next.push(newAnchor); + return next; + }); + + if (placing) { + // index will be previous length + setDragState(prev => ({ type: 'placing', index: (anchors.length), start: { x, y } })); } }; - const handleMaximize = () => { - const poly = polyRef.current; - if (!poly) return; - - const points = getPlottedPoints(poly); - if (!points) return; - const xs = points.map(p => p[0]); - const ys = points.map(p => p[1]); - - const width = Math.abs(Math.max(...xs) - Math.min(...xs)); - const height = Math.abs(Math.max(...ys) - Math.min(...ys)); - - poly.size(width > height ? 520 : undefined, height >= width ? 520 : undefined); - poly.move(0, 0); - showCytoArray(); + const updateAnchorHandle = (index, handleKey, hx, hy, symmetric = true) => { + setAnchors(prev => { + const next = cloneAnchors(prev); + if (!next[index]) return prev; + next[index][handleKey] = { x: hx, y: hy }; + if (symmetric) { + const ax = next[index].x; + const ay = next[index].y; + const dx = hx - ax; + const dy = hy - ay; + const opposite = handleKey === 'handleOut' ? 'handleIn' : 'handleOut'; + next[index][opposite] = { x: ax - dx, y: ay - dy }; + } + return next; + }); }; - const handleKeyDown = (e) => { - const poly = polyRef.current; - if (!poly) return; + const updatePathOnMove = (clientX, clientY) => { + if (!boardRef.current) return; + const pt = getSvgPoint(boardRef.current, clientX, clientY); + if (!dragState) return; - if (e.ctrlKey) { - poly.draw("param", "snapToGrid", 0.001); + if (dragState.type === 'placing') { + updateAnchorHandle(dragState.index, 'handleOut', pt.x, pt.y, true); + } else if (dragState.type === 'handle') { + updateAnchorHandle(dragState.index, dragState.handleKey, pt.x, pt.y, dragState.symmetric); } + }; - if (e.key === "Enter") { - poly.draw("done"); - poly.fill("#00B39F"); - showCytoArray(); - } + // Mouse handlers + const onMouseDown = (e) => { + // left button only + if (e.button !== 0) return; + if (isClosed) return; - if (e.ctrlKey && e.key.toLowerCase() === "z") { - const points = getPlottedPoints(poly); - if (!points) return; - poly.plot(points.slice(0, -1)); - } + const pt = getSvgPoint(boardRef.current, e.clientX, e.clientY); + addAnchor(pt.x, pt.y, true); }; - const handleKeyUp = (e) => { - const poly = polyRef.current; - if (!poly || e.ctrlKey) return; - poly.draw("param", "snapToGrid", 16); + const onMouseMove = (e) => { + // update preview point + if (!boardRef.current) return; + const pt = getSvgPoint(boardRef.current, e.clientX, e.clientY); + setMousePoint(pt); + if (dragState) { + updatePathOnMove(e.clientX, e.clientY); + } }; - const attachKeyListeners = () => { - document.addEventListener("keydown", handleKeyDown); - document.addEventListener("keyup", handleKeyUp); - keyHandlersRef.current = { handleKeyDown, handleKeyUp }; + const onMouseUp = (e) => { + // finalize placing/dragging + setDragState(null); }; - const detachKeyListeners = () => { - const { handleKeyDown, handleKeyUp } = keyHandlersRef.current; - if (handleKeyDown) document.removeEventListener("keydown", handleKeyDown); - if (handleKeyUp) document.removeEventListener("keyup", handleKeyUp); - keyHandlersRef.current = {}; + const onHandleMouseDown = (e, index, handleKey) => { + e.stopPropagation(); + const symmetric = !e.shiftKey; // shift decouples handles + setDragState({ type: 'handle', index, handleKey, symmetric }); }; - const initializeDrawing = () => { - if (!boardRef.current) { - setError("Canvas reference not found"); - return; - } - - try { - const draw = SVG() - .addTo(boardRef.current) - .size("100%", "100%") - .polygon() - .draw() - .attr({ stroke: "#00B39F", "stroke-width": 1, fill: "none" }); - - draw.draw("param", "snapToGrid", 16); - draw.on("drawstart", attachKeyListeners); - draw.on("drawdone", detachKeyListeners); - - polyRef.current = draw; - setError(null); - } catch (err) { - setError(`Failed to initialize drawing: ${err.message}`); - } + const onAnchorMouseDown = (e, index) => { + e.stopPropagation(); + const start = getSvgPoint(boardRef.current, e.clientX, e.clientY); + setDragState({ type: 'moveAnchor', index, start }); }; - const clearShape = () => { - const poly = polyRef.current; - if (!poly) return; - - poly.draw("cancel"); - poly.remove(); - detachKeyListeners(); - polyRef.current = null; - setResult(""); - initializeDrawing(); - }; + // move anchor effect + useEffect(() => { + if (!dragState || dragState.type !== 'moveAnchor') return; - const closeShape = () => { - const poly = polyRef.current; - if (!poly) return; + const move = (ev) => { + const pt = getSvgPoint(boardRef.current, ev.clientX, ev.clientY); + setAnchors(prev => { + const next = cloneAnchors(prev); + const idx = dragState.index; + if (!next[idx]) return prev; + const dx = pt.x - dragState.start.x; + const dy = pt.y - dragState.start.y; + next[idx].x += dx; next[idx].y += dy; + next[idx].handleIn.x += dx; next[idx].handleIn.y += dy; + next[idx].handleOut.x += dx; next[idx].handleOut.y += dy; + return next; + }); + setDragState(s => ({ ...s, start: pt })); + }; - poly.draw("done"); - poly.fill("#00B39F"); - showCytoArray(); - }; + const up = () => setDragState(null); + window.addEventListener('mousemove', move); + window.addEventListener('mouseup', up); + return () => { + window.removeEventListener('mousemove', move); + window.removeEventListener('mouseup', up); + }; + }, [dragState]); + // global handle/placing drag listeners useEffect(() => { - initializeDrawing(); + if (!dragState) return; + if (dragState.type !== 'handle' && dragState.type !== 'placing') return; + + const onMove = (ev) => updatePathOnMove(ev.clientX, ev.clientY); + const onUp = () => setDragState(null); + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); return () => { - detachKeyListeners(); - if (polyRef.current) { - polyRef.current.draw("cancel"); - polyRef.current.remove(); - polyRef.current = null; + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + }; + }, [dragState]); + + // keyboard handlers + useEffect(() => { + const onKeyDown = (e) => { + if (e.key === 'Enter' && anchors.length >= 3) { + setIsClosed(true); + } + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') { + setAnchors(prev => prev.slice(0, -1)); + setIsClosed(false); + } + if (e.key === 'Escape') { + // Close shape on ESC + if (anchors.length >= 3) { + setIsClosed(true); + } + setDragState(null); } }; - }, []); + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [anchors]); + + const buildPathD = () => { + if (anchors.length === 0) return ''; + let d = `M ${anchors[0].x} ${anchors[0].y}`; + for (let i = 1; i < anchors.length; i++) { + const prev = anchors[i - 1]; + const curr = anchors[i]; + d += ` C ${prev.handleOut.x} ${prev.handleOut.y}, ${curr.handleIn.x} ${curr.handleIn.y}, ${curr.x} ${curr.y}`; + } + if (isClosed && anchors.length >= 2) { + const last = anchors[anchors.length - 1]; + const first = anchors[0]; + d += ` C ${last.handleOut.x} ${last.handleOut.y}, ${first.handleIn.x} ${first.handleIn.y}, ${first.x} ${first.y} Z`; + } + return d; + }; + + // Export SVG path d instead of normalized points + const computeExportString = () => { + return buildPathD(); + }; + + useEffect(() => { + setResult(computeExportString()); + }, [anchors, isClosed]); + + const clear = () => { + setAnchors([]); + setIsClosed(false); + setDragState(null); + setResult(''); + }; return ( - + - - + + + + + + + - + + + + {/* path preview */} + + + {/* preview mouse point */} + {mousePoint && anchors.length > 0 && !isClosed && ( + + )} + + {mousePoint && !isClosed && ( + + )} + + {/* anchors, handles */} + {anchors.map((a, idx) => ( + + + + + onHandleMouseDown(e, idx, 'handleIn')} + /> + + onHandleMouseDown(e, idx, 'handleOut')} + /> + + onAnchorMouseDown(e, idx)} + /> + + ))} - {error && ( -
- {error} -
- )}
- - - - + + + - Polygon Coordinates (SVG format): + SVG Path (d attribute):