|
| 1 | +## Context |
| 2 | + |
| 3 | +`layouts/default.vue`'s `computeCallouts` places every callout of a slide in one pass: |
| 4 | +- **Code callouts** (grouped highlight spans per `<pre>`) and **slide callouts** (from `callouts` frontmatter) share one `placed: PlacedRect[]` list. |
| 5 | +- **Placement:** `placeCallout` (in `useHighlightLayout.ts`) tries right → left → below → above, with shelf candidates past already-placed boxes on the same side, and skips boxes whose step range can't overlap. |
| 6 | +- **Connectors:** each item gets `elbowPath(highlightRect, rect, side)` = `[anchor, bend, boxEdge]` as an SVG path. The small `#callout-arrowhead` marker goes on `marker-start` (the anchor end). |
| 7 | +- **Other cases:** bare arrows (no text) get a two-point stub `[point, box corner]`. A box containing its anchor gets no path. |
| 8 | +- **Remeasure:** `remeasureCallouts` recomputes paths after render against the boxes' real sizes, from `item.highlightRect` and `item.side`. |
| 9 | + |
| 10 | +Geometry and callouts are read from the slide-info ref (`liveFrontmatter`), which is Slidev's raw slide frontmatter. Slidev merges the headmatter's `defaults:` only into the compiled per-slide frontmatter module (`getFrontmatter` in `@slidev/cli`), which is what `$frontmatter` reflects. |
| 11 | + |
| 12 | +## Goals / Non-Goals |
| 13 | + |
| 14 | +**Goals:** |
| 15 | +- An `arrow` connector: straight, from the anchor to the box border, with a larger head into the box. It is the default. |
| 16 | +- `calloutStyle: elbow` per slide, or deck-wide through `defaults:`, reproducing today's connectors and placement exactly. |
| 17 | +- On `arrow` slides, boxes on one side of one obstacle stacked in their anchors' order, so the lines don't cross in the common case. |
| 18 | +- Pure, unit-tested geometry for both the arrow path and the stacking. |
| 19 | + |
| 20 | +**Non-Goals:** |
| 21 | +- A per-callout style. |
| 22 | +- A marker at the anchor end of `arrow` connectors. |
| 23 | +- Reordering on `elbow` slides. |
| 24 | +- Guaranteeing zero crossings (diagonals from anchors at very different x can still cross). |
| 25 | +- Changes to the ODP importer or the VS Code extension. |
| 26 | + |
| 27 | +## Decisions |
| 28 | + |
| 29 | +### 1. The style comes from `$frontmatter` |
| 30 | + |
| 31 | +`calloutStyle` is read as `$frontmatter.calloutStyle`, which includes the headmatter's `defaults:` merged under the slide's own keys. |
| 32 | +- `'elbow'` → elbow. |
| 33 | +- Absent or `'arrow'` → arrow. |
| 34 | +- Anything else → arrow, with a one-time console warning naming the slide. |
| 35 | + |
| 36 | +The editor never writes `calloutStyle`, so the frontmatter-only-patch staleness that made geometry and callouts read from the slide-info ref doesn't apply: a hand edit re-renders the slide. An e2e test covers `defaults:` so the assumption about Slidev's merge is pinned. |
| 37 | + |
| 38 | +*Alternative considered:* reading `data.headmatter.defaults` through the slide-info ref. Rejected: it isn't exposed there, and `$frontmatter` already has the merged value. |
| 39 | + |
| 40 | +### 2. `arrowPath(anchor, box)` in `useHighlightLayout.ts` |
| 41 | + |
| 42 | +`arrowPath(anchor: Point, box: Rect): Point[]` returns `[anchor, borderPoint]`, where `borderPoint` is the point on the ray from the box's centre towards `anchor` where it leaves the box. It uses the smaller of the x and y scale factors to reach the border (`t = min(hw / |dx|, hh / |dy|)`). |
| 43 | +- **Anchor on the centre:** returns `[]` (no connector). |
| 44 | +- **Anchor for code callouts:** the same highlight edge `elbowPath` uses, facing `side`. It moves into a small `anchorPoint(highlightRect, side)` helper that both paths share. |
| 45 | +- **Anchor for slide callouts:** the anchor point itself. |
| 46 | + |
| 47 | +The path keeps starting at the anchor, so existing e2e helpers that read a connector's start keep working. |
| 48 | + |
| 49 | +### 3. Two markers |
| 50 | + |
| 51 | +- **Elbow:** the existing `#callout-arrowhead` (7 × stroke width) stays on `marker-start` for `elbow` connectors and for bare arrows in both styles. |
| 52 | +- **Arrow:** a new `#callout-arrowhead-box` (10 × stroke width, `orient="auto"`, `refX` at the tip) goes on `marker-end` for `arrow` connectors. |
| 53 | + |
| 54 | +The slide's style (a computed `calloutStyle`) and each item's `boxless` flag decide the marker attribute (`headOnBox`) and the path (`connectorPath`, shared by `computeCallouts` and `remeasureCallouts`). |
| 55 | + |
| 56 | +### 4. Anchor-ordered stacking: `stackCallouts` |
| 57 | + |
| 58 | +`stackCallouts(entries, slideRect)` in `useHighlightLayout.ts` is pure. Each entry has: |
| 59 | +- `rect` (the result of `placeCallout`); |
| 60 | +- `side`; |
| 61 | +- `group` (a key identifying the obstacle: the code block's `<pre>` for code callouts, the anchor's element for slide callouts, a unique key for bare points); |
| 62 | +- `anchor` (the anchor's coordinate along the side: y for right/left, x for above/below); |
| 63 | +- `range` (step range); |
| 64 | +- `fixed` (a position override or `box:`). |
| 65 | + |
| 66 | +It returns the adjusted rects. For each group and side: |
| 67 | + |
| 68 | +1. **Order.** Sort the non-fixed entries by `anchor`. |
| 69 | +2. **Forward pass.** Each entry's main-axis coordinate starts at its level position: the anchor aligned with the box's top, clamped to the slide, as `candidateRect` computes it. It is then raised to just past (`GAP`) any earlier entry in the group whose range overlaps it. It is also pushed past any fixed or other-group box on the same column that it would overlap with an overlapping range. |
| 70 | +3. **Backward pass.** If the last entry runs past the slide's far edge, walk back from the end, pulling each entry before the next one (again only against range-overlapping entries) and clamping to the slide. A stack that doesn't fit at all degrades to clamped overlap, like `placeCallout`'s fallback. |
| 71 | + |
| 72 | +The cross-axis coordinate (x for right/left) is left as placed, so a column stays a column. |
| 73 | + |
| 74 | +In `computeCallouts`, placement is split into gathering (greedy placement plus metadata for every item) and finishing (stacking on `arrow` slides, then `editor.ensurePosition`, paths and `items`). Code and slide callouts are stacked together, because they share `placed`. On `elbow` slides the stacking step is skipped, so the result is identical to today. |
| 75 | + |
| 76 | +*Alternative considered:* re-running `placeCallout` in anchor order. Rejected: greedy side choice with shelf candidates can still invert neighbours, and it would change which side boxes land on. |
| 77 | + |
| 78 | +### 5. What stays the same |
| 79 | + |
| 80 | +- **No connector when needed:** `boxContainsAnchor` (label) and boxless callouts keep their current behaviour, with the bare-arrow stub and small head at the anchor. |
| 81 | +- **Dragging:** a dragged box is `fixed`. Its connector follows it in the new style as the box moves. |
| 82 | +- **Printing and export:** they render the same SVG, so no change is needed. |
| 83 | + |
| 84 | +## Risks / Trade-offs |
| 85 | + |
| 86 | +- **[Existing decks change look]** Every deck without `calloutStyle` switches to arrows, and some boxes move to follow their anchors' order. → A release note, plus `defaults: { calloutStyle: elbow }` restores the old look deck-wide in one line. |
| 87 | +- **[Diagonals crossing code text]** A box stacked far from its highlight draws a long diagonal over other lines. → Stacking keeps boxes as close to level with their anchors as possible, and `elbow` exists for dense code. |
| 88 | +- **[Stacking pushes a box into another group's space]** → The forward pass also avoids overlapping boxes from other groups (with range overlap). Genuine overcrowding degrades to clamped overlap, as today's fallback does. |
| 89 | +- **[Existing e2e expectations of the L shape]** → Specs that assert elbow geometry get `calloutStyle: elbow` in their fixture; specs that only read a connector's start are unaffected. |
| 90 | + |
| 91 | +## Open Questions |
| 92 | + |
| 93 | +None. |
0 commit comments