Skip to content

feat(graph): interactive drag, wheel zoom, pan, hover highlight (G2-G5)#171

Merged
thetechjon merged 1 commit into
devfrom
feat/graph-interactive-g2-g5
Jun 10, 2026
Merged

feat(graph): interactive drag, wheel zoom, pan, hover highlight (G2-G5)#171
thetechjon merged 1 commit into
devfrom
feat/graph-interactive-g2-g5

Conversation

@thetechjon

Copy link
Copy Markdown
Collaborator

Makes the noteser-graph fullscreen view feel like Obsidian by consuming the v1.3 interaction surface (L1-L4). Only public/plugins/noteser-graph/ and a new test under src/__tests__/ are touched; src/plugins (the platform) is untouched.

Behaviours

G2 — wheel zoom + drag pan. The svg declares panZoom: 'host', so the host owns the viewBox: wheel-zoom and dragging the background pan instantly with no worker round-trip. On settle the host emits one surface.transform {x,y,scale} event; the plugin reconstructs the viewport box (viewportFromTransform) and persists it under g2.viewport, so the viewport survives a reload. The nine manual zoom/pan buttons are deleted; a single Reset view button remains (net code removal).

G3 — node drag (pin/unpin). onPointerDown pins a node (the host auto-captures the pointer because the circle declares both down + move); onPointerMove drags it to payload.{x,y} (already svg user-space). The simulation reheats around pinned nodes (simulationStep skips integration for fixed nodes) and streams only the moved coordinates via ctx.patchSvgPositions({ viewId, patches }) each frame (L4 patch channel) — no full-tree re-emit, so a 500-node drag holds 60fps. A pinned node keeps a distinct ring; click a pinned node to unpin, or use Release pinned.

G4 — hover highlight. onPointerEnter/onPointerLeave dim every non-neighbour circle and brighten/thicken the hovered node's edges (hoverNeighbours). Recolor only — no re-layout, no patch channel.

G5 — click vs drag. A press that releases within ~4px and ~250ms (isTapGesture) is a click: a click on a free note node selects it and surfaces the open link; a click on a pinned node unpins it. A real drag never opens the note.

Platform events used

  • L1 pointer: onPointerDown/Move/Up + automatic pointer capture (G3, G5)
  • L2 wheel + host pan/zoom: VNodeSvg.panZoom:'host' + the surface.transform settle event (G2)
  • L3 hover: onPointerEnter/Leave (G4)
  • L4 patch channel: ctx.patchSvgPositions(...) + line.sourceId/targetId + circle.id (G3)

Known platform limitation

PluginCtx exposes no ctx.openNote(id) and SVG children cannot be a link VNode, so a true single-click programmatic open is not possible. G5 ships as click-vs-drag disambiguation that surfaces the existing wikilink:// open link on a genuine click. The existing wikilink open behaviour still works. Closing this fully would need a new ctx.openNote host hook (intentionally not added here — the platform is done).

Preserved

All G1 features (local graph, color groups, filters, force tuning, tags-as-nodes) and the sidebar backlinks panel are unchanged. Manifest bumped to v0.3.0 with interaction: { pointer, wheel, hover } on the fullscreen view.

Tests / verify

  • New pure-helper tests in src/__tests__/noteserGraphPluginG2G5.test.ts: click-vs-drag threshold, pinned-node fixed-flag in the sim step, hover neighbour set, viewport round-trip.
  • npm run lint clean, npx tsc --noEmit zero errors, npm test -- --ci green (225 suites / 2891 tests).

🤖 Generated with Claude Code

Makes the noteser-graph fullscreen view feel like Obsidian by consuming
the v1.3 interaction surface (L1-L4). All changes are in the plugin;
src/plugins is untouched.

- G2 wheel zoom + drag pan: the svg declares panZoom:'host', so the host
  owns the viewBox (instant, no worker round-trip). The viewport is
  reconstructed from the surface.transform settle event and persisted
  under g2.viewport so it survives reload. The nine manual zoom/pan
  buttons are deleted; a single "Reset view" remains.
- G3 node drag (pin/unpin): onPointerDown pins a node (host auto-captures
  the pointer), onPointerMove drags it to payload.{x,y}; the simulation
  reheats around pinned nodes (simulationStep skips integration for fixed
  nodes) and streams only the moved coordinates via
  ctx.patchSvgPositions each frame, so a 500-node drag holds 60fps with
  no full-tree re-emit. Click a pinned node to unpin, or "Release pinned".
- G4 hover highlight: onPointerEnter/Leave dims non-neighbour circles and
  brightens the hovered node's edges (recolor only, no re-layout).
- G5 click vs drag: a press within ~4px and ~250ms is a click (selects a
  note and surfaces the open link, or unpins a pinned node); a real drag
  never opens. Open still flows through the existing wikilink:// intercept
  (PluginCtx exposes no ctx.openNote, so a true one-click programmatic
  open is not possible without a new platform hook).

All G1 features (local graph, color groups, filters, force tuning,
tags-as-nodes) and the sidebar backlinks panel are preserved.

Manifest bumped to v0.3.0 with interaction:{pointer,wheel,hover} on the
fullscreen view. New pure helpers (isTapGesture, simulationStep,
hoverNeighbours, viewportFromTransform) are unit-tested in
noteserGraphPluginG2G5.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thetechjon thetechjon merged commit 0204716 into dev Jun 10, 2026
3 checks passed
@thetechjon thetechjon deleted the feat/graph-interactive-g2-g5 branch June 10, 2026 17:17
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