Skip to content

feat: arrow ↔ shape binding (arrows follow boxes when moved) - #110

Merged
Amark19 merged 8 commits into
masterfrom
feat/arrow-shape-binding
Aug 23, 2026
Merged

feat: arrow ↔ shape binding (arrows follow boxes when moved)#110
Amark19 merged 8 commits into
masterfrom
feat/arrow-shape-binding

Conversation

@Amark19

@Amark19 Amark19 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The defining eraser.io behavior: draw an arrow onto a box, move the box, and the arrow follows — staying anchored to the box's edge. First slice of arrow↔shape binding.

What changed

  • utils/binding.js — the model:
    • ensureId — stable ids on binding targets/arrows
    • borderPoint(shape, toward) — clips the centre→target ray to the shape's bbox so the arrow touches the edge, not the middle
    • bindArrowOnDraw — on draw, binds whichever end landed inside a shape and snaps it to the border
    • rerouteArrow — recomputes a bound arrow's endpoints (bound end → border facing the other end; unbound end stays), funnelled through the existing setArrowEndpoints layout seam
    • enableBindingPersistence() — serialises id/startBinding/endBinding on toObject, so bindings survive reload and undo/redo
  • arrowBindingHandler — on object:moving/object:scaling of a bound shape, re-routes its arrows live. Reacting to the live gesture (not object:modified) keeps the drop snapshot consistent, so undo restores shape + arrows together with no extra history plumbing.
  • arrow tool done() — calls bindArrowOnDraw.
  • Wired enableBindingPersistence() + ArrowBindingHandler into Canvas.

Scope (v1) & follow-ups

  • v1 binds on draw and reroutes on single-shape move/resize. Deliberately deferred: binding by dragging an existing arrow's endpoint onto a shape, multi-select move reroute, unbind-on-delete, and true edge-geometry for ellipse/diamond (v1 uses the bounding box). Called out so they're tracked, not forgotten.

Test plan

  • Unit: binding.test.js — 11 tests (borderPoint geometry, isBindable, shapeUnderPoint, boundArrows, rerouteArrow snap, bindArrowOnDraw, toObject persistence). Full suite 66 pass / 8 suites.
  • Manual (in-browser): drew A→B via the real arrow tool → both ends bound; moved and resized each box → arrow follows, anchored to the border, arrowhead re-aimed; reloaded → bindings persisted and reroute still works on the restored scene.

🤖 Generated with Claude Code

Draw an arrow so an end lands on a shape and that end binds to the shape;
moving or resizing the shape re-routes the arrow so its bound end stays
glued to the shape's border, facing the other end (eraser.io style).

- utils/binding.js: stable persisted ids (ensureId), border-anchor geometry
  (borderPoint clips the centre->target ray to the shape's bbox edge),
  bind-on-draw (bindArrowOnDraw), and rerouteArrow — all funnelling through
  the existing setArrowEndpoints layout seam. enableBindingPersistence()
  serialises id/startBinding/endBinding on toObject so bindings survive
  reload and undo/redo.
- arrowBindingHandler: on object:moving/scaling of a bound shape, re-route
  its arrows live (single-shape for v1; drop snapshot stays consistent).
- arrow tool done(): bind whichever end was dropped on a shape.
- binding.test.js (11 tests) + wired into Canvas.

Verified in-browser: draw A->B, move/resize either box, arrow follows and
stays anchored to the border; bindings persist across reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying whiteboard with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5aa1350
Status: ✅  Deploy successful!
Preview URL: https://76522a09.whiteboard-988.pages.dev
Branch Preview URL: https://feat-arrow-shape-binding.whiteboard-988.pages.dev

View logs

Amark19 and others added 7 commits August 17, 2026 19:56
Binding only fired when an arrow endpoint landed strictly inside a shape's
bbox, but people draw arrows TO the edge (or just past it), so nothing bound
and moving a box did nothing. Add a BIND_MARGIN (24px) slack to the draw-time
hit test; the bound end still snaps to the border via rerouteArrow, so the
extra reach is free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was no signal that an arrow would bind. Now, while drawing, the shape
the endpoint is over gets an indigo outline (Excalidraw-style), so binding is
visible before release; it clears on drop.

- bindingHighlight.js: transient, non-interactive, excludeFromExport overlay
  outlining the target; kept on canvas.__bindHighlight, flagged __nonBindable
  so it can't be its own target.
- isBindable skips __nonBindable; shapeUnderPoint accepts an array exclude so
  the arrow tool can ignore its own preview line/head while highlighting.
- arrow tool: highlight on draw, clear on done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…highlight

Addresses binding UX feedback:
- Endpoint drag now (re)binds: drop an arrow endpoint on a shape to bind it,
  drop it in empty space to unbind — no more stale binding snapping back.
  arrowEndpoints fires arrow:endpoint:moving/up; arrowBindingHandler commits it.
- Attach at ANY reachable border point: each bound end stores a normalized
  anchor (fraction of the shape's half-extents) and re-routes to that side/
  corner, so an arrow keeps its attach point as the shape moves — instead of
  always snapping to the single centre-facing point. Near-centre anchors fall
  back to centre-facing so they still land on a clean edge.
- Highlight is now subtle (soft indigo tint + 1.5px border) and shows on BOTH
  the source and destination shapes while drawing/dragging an endpoint.

startAnchor/endAnchor persist alongside the bindings. Tests: 72 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bind highlight was a heavy filled box. Make it a thin (2px) violet-indigo
outline with no fill that hugs the shape's border — a clean hover affordance
like eraser.io, on both source and destination.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s target

- Clear the bind highlight on any canvas mouse:up (catch-all), so a stray
  highlight can never persist past a release, whatever ended the gesture.
- Dragging one arrow endpoint now highlights only the shape THAT endpoint is
  over — not the unchanged other/source end, which just added clutter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default strokeWidth (3) wasn't one of the Width presets (2/4/8), so no
Width button highlighted for a default shape. Set presets to Thin/Medium/Bold
= 2/3/6 so the default (3) = Medium and highlights; existing default shapes
(also 3) now highlight too. Also snap any legacy stored width to the nearest
preset (mirrors the font-size snap), so the control is never blank.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
borderPoint clipped the ray to the axis-aligned bounding box — exact for
rectangles, but for a circle/ellipse the endpoint landed on the bounding
square, leaving a visible gap between the arrowhead and the curve. Use a true
ray-ellipse intersection for ellipse/circle; rect and others keep bbox
clipping (exact for rects, close for polygons).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Amark19
Amark19 merged commit fca0f06 into master Aug 23, 2026
2 checks passed
@Amark19
Amark19 deleted the feat/arrow-shape-binding branch August 23, 2026 17:14
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