Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'fix(diagrams): arrow tool works from empty space' (#220) from fix/diagram-arrow-line into main

scott 4e9f33a6 5871cbdc

+36 -9
+6
CHANGELOG.md
··· 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 7 8 + ## [0.22.1] — 2026-04-03 9 + 10 + ### Fixed 11 + - Arrow tool now works from empty canvas space, not just shape-to-shape (#348) 12 + - Arrow endpoints support mixed types (shape anchor or free-standing point) 13 + 8 14 ## [0.22.0] — 2026-04-03 9 15 10 16 ### Added
+1 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.22.0", 3 + "version": "0.22.1", 4 4 "private": true, 5 5 "type": "module", 6 6 "main": "electron/main.js",
+29 -8
src/diagrams/main.ts
··· 1009 1009 return; 1010 1010 1011 1011 } else if (activeTool === 'arrow') { 1012 + isDrawingArrow = true; 1012 1013 const hit = shapeAtPoint(wb, pt.x, pt.y); 1013 1014 if (hit) { 1014 - isDrawingArrow = true; 1015 1015 arrowFromShape = hit.id; 1016 1016 arrowFromAnchor = nearestEdgeAnchor(hit, pt.x, pt.y); 1017 + } else { 1018 + // Free-standing arrow from empty space 1019 + arrowFromShape = null; 1020 + arrowFromAnchor = { anchor: 'center', x: pt.x, y: pt.y }; 1017 1021 } 1018 1022 1019 1023 } else if (activeTool === 'freehand' || activeTool === 'highlighter') { ··· 1223 1227 syncToYjs(); 1224 1228 } 1225 1229 1226 - if (isDrawingArrow && arrowFromShape && arrowFromAnchor) { 1230 + if (isDrawingArrow && arrowFromAnchor) { 1227 1231 const pt = screenToCanvas(e.clientX, e.clientY); 1228 1232 const hit = shapeAtPoint(wb, pt.x, pt.y); 1233 + 1234 + // Build from endpoint 1235 + let fromEp: ArrowEndpoint; 1236 + if (arrowFromShape) { 1237 + fromEp = { shapeId: arrowFromShape, anchor: arrowFromAnchor.anchor as 'top' | 'bottom' | 'left' | 'right' | 'center' }; 1238 + } else { 1239 + fromEp = { x: arrowFromAnchor.x, y: arrowFromAnchor.y }; 1240 + } 1241 + 1242 + // Build to endpoint 1243 + let toEp: ArrowEndpoint; 1229 1244 if (hit && hit.id !== arrowFromShape) { 1245 + const toAnchorInfo = nearestEdgeAnchor(hit, pt.x, pt.y); 1246 + toEp = { shapeId: hit.id, anchor: toAnchorInfo.anchor }; 1247 + } else { 1248 + toEp = { x: pt.x, y: pt.y }; 1249 + } 1250 + 1251 + // Only create arrow if endpoints are different positions 1252 + const fromPt = arrowFromAnchor; 1253 + const dist = Math.sqrt((pt.x - fromPt.x) ** 2 + (pt.y - fromPt.y) ** 2); 1254 + if (dist > 5) { 1230 1255 pushHistory(); 1231 - const fromAnchor = arrowFromAnchor.anchor as 'top' | 'bottom' | 'left' | 'right'; 1232 - const toAnchorInfo = nearestEdgeAnchor(hit, arrowFromAnchor.x, arrowFromAnchor.y); 1233 - wb = addArrow(wb, 1234 - { shapeId: arrowFromShape, anchor: fromAnchor }, 1235 - { shapeId: hit.id, anchor: toAnchorInfo.anchor }, 1236 - ); 1256 + wb = addArrow(wb, fromEp, toEp); 1237 1257 syncToYjs(); 1238 1258 } 1259 + 1239 1260 removeArrowPreview(); 1240 1261 arrowHoverTargetId = null; 1241 1262 isDrawingArrow = false;