feat: arrow ↔ shape binding (arrows follow boxes when moved) - #110
Merged
Conversation
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>
Deploying whiteboard with
|
| 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 |
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>
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.
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/arrowsborderPoint(shape, toward)— clips the centre→target ray to the shape's bbox so the arrow touches the edge, not the middlebindArrowOnDraw— on draw, binds whichever end landed inside a shape and snaps it to the borderrerouteArrow— recomputes a bound arrow's endpoints (bound end → border facing the other end; unbound end stays), funnelled through the existingsetArrowEndpointslayout seamenableBindingPersistence()— serialisesid/startBinding/endBindingontoObject, so bindings survive reload and undo/redoarrowBindingHandler— onobject:moving/object:scalingof a bound shape, re-routes its arrows live. Reacting to the live gesture (notobject:modified) keeps the drop snapshot consistent, so undo restores shape + arrows together with no extra history plumbing.done()— callsbindArrowOnDraw.enableBindingPersistence()+ArrowBindingHandlerintoCanvas.Scope (v1) & follow-ups
Test plan
binding.test.js— 11 tests (borderPoint geometry, isBindable, shapeUnderPoint, boundArrows, rerouteArrow snap, bindArrowOnDraw, toObject persistence). Full suite 66 pass / 8 suites.🤖 Generated with Claude Code