# Gesture System v2 Plan

## Recommendation

Do a hard cutoff.

Do not spend time on adapters, dual dispatch, legacy callbacks, or persisted settings compatibility. v2 should replace the current gesture surface with one coherent shared-input runtime and delete the old stack in the same migration.

No deprecation window. No compatibility bridge. One breaking migration.

This is still not a blind greenfield rewrite. Reuse low-level pieces that are already correct, but stop designing around compatibility.

---

## Cutoff Decision

### Keep

- `core/input-store.ts` as the global pointer/source truth
- `core/interaction-store.ts` as the source of input modes
- `core/selection-path-store.ts` for lasso / rect-select state
- `@use-gesture/react` for drag / pinch / wheel recognition
- local gesture scopes where pointer capture or special ownership is required

### Replace

- `core/gesture-rules.ts`
- `core/gesture-rule-store.ts`
- `core/settings-types.ts`
- `core/settings-store.ts`
- `core/action-registry.ts`
- `core/action-executor.ts`
- `hooks/useCanvasSettings.ts`
- `components/SettingsPanel.tsx`
- gesture logic embedded in `Viewport.tsx`, `Node.tsx`, and `useNodeDrag.ts`

### Delete At Cutoff

- `gestureRules` prop
- per-event compatibility callbacks such as `onNodeClick`, `onNodeDoubleClick`, `onBackgroundClick`, etc.
- `CanvasEventType`
- presets and event-to-action settings persistence
- any legacy adapter or bridge layer

---

## Goals

1. Replace the current split gesture logic with one shared-input runtime.
2. Fix lifecycle bugs around `pointercancel`, hidden tabs, stale stylus state, and orphaned timers.
3. Move from event-name mapping to input-event mapping with context layering and phase-aware actions.
4. Keep local scopes only where they are structurally necessary.
5. End with less code, fewer parallel abstractions, and one public gesture API.

## Non-Goals

- Maintaining old props, old settings, or old storage formats
- Preserving old click / double-click semantics exactly
- Rebuilding the settings UI in the same phase as the runtime cutoff
- Unifying resize, minimap, edge creation, and split into the central runtime on day one

---

## Current Problems

| Area | Problem |
|------|---------|
| `Viewport.tsx` | Pan, zoom, inertia, long-press, threshold logic, and gesture ownership are all inline |
| `Node.tsx` | Rendering is mixed with click counting, touch long-press timers, and context menu handling |
| `useNodeDrag.ts` | Drag mechanics are coupled to gesture policy and right-drag suppression |
| `gesture-rules` + `settings-types` + `action-registry` | The system is split between gesture matching, event mappings, and action execution with overlapping concerns |
| settings UI | The current UI is built around discrete event names, not gesture streams |

The main mistake to avoid is preserving this split under a new name.

---

## Architecture

v2 should have a single shared-input pipeline with four layers.

### Layer 1: Normalize

Normalize raw DOM input into pointer events plus shared keyboard state:

- source
- button
- modifiers
- screen position
- pressure
- timestamp
- pointer lifecycle including `cancel`
- coalesced points when available

### Layer 2: Recognize

Turn normalized pointers into pointer gestures and model keyboard as a parallel input event stream:

- drag / pinch / wheel via `@use-gesture/react`
- tap / double-tap / triple-tap / long-press via a small timed state machine
- explicit gesture ownership rules
- source-aware thresholds in one shared module

### Layer 3: Resolve

Resolve recognized pointer gestures and keyboard events against one context stack:

- default bindings
- palm rejection context
- input-mode context
- local blocking contexts
- consumer-pushed contexts

### Layer 4: Dispatch

Dispatch all input events through one action model:

- discrete gestures use `phase: 'instant'`
- continuous gestures use `start` / `move` / `end` / `cancel`
- no separate `CanvasEventType` bridge

---

## Core Runtime Types

```ts
type GestureSubject =
  | { kind: 'background' }
  | { kind: 'node'; nodeId: string }
  | { kind: 'edge'; edgeId: string }
  | { kind: 'port'; nodeId: string; portId: string };

type GestureType =
  | 'tap'
  | 'double-tap'
  | 'triple-tap'
  | 'long-press'
  | 'drag'
  | 'pinch'
  | 'scroll';

type GesturePhase = 'start' | 'move' | 'end' | 'instant' | 'cancel';

interface GestureEvent {
  type: GestureType;
  phase: GesturePhase;
  subject: GestureSubject;
  source: 'mouse' | 'finger' | 'pencil';
  button: 0 | 1 | 2;
  modifiers: { shift: boolean; ctrl: boolean; alt: boolean; meta: boolean };
  pointerId?: number;
  screenPosition: { x: number; y: number };
  worldPosition: { x: number; y: number };
  delta?: { x: number; y: number };
  velocity?: { x: number; y: number };
  scale?: number;
  originalEvent?: Event;
}

interface Binding {
  id: string;
  pattern: Partial<Pick<GestureEvent, 'type' | 'source' | 'button' | 'phase'>> & {
    subjectKind?: GestureSubject['kind'];
    modifiers?: Partial<GestureEvent['modifiers']>;
  };
  actionId: string;
  consumeInput?: boolean;
  when?: (ctx: GuardContext) => boolean;
}

interface MappingContext {
  id: string;
  priority: number;
  enabled: boolean;
  bindings: Binding[];
}
```

This replaces the old `CanvasEventType -> actionId` model completely.

---

## Design Decisions

### 1. Replace Event-Mapping With Gesture-Mapping

The current settings/event system is built around labels like `node:click` and `background:long-press`.

That model is too narrow for:

- continuous phases
- pointer ownership
- source-aware gesture behavior
- context stacking

v2 should map actual gesture patterns to actions, not synthetic event names.

### 2. Own Click And Tap Recognition In One Place

Do not keep browser `click`, `dblclick`, and `contextmenu` as a separate path.

v2 should own:

- tap
- double-tap / double-click
- triple-tap
- long-press
- right-click

That gives one recognition model across mouse, touch, and pencil.

### 3. Keep Local Scopes Where The Physics Differ

These stay local in v2:

| Interaction | Why |
|-------------|-----|
| Resize handles | direct geometry updates + pointer capture |
| Minimap drag | separate coordinate system |
| Edge creation drag | direct hit testing + pointer capture |
| Split gesture | specialized two-finger ownership |

These local scopes still interact with the runtime by:

- setting guard flags
- pushing temporary blocking contexts
- dispatching high-level actions when needed

### 4. Input Mode Must Use The Real Store Shape

`interaction-store.ts` already models input mode as a discriminated union. Keep that.

Guard context should carry:

- the full input mode object, or
- a derived `inputMode.type`

Do not flatten it into an invented string union.

### 5. Context Sync Belongs In Hooks

Input mode and local blocking contexts should be pushed from explicit hooks.

Do not hide that work in effectful derived atoms.

### 6. No Legacy Surface At The End

When v2 lands:

- the old public props are removed
- the old settings UI is removed or disabled
- the old stores and registry files are deleted

No compatibility release.

---

## New File Plan

Create a dedicated `packages/canvas/src/gestures/` module:

- `types.ts`
- `normalize.ts`
- `timed-state.ts`
- `specificity.ts`
- `mapper.ts`
- `contexts.ts`
- `dispatcher.ts`
- `inertia.ts`
- `useCanvasGestures.ts`
- `useNodeGestures.ts`
- `useGestureSystem.ts`
- `useInputModeGestureContext.ts`

Extend or keep:

- `core/input-store.ts`
- `core/interaction-store.ts`
- `core/selection-path-store.ts`
- `hooks/useNodeResize.ts`
- `hooks/useSplitGesture.ts`

Delete after cutoff:

- `core/gesture-rules.ts`
- `core/gesture-rule-store.ts`
- `core/settings-types.ts`
- `core/settings-store.ts`
- `core/action-registry.ts`
- `core/action-executor.ts`
- `hooks/useCanvasSettings.ts`
- `components/SettingsPanel.tsx`

---

## Public API Direction

Replace the old callback-heavy surface with a gesture-native API.

### Remove

- `gestureRules`
- `onGestureAction`
- `onNodeClick`
- `onNodeDoubleClick`
- `onNodeTripleClick`
- `onNodeRightClick`
- `onNodeLongPress`
- `onBackgroundClick`
- `onBackgroundDoubleClick`
- `onBackgroundRightClick`
- `onBackgroundLongPress`

### Add

```ts
interface CanvasProps {
  gestureConfig?: {
    contexts?: MappingContext[];
    debug?: boolean;
    palmRejection?: boolean;
  };
  onAction?: (event: GestureEvent, resolution: ResolvedAction) => void;
}
```

If consumers need click-specific behavior, they express it as bindings and action handlers, not bespoke props.

---

## Phased Implementation Plan

## Phase 0: Capture Expected Behavior

Before rewriting, document the behavior we still care about.

Deliverables:

- tests for `Viewport` pan / pinch / wheel / background gesture behavior
- tests for `Node` tap, double-tap, triple-tap, right-click, and long-press behavior
- tests for `useNodeDrag` modifier and button behavior
- tests for stylus palm rejection
- tests for split-vs-pinch ownership on nodes

Success criteria:

- we have a concrete acceptance matrix
- we know what to preserve functionally and what can change

## Phase 1: Lifecycle Hardening

Fix the bugs that should survive the rewrite.

Deliverables:

- handle `pointercancel` in all pointer-tracking code
- clear active pointers on `visibilitychange`
- cancel timers and inertia on hidden tab, cancel, and unmount
- extract pure source-threshold and inertia helpers
- add cancellation handling to `useSplitGesture.ts`

Success criteria:

- no stale pointer state
- no orphaned timers
- no runaway inertia

## Phase 2: Build The New Pure Runtime

Implement the new system in `gestures/` without touching public components yet.

Deliverables:

- normalization layer
- timed state machine
- specificity scoring
- context stack resolver
- phase-aware dispatcher
- inertia engine

Success criteria:

- all pure pieces are independently testable
- the runtime has no dependency on old event-mapping types

## Phase 3: Add React Integration

Wire the runtime into hooks and stores.

Deliverables:

- `useCanvasGestures.ts`
- `useNodeGestures.ts`
- `useGestureSystem.ts`
- `useInputModeGestureContext.ts`
- hook-driven context sync for input modes and local blockers

Success criteria:

- hooks can drive gestures end-to-end without using the old rule or settings stack

## Phase 4: Cut Off Components

Replace the old component gesture paths directly.

Deliverables:

- `Viewport.tsx` uses `useCanvasGestures.ts`
- `Node.tsx` uses `useNodeGestures.ts`
- `useNodeDrag.ts` is reduced to drag mechanics
- DOM click / dblclick / contextmenu paths are removed
- old callback props are removed from `Canvas.tsx`

Success criteria:

- one runtime owns gesture interpretation
- components mostly render and forward gesture props

## Phase 5: Delete The Old Stack

Remove everything that only existed for the old model.

Deliverables:

- delete old rules and settings files
- delete the settings panel
- delete the old action registry / executor
- delete unused exports
- update docs and examples to the new API

Success criteria:

- no dead compatibility code remains
- there is one obvious path for gesture behavior

---

## Cutover Risks

### 1. Desktop Semantics Will Change

Owning double-click and context menu behavior ourselves means web behavior may differ from native browser expectations.

That is acceptable, but it must be deliberate.

### 2. Settings UI Disappears Until Rebuilt

If the current settings panel is still valuable, it should be rebuilt later against gesture bindings and contexts. It should not block the runtime rewrite.

### 3. `@use-gesture` Ownership Must Be Verified

Do not assume pinch cancellation and drag ownership work exactly as the new runtime wants.

Test:

- pinch over background
- split over node
- drag interrupted by second pointer
- cancel during active gesture

### 4. Local Scopes Can Drift

Resize, minimap, edge creation, and split are intentionally local at first. If they do not publish guard state consistently, gesture conflicts will remain.

---

## Verification

Automated:

1. `pnpm --filter @blinksgg/canvas vitest run`
2. `pnpm --filter @blinksgg/canvas check-types`
3. `pnpm --filter @blinksgg/canvas build`

Manual:

- mouse: tap, double-tap, right-click, drag, wheel zoom
- touch: pan, pinch zoom, long-press
- pencil: tap, drag, lasso
- stylus + palm rejection
- node drag with modifiers and alternate buttons
- split gesture on node vs pinch on background
- resize handles
- minimap drag
- edge creation
- input modes overriding normal behavior
- visibility change during active gesture

Exit criteria:

- all supported gestures route through the new runtime
- old settings and callback code is deleted
- no stale pointer or timer bugs remain
- `Viewport.tsx`, `Node.tsx`, and `useNodeDrag.ts` are materially smaller

---

## Summary

The right v2 is a hard cutoff, not a compatibility bridge.

Build one gesture-native runtime, cut the old event-mapping stack, keep only the low-level stores that still make sense, and delete the rest in the same migration.
