Files
Genarrative/src/services/input-devices/runtimeDragInputController.ts
T
2026-05-10 17:50:00 +08:00

169 lines
4.1 KiB
TypeScript

export type RuntimeInputDeviceKind = 'pointer' | 'mocap' | 'keyboard' | 'unknown';
export type RuntimeInputPoint = {
clientX: number;
clientY: number;
normalizedX?: number;
normalizedY?: number;
};
export type RuntimeDragInputSession<TTargetId extends string = string> = {
targetId: TTargetId;
inputId: string;
deviceKind: RuntimeInputDeviceKind;
startPoint: RuntimeInputPoint;
currentPoint: RuntimeInputPoint;
dragging: boolean;
forceDrop: boolean;
};
export type RuntimeDragInputPress<TTargetId extends string = string> = {
targetId: TTargetId;
inputId: string;
deviceKind: RuntimeInputDeviceKind;
point: RuntimeInputPoint;
};
export type RuntimeDragInputMove = {
inputId: string;
point: RuntimeInputPoint;
forceDragging?: boolean;
};
export type RuntimeDragInputRelease = {
inputId: string;
point: RuntimeInputPoint;
forceDrop?: boolean;
};
export type RuntimeDragInputControllerOptions<
TTargetId extends string = string,
> = {
dragThresholdPx?: number;
onPress?: (session: RuntimeDragInputSession<TTargetId>) => void;
onDragStart?: (session: RuntimeDragInputSession<TTargetId>) => void;
onDragMove?: (session: RuntimeDragInputSession<TTargetId>) => void;
onDrop?: (session: RuntimeDragInputSession<TTargetId>) => void;
onTap?: (session: RuntimeDragInputSession<TTargetId>) => void;
onCancel?: (session: RuntimeDragInputSession<TTargetId>) => void;
};
const DEFAULT_DRAG_THRESHOLD_PX = 8;
function clonePoint(point: RuntimeInputPoint): RuntimeInputPoint {
return { ...point };
}
function shouldStartDragging(
session: RuntimeDragInputSession,
point: RuntimeInputPoint,
thresholdPx: number,
forceDragging = false,
) {
if (session.dragging || forceDragging) {
return true;
}
return (
Math.hypot(
point.clientX - session.startPoint.clientX,
point.clientY - session.startPoint.clientY,
) >= thresholdPx
);
}
export function createRuntimeDragInputController<
TTargetId extends string = string,
>(initialOptions: RuntimeDragInputControllerOptions<TTargetId> = {}) {
let options = initialOptions;
let session: RuntimeDragInputSession<TTargetId> | null = null;
const setOptions = (
nextOptions: RuntimeDragInputControllerOptions<TTargetId>,
) => {
options = nextOptions;
};
const cancel = (inputId?: string) => {
if (inputId && session?.inputId !== inputId) {
return;
}
const activeSession = session;
session = null;
if (activeSession) {
options.onCancel?.(activeSession);
}
};
const press = (input: RuntimeDragInputPress<TTargetId>) => {
cancel();
session = {
targetId: input.targetId,
inputId: input.inputId,
deviceKind: input.deviceKind,
startPoint: clonePoint(input.point),
currentPoint: clonePoint(input.point),
dragging: false,
forceDrop: false,
};
options.onPress?.(session);
return session;
};
const move = (input: RuntimeDragInputMove) => {
if (!session || session.inputId !== input.inputId) {
return null;
}
const wasDragging = session.dragging;
session = {
...session,
currentPoint: clonePoint(input.point),
dragging: shouldStartDragging(
session,
input.point,
options.dragThresholdPx ?? DEFAULT_DRAG_THRESHOLD_PX,
input.forceDragging,
),
};
if (!wasDragging && session.dragging) {
options.onDragStart?.(session);
}
if (session.dragging) {
options.onDragMove?.(session);
}
return session;
};
const release = (input: RuntimeDragInputRelease) => {
if (!session || session.inputId !== input.inputId) {
return null;
}
const completedSession = {
...session,
currentPoint: clonePoint(input.point),
forceDrop: input.forceDrop === true,
};
session = null;
if (completedSession.dragging || completedSession.forceDrop) {
options.onDrop?.(completedSession);
} else {
options.onTap?.(completedSession);
}
return completedSession;
};
return {
cancel,
getSession: () => session,
move,
press,
release,
setOptions,
};
}