feat: arrow labels (double-click an arrow to add centred text) - #100
Merged
Conversation
Extends labels to arrows (eraser.io-style edge labels), reusing the label editor. An arrow can now carry an OPTIONAL text child; detection is centralised in shapeLabel's isArrow (single source of truth, shared by the resize handler and properties panel) so an arrow stays an arrow with or without a label and survives undo/reload without custom props. - buildArrowGroup: optional label at the line midpoint (defensive about an undefined fontFamily, which crashes fabric's font cache). - editing: pull the text child OUT of the arrow group to edit it (addWithUpdate/removeWithUpdate), keeping the arrow's own group config; fold it back on exit, or drop it if left empty. - resize (rebuildScaledArrow) and head-toggle (setArrowHeads) carry the label through the rebuild, re-centred on the new midpoint. - arrow restyle leaves the label's own colour (like shape labels). Also fixes a latent crash (shapes too): clearing an existing label made fabric's exitEditing tail call this.canvas.fire after we removed the text — restore the canvas back-ref so the trailing fire is a harmless no-op. Tests: 32 total (+7 arrow) — isArrow variants, label folds into the arrow, empty leaves a plain arrow. Verified in-browser: add/edit/clear label, resize keeps it centred + head undistorted, re-edit round-trips. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deploying whiteboard with
|
| Latest commit: |
6ce41a5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4ea85a4a.whiteboard-988.pages.dev |
| Branch Preview URL: | https://feat-arrow-labels.whiteboard-988.pages.dev |
An arrow label sat directly on the line, so the line struck through the text. Give the label a backgroundColor matching the board (read from the body background) so it masks the line behind the text, excalidraw-style. Carried through the resize/head-toggle rebuilds and applied on re-edit (retro-fitting older labels). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
onScaling counter-scaled the head(s) but not the text label, so the label ballooned/shrank with the group during a resize drag (snapping back only on drop). Extract counterScaleArrowDecorations — counter-scales every non-line child (heads AND label) by 1/|groupScale| each frame — and use it in onScaling. Line still stretches (arrow length). +2 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Textbox has a fixed width, so a multi-word arrow label wrapped into a cramped narrow block (e.g. 'sahi hai kya' stacked over 3 lines) that stayed cramped even as the arrow was extended. Use IText for arrow labels so they size to their content on a single line, like excalidraw. Shape labels keep Textbox (they intentionally wrap to fit inside the shape). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the arrow's bounding-box corner-scaling with excalidraw-style endpoint handles: two draggable controls at the tail (e1) and tip (e2). Dragging one re-aims/extends the arrow by mutating the line + head(s) + label IN PLACE (no rebuild mid-drag), keeping the group centre fixed so children keep rendering; the group's bounds are re-fitted on drop (mirroring fabric's addWithUpdate: restore state -> reset transform -> recalc bounds -> re-base children, which preserves absolute positions). - new utils/arrowEndpoints.js: reshapeArrow, refitArrowBounds, custom fabric Controls (position/action/render + mouseUp refit). screenMatrix guards the no-canvas case (buildArrowGroup setCoords() pre-add). - buildArrowGroup attaches the endpoint controls instead of hiding side handles; scale stays 1 (no bbox scaling). - 4 tests: endpoint move keeps the other end, head+label follow, refit preserves absolute positions. 38 total. Multi-select scaling still rebuilds arrows via the resize handler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Endpoint handle cursor: crosshair -> pointer (matches excalidraw). - Extract applyEndpointsLocal() as the single source of truth for arrow layout (line + head(s) + label from two endpoints); reshapeArrow (drag one end) delegates to it. - Add setArrowEndpoints(group, tailScene, tipScene): re-route from absolute coords + re-fit — the entry point a future shape-binding (A3) calls when a bound shape moves. Documented the extension seams for binding (needs a persisted arrow<->shape ref — the real A3 work) and bending/elbow (A2: the line child becomes a polyline). +1 test (39 total). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
shapeLabel.isArrow(single source of truth, shared by the resize handler + properties panel) so an arrow stays an arrow with or without a label, and survives undo/reload with no custom props.rebuildScaledArrow) and head-toggle (setArrowHeads) carry the label through the rebuild, re-centred on the new midpoint.add/removeWithUpdate) so the arrow's own group config (perPixelTargetFind, hidden side handles, objectCaching) is preserved; folds it back on exit, or drops it if left empty.Bug fix (latent, affected shapes too)
Clearing an existing label crashed: fabric's
IText.exitEditingcallsthis.canvas.fire('object:modified')after thetext:editing:exitedhandler removed the text (nullingtext.canvas). Restore the canvas back-ref so the trailing fire is a harmless no-op. Fixed at all three removal sites (arrow label, shape label, generic text handler).Tests
32 total (+7 arrow):
isArrowvariants (single/double head, with-label, not-a-labeled-shape), label folds into the arrow group, empty leaves a plain arrow.npm run test:unit, lint, and format all green.Manual verification (in-browser)
Known cosmetic follow-up
A short label wraps to 2 lines with the arrow passing through the gap; a single-word label would be struck through by the line. Options for a later pass: wider textbox + canvas-coloured background mask (excalidraw's approach) or a small perpendicular offset.
🤖 Generated with Claude Code