# Arrow Routing in tldraw — Research Notes

## Source Files Examined

| File | Role |
|------|------|
| `packages/tlschema/src/shapes/TLArrowShape.ts` | Schema: `kind` (`arc` \| `elbow`), `bend`, `elbowMidPoint`, arrowhead styles |
| `packages/tldraw/src/lib/shapes/arrow/arrow-types.ts` | Type definitions: `TLStraightArrowInfo`, `TLArcArrowInfo`, `TLElbowArrowInfo` |
| `packages/tldraw/src/lib/shapes/arrow/getArrowInfo.ts` | Top-level dispatch — cached per shape, branches on `kind` and `bend` |
| `packages/tldraw/src/lib/shapes/arrow/straight-arrow.ts` | Straight-line routing: intersection with bound geometry, arrowhead offsets |
| `packages/tldraw/src/lib/shapes/arrow/curved-arrow.ts` | Arc routing: three-point circle, sweep/large-arc flags |
| `packages/tldraw/src/lib/shapes/arrow/shared.ts` | Binding helpers, terminal resolution, mask clamping |
| `packages/tldraw/src/lib/shapes/arrow/ArrowPath.tsx` | SVG path generation for all three arrow types |
| `packages/tldraw/src/lib/shapes/arrow/elbow/definitions.ts` | Core elbow types: `ElbowArrowRoute`, `ElbowArrowTargetBox`, `ElbowArrowEdge`, side/axis enums |
| `packages/tldraw/src/lib/shapes/arrow/elbow/getElbowArrowInfo.tsx` | **Main elbow entry point** — terminal resolution, edge usability, gap/midpoint calculation, route dispatch, geometry casting |
| `packages/tldraw/src/lib/shapes/arrow/elbow/range.tsx` | Range arithmetic: create, expand, subtract, overlap |
| `packages/tldraw/src/lib/shapes/arrow/elbow/routes/ElbowArrowWorkingInfo.ts` | Mutable working state + geometric transforms (flip, rotate, transpose) |
| `packages/tldraw/src/lib/shapes/arrow/elbow/routes/ElbowArrowRouteBuilder.ts` | Builder pattern for constructing routes; collinear-point merging, Manhattan distance |
| `packages/tldraw/src/lib/shapes/arrow/elbow/routes/elbowArrowRoutes.tsx` | **The 4 canonical route functions** (`routeRightToLeft`, `routeRightToTop`, `routeRightToBottom`, `routeRightToRight`) + 16-entry transform lookup table |
| `packages/tldraw/src/lib/shapes/arrow/elbow/routes/routeArrowWithAutoEdgePicking.tsx` | Three edge-picking strategies: auto, partial (one side manual), fully manual; `pickBest` shortest-path selector |
| `packages/tldraw/src/lib/shapes/arrow/elbow/elbowArrowSnapLines.tsx` | Snap-line computation for aligning elbow segments to other arrows |
| `packages/tldraw/src/lib/bindings/arrow/ArrowBindingUtil.ts` | Binding lifecycle: keeps arrow geometry in sync when bound shapes move |

---

## Architecture Overview

```
getArrowInfo(editor, shape)
  ├─ kind === 'elbow'  →  getElbowArrowInfo()
  │     ├─ resolve terminals (binding → geometry → target point)
  │     ├─ compute usable edges per side (range subtraction when shapes overlap)
  │     ├─ calculate gaps (gapX, gapY) and midpoint ranges
  │     ├─ determine edge-picking mode:
  │     │     ├─ both sides manual  →  routeArrowWithManualEdgePicking
  │     │     ├─ one side manual    →  routeArrowWithPartialEdgePicking
  │     │     └─ neither manual     →  routeArrowWithAutoEdgePicking
  │     ├─ tryRouteArrow(info, aSide, bSide)
  │     │     ├─ lookup transform in 4×4 table (16 combos → 4 canonical fns)
  │     │     ├─ apply transform to working info
  │     │     ├─ run canonical route fn (e.g. routeRightToLeft)
  │     │     └─ reset transform
  │     ├─ castPathSegmentIntoGeometry (snap endpoints to actual shape outline)
  │     └─ fixTinyEndNubs (remove degenerate L-shaped stubs)
  │
  ├─ kind === 'arc' && |bend| < 8px  →  getStraightArrowInfo()
  ├─ kind === 'arc' && |bend| ≥ 8px  →  getCurvedArrowInfo()
  └─ fallback: elbow with no route   →  getStraightArrowInfo()
```

---

## Key Design Decisions

1. **Transform-based symmetry reduction.** Only 4 route functions exist (`rightToLeft`, `rightToTop`, `rightToBottom`, `rightToRight`). All 16 side-pair combinations are handled by applying flip/rotate/transpose transforms to the working info, running the canonical function, then inverting the transform on the output points. This is the single most important architectural choice — it keeps the routing code ~4× smaller than a naive approach.

2. **Three-tier edge picking.** The system supports fully automatic edge selection, partially manual (user pins one side), and fully manual (user pins both sides). If a manual pick fails, it falls back to auto with a `'fallback'` reason tag for debugging.

3. **Range-based edge usability.** Each of the 4 edges of a bounding box is modeled as a `ElbowArrowEdge` with a cross-axis range. When shapes overlap, `subtractRange` carves out the blocked portion. If no usable range remains, the edge is `null` and the terminal may be downgraded to a "point" (zero-size bounding box), which relaxes routing constraints at the cost of visual quality.

4. **Midpoint dragging.** The `elbowMidPoint` prop (0–1) controls where the "middle" segment of an elbow sits between the two shapes. The `ElbowArrowRouteBuilder.midpointHandle()` method records which segment is draggable and on which axis.

5. **Geometry casting.** Routes are initially computed against bounding boxes. `castPathSegmentIntoGeometry` then ray-casts the first/last segment into the actual shape outline (e.g., an ellipse or star), adjusting the endpoint and applying arrowhead offsets.

6. **Computed caching.** `getArrowInfo` uses `createComputedCache` keyed on shape id, with a custom equality check (`isEqualAllowingForFloatingPointErrors`) to avoid unnecessary recomputation when floating-point jitter is the only change.

7. **Self-binding special case.** When an arrow binds to the same shape on both ends, the system detects `a.targetShapeId === b.targetShapeId` and routes externally only if both snaps are edge-type.

---

## Elbow Route Variants (from ASCII art in source)

### routeRightToLeft — 5 variants
- **1:** Simple S-bend through midX (gap > 0)
- **2:** U-bend via midY when gap exists vertically but not horizontally
- **3:** Route around common bounding box bottom-right
- **4:** Route around common bounding box top-left
- **5:** Complex 8-point route when shapes overlap on X but have negative gap with midX available

### routeRightToTop — 6 variants
- **1:** L-bend (direct corner)
- **2:** S-bend through midX
- **3:** S-bend through midY
- **4:** Route around common bounding box corner
- **5:** 7-point route via B's expanded left edge
- **6:** 7-point route via A's expanded bottom edge

### routeRightToBottom
- Delegates to `routeRightToTop` with a Y-flip transform.

### routeRightToRight — 3 variants
- **1:** U-bend via common expanded right edge
- **2:** Route via B's expanded top/bottom (gap ≥ 0)
- **3:** Route via A's expanded top/bottom (gap ≤ 0)

---

## pickBest Algorithm

When heuristics don't yield an ideal route, `pickBest` tries all candidate (aSide, bSide) pairs:
- Generates route for each pair via `tryRouteArrow`
- Ranks by: (1) fewest points (corners), (2) shortest Manhattan distance (with a small bias per candidate to break ties deterministically)
- Returns the winner or `null`

---

## Snap Lines

`elbowArrowSnapLines.tsx` builds a `Map<angle, Set<ElbowArrowSnapLine>>` from all unselected elbow arrows. Each horizontal/vertical segment of an existing arrow becomes a snap line. When the user drags a new arrow, the system can query this map to align segments.

---

## Binding Lifecycle

`ArrowBindingUtil` hooks into `onAfterCreate`, `onAfterChange`, `onAfterChangeFromShape`, and `onAfterChangeToShape` to keep arrow geometry in sync. When a bound shape moves, the binding util triggers `arrowDidUpdate`, which re-resolves terminals and recomputes the route.

---

## Performance Notes

- The `createComputedCache` with `areRecordsEqual: (a, b) => a.props === b.props` means arrow info is only recomputed when the shape's props object reference changes (structural sharing from the store).
- `ElbowArrowWorkingInfo` mutates in place and resets, avoiding allocations during the route search.
- The transform lookup table (`routes` object in `elbowArrowRoutes.tsx`) is a static 4×4 map — O(1) dispatch.
