Files
tools/vue/primitives/src/canvas/flow/composables/useConnection.ts
T
robonen eefd7abf83 feat(primitives): media-editor components, category reorg, perf + type cleanup
Reorganize components into category folders (forms/canvas/overlays/etc.); add the
media-editor headless family (timeline, curve-editor, waveform, crop, color
picker, etc.); apply perf fixes (O(1) collection lookups, plain-object drag
state, gesture-leak teardown, shallowRef color state, rect caching) and replace
source `any` with proper types.
2026-06-15 16:54:29 +07:00

64 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { onScopeDispose } from 'vue';
import { useEventListener } from '@robonen/vue';
import type { FlowContext } from '../context';
import type { XYPosition } from '../types';
/**
* Drives an in-progress connection: while `connection.inProgress`, follows the
* pointer (converting to flow space and hit-testing candidate handles via
* `ctx.updateConnection`) and finalises on pointerup (`ctx.endConnection`).
*
* Listeners are bound to the window ONCE on mount (client only) and stay bound
* for the component's life — the handlers no-op unless a connection is active.
* This avoids the race of attaching them reactively when a drag starts.
*
* `updateConnection` is RAF-batched: a pointermove only stashes the latest flow
* point, and at most one hit-test (`findClosestHandle` scans every handle) +
* state write runs per frame — matching usePanZoom / useNodeDrag, instead of
* 60120×/sec. pointerup flushes the pending point synchronously first so the
* committed connection reflects the final cursor position.
*/
export function useConnection(ctx: FlowContext): void {
let rafId: number | null = null;
let pending: XYPosition | null = null;
function flush(): void {
rafId = null;
if (pending) {
ctx.updateConnection(pending);
pending = null;
}
}
function onMove(event: PointerEvent): void {
if (!ctx.connection.value.inProgress) return;
pending = ctx.screenToFlow({ x: event.clientX, y: event.clientY });
if (rafId === null) rafId = requestAnimationFrame(flush);
}
function onUp(): void {
if (!ctx.connection.value.inProgress) return;
// Apply the last pointer position before committing.
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
flush();
ctx.endConnection();
}
// Listeners stay bound to the window for the component's life and are
// auto-removed on scope dispose. SSR-safe: the window default no-ops without a
// `window`. The handlers no-op unless a connection is active.
useEventListener('pointermove', onMove);
useEventListener('pointerup', onUp);
// `useEventListener` owns the listeners; we still cancel any pending RAF here.
onScopeDispose(() => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
});
}