diff --git a/README.md b/README.md index 413525b..236412b 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,9 @@ Create polylines made of straight or curved segments. **Editing:** -- **Toggle corner/curve:** Click a point to switch between corner and smooth curve (handles will appear). -- **Adjust curvature:**: Drag the handles to refine the shape. +- **Delete points:** Press `Del` or `Backspace` while points are selected to remove those points from the path. +- **Toggle corner/curve:** Hold `Alt` (or `Option` on Mac) and click a point to switch between corner and smooth curve (handles will appear when a single point is selected). +- **Adjust curvature:** Drag the handles to refine the shape. - **Sharp corner between curves:** Hold `Alt` (or `Option` on Mac) while dragging a handle to move it independently. - **Re-link handles:** Double-click the point to snap handles back together. diff --git a/src/path/PathEditor.svelte b/src/path/PathEditor.svelte index e12212d..38bb168 100644 --- a/src/path/PathEditor.svelte +++ b/src/path/PathEditor.svelte @@ -37,7 +37,7 @@ let visibleMidpoint: number | undefined; let isHandleHovered = false; let lastHandleClick: number | null = null; - let selectedCorner: number | null = null; + let selectedCorners: number[] = []; let isAltPressed = false; @@ -69,6 +69,11 @@ /** Determine visible midpoint, if any **/ const onPointerMove = (evt: PointerEvent) => { + if (selectedCorners.length > 0 || !midpoints.some(m => m.visible)) { + visibleMidpoint = undefined; + return; + } + const [px, py] = transform.elementToImage(evt.offsetX, evt.offsetY); const getDistSq = (pt: number[]) => @@ -96,7 +101,22 @@ visibleMidpoint = undefined; } - const onShapePointerUp = () => selectedCorner = null; + /** + * SVG element keeps losing focus when interacting with + * shapes–this function refocuses. + */ + const reclaimFocus = () => { + if (document.activeElement !== svgEl) + svgEl.focus(); + } + + /** + * De-selects all corners and reclaims focus. + */ + const onShapePointerUp = () => { + selectedCorners = []; + reclaimFocus(); + } /** * Updates state, waiting for potential click. @@ -111,30 +131,37 @@ } /** Selection handling logic **/ - const onHandlePointerUp = (idx: number) => () => { + const onHandlePointerUp = (idx: number) => (evt: PointerEvent) => { if (!lastHandleClick) return; // Drag, not click if (performance.now() - lastHandleClick > CLICK_THRESHOLD) return; - // Click on a CORNER instantly selects and converts to curve - const { type } = geom.points[idx]; - - if (type === 'CORNER') { - selectedCorner = idx; + const isSelected = selectedCorners.includes(idx); + // Clicking on a handle with alt key pressed toggles between corner/curve + if (isAltPressed) { const polyline = togglePolylineCorner(shape, idx, viewportScale); dispatch('change', polyline); - } else { - const isSelected = selectedCorner === idx; - if (isSelected) { - // If already selected, toggle to corner - const polyline = togglePolylineCorner(shape, idx,viewportScale); - dispatch('change', polyline); - } else { - // Just select - selectedCorner = idx; + + // Ensure the toggled corner is selected, and deselect others + if (!isSelected || selectedCorners.length > 1) { + selectedCorners = [idx]; } + } else if (evt.metaKey || evt.ctrlKey || evt.shiftKey) { + if (isSelected) + selectedCorners = selectedCorners.filter(i => i !== idx); + else + selectedCorners = [...selectedCorners, idx]; + } else { + if (isSelected && selectedCorners.length > 1) + // Keep selected, de-select others + selectedCorners = [idx] + else if (isSelected) + // De-select + selectedCorners = []; + else + selectedCorners = [idx]; } } @@ -292,7 +319,7 @@ const onAddPoint = (midpointIdx: number) => async (evt: PointerEvent) => { evt.stopPropagation(); - selectedCorner = null; + // selectedCorner = null; const points = [ ...geom.points.slice(0, midpointIdx + 1), @@ -328,13 +355,11 @@ const onAddPoint = (midpointIdx: number) => async (evt: PointerEvent) => { } const onDeleteSelected = () => { - if (selectedCorner === null) return; - // Open path needs 2 points min, closed path needs 3 - const minLen = geom.closed ? 4 : 3; - if (geom.points.length < minLen) return; + const minLen = geom.closed ? 3 : 2; + if (geom.points.length - selectedCorners.length < minLen) return; - const points = geom.points.filter((_, i) => i !== selectedCorner); + const points = geom.points.filter((_, i) => !selectedCorners.includes(i)); const bounds = boundsFromPoints(approximateAsPolygon(points, geom.closed)); dispatch('change', { @@ -346,7 +371,7 @@ const onAddPoint = (midpointIdx: number) => async (evt: PointerEvent) => { } }); - selectedCorner = null; + selectedCorners = []; } onMount(() => { @@ -419,8 +444,9 @@ const onAddPoint = (midpointIdx: number) => async (evt: PointerEvent) => { d={d} /> - - {#if selectedCorner !== null} + + {#if selectedCorners.length === 1} + {@const selectedCorner = selectedCorners[0]} {@const corner = geom.points[selectedCorner]} {#if corner.type === 'CURVE'} {#if corner.inHandle} @@ -447,6 +473,7 @@ const onAddPoint = (midpointIdx: number) => async (evt: PointerEvent) => { x={pt.point[0]} y={pt.point[1]} scale={viewportScale} + selected={selectedCorners.includes(idx)} on:dblclick={onDoubleClick(idx)} on:pointerenter={onEnterHandle} on:pointerleave={onLeaveHandle}