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.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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
|
||||
* 60–120×/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;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user