Skip to content

feat: arrow labels (double-click an arrow to add centred text) - #100

Merged
Amark19 merged 6 commits into
masterfrom
feat/arrow-labels
Aug 16, 2026
Merged

feat: arrow labels (double-click an arrow to add centred text)#100
Amark19 merged 6 commits into
masterfrom
feat/arrow-labels

Conversation

@Amark19

@Amark19 Amark19 commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

What changed

  • Double-click an arrow → add a centred text label at its midpoint (eraser.io-style edge labels), reusing the shape-label editor.
  • An arrow can now carry an optional text child. Arrow detection is centralised in 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.
  • Resize (rebuildScaledArrow) and head-toggle (setArrowHeads) carry the label through the rebuild, re-centred on the new midpoint.
  • Editing pulls the text child out of the arrow group (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.
  • Arrow restyle leaves the label's own colour (consistent with shape labels).

Bug fix (latent, affected shapes too)

Clearing an existing label crashed: fabric's IText.exitEditing calls this.canvas.fire('object:modified') after the text:editing:exited handler removed the text (nulling text.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): isArrow variants (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)

  • Add / edit / clear a label on an arrow ✔
  • Resize keeps the label centred and the head undistorted, stroke width unchanged ✔
  • Re-edit round-trips (pull-out → edit → fold-back) ✔

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

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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 15, 2026

Copy link
Copy Markdown

Deploying whiteboard with  Cloudflare Pages  Cloudflare Pages

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

View logs

Amark19 and others added 5 commits August 16, 2026 01:37
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>
@Amark19
Amark19 merged commit 3ccda60 into master Aug 16, 2026
2 checks passed
@Amark19
Amark19 deleted the feat/arrow-labels branch August 16, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant