@turbox3d/design-engine
Version:
Large-scale design application engine library
312 lines (290 loc) • 11 kB
text/typescript
import { Extra, GesturesExtra, InteractiveEvent } from '@turbox3d/event-manager';
import { Vec2 } from '@turbox3d/shared';
import { Vector2, MathUtils } from '@turbox3d/math';
export enum DragStatus {
Ready = 'ready',
Dragging = 'dragging',
End = 'end',
}
/**
* 当次鼠标事件(down -> move -> up)的处理类型
*/
export enum MouseDealType {
/**
* 作为一次 Drag 事件处理
*/
Drag = 'drag',
/**
* 作为一次 Click 事件处理
*/
Click = 'click',
/** 多点触控事件 */
MultiTouch = 'multi-touch',
/** 按压 */
Press = 'press',
}
export default class MaterialDragSystem {
/** 入参 */
args: any[];
/** 事件回调处理 */
handler: (e: InteractiveEvent, event: PointerEvent | WheelEvent | React.PointerEvent | Touch, extra?: GesturesExtra | Extra) => void;
maxFPS = 60;
static touchableDevice = 'ontouchstart' in window;
/** 渲染器的 canvas 元素 */
private canvas: HTMLCanvasElement;
/** 当前鼠标是否处于按下状态 */
private isMouseDown = false;
private mouseDownInfo?: Vec2;
/** 当前的拖拽状态 */
private dragStatus?: DragStatus;
/** 当次鼠标事件处理类型 */
private mouseDealType?: MouseDealType;
/** 坐标轴类型 */
private coordinateType?: string;
/** 缓存事件池,用于多点触控 */
private eventCache: PointerEvent[] = [];
/** 双指间距 */
private prevDiff = new Vector2(-1, -1);
/** 初始的间距 */
private startDiff = new Vector2(-1, -1);
/** 双指方向 */
private prevVector = new Vector2();
/** 初始的方向 */
private startVector = new Vector2();
private pressTimer: number;
/** 使用 x 还是 y 轴间距作为缩放系数计算因子 */
private useX?: boolean;
constructor(
handler: (e: InteractiveEvent, event: PointerEvent | WheelEvent | React.PointerEvent | Touch, extra?: GesturesExtra | Extra) => void,
maxFPS = 60
) {
this.handler = handler;
this.maxFPS = maxFPS;
this.registerListener();
}
private isMouseMoved(mouseDownInfo: Vec2, moveEvent: PointerEvent | Touch, tolerance: number) {
const dx = mouseDownInfo.x - moveEvent.clientX;
const dy = mouseDownInfo.y - moveEvent.clientY;
return dx * dx + dy * dy > tolerance * tolerance;
}
dispose() {
if (MaterialDragSystem.touchableDevice) {
document.removeEventListener('touchmove', this.onTouchMove);
document.removeEventListener('touchend', this.onTouchUp);
document.removeEventListener('touchcancel', this.onTouchUp);
} else {
document.removeEventListener('pointermove', this.onMouseMove);
document.removeEventListener('pointerup', this.onMouseUp);
document.removeEventListener('pointercancel', this.onMouseUp);
document.removeEventListener('pointerleave', this.onMouseUp);
}
}
registerListener() {
if (MaterialDragSystem.touchableDevice) {
document.addEventListener('touchmove', this.onTouchMove);
document.addEventListener('touchend', this.onTouchUp);
document.addEventListener('touchcancel', this.onTouchUp);
} else {
document.addEventListener('pointermove', this.onMouseMove);
document.addEventListener('pointerup', this.onMouseUp);
document.addEventListener('pointercancel', this.onMouseUp);
document.addEventListener('pointerleave', this.onMouseUp);
}
}
private triggerEvent(e: InteractiveEvent, event: PointerEvent | WheelEvent | Touch, extra?: GesturesExtra | Extra) {
this.handler(e, event, extra);
}
private addEventCache(event: PointerEvent) {
this.eventCache.push(event);
}
private removeEventCache(event: PointerEvent) {
for (let i = 0; i < this.eventCache.length; i++) {
if (this.eventCache[i].pointerId === event.pointerId) {
this.eventCache.splice(i, 1);
break;
}
}
}
private updateEventCache(event: PointerEvent) {
for (let i = 0; i < this.eventCache.length; i++) {
if (event.pointerId === this.eventCache[i].pointerId) {
this.eventCache[i] = event;
break;
}
}
}
private downHandler = <E extends PointerEvent | Touch>(eventList: E[]) => {
this.isMouseDown = true;
if (eventList.length === 1) {
this.mouseDealType = MouseDealType.Click;
this.dragStatus = DragStatus.Ready;
this.mouseDownInfo = { x: eventList[0].clientX, y: eventList[0].clientY };
this.pressTimer = window.setTimeout(() => {
this.mouseDealType = MouseDealType.Press;
this.triggerEvent(InteractiveEvent.Press, eventList[0]);
}, 250);
} else if (eventList.length === 2) {
// 双指操作,rotate or pinch
window.clearTimeout(this.pressTimer);
this.startDiff = new Vector2(
Math.abs(eventList[0].clientX - eventList[1].clientX),
Math.abs(eventList[0].clientY - eventList[1].clientY)
);
this.startVector = new Vector2(
eventList[1].clientX - eventList[0].clientX,
eventList[1].clientY - eventList[0].clientY
);
this.mouseDealType = MouseDealType.MultiTouch;
}
};
onMouseDown = (...args: any[]) => (event: PointerEvent) => {
event.preventDefault();
this.args = args;
this.addEventCache(event);
this.downHandler(this.eventCache);
};
onTouchDown = (...args: any[]) => (event: TouchEvent) => {
event.preventDefault();
this.args = args;
this.isMouseDown = true;
this.downHandler(Array.from(event.targetTouches || event.touches));
};
private moveHandler = <E extends PointerEvent | Touch>(event: E, eventList: E[]) => {
if (this.mouseDealType === MouseDealType.MultiTouch) {
if (eventList.length === 2) {
const curDiff = new Vector2(
Math.abs(eventList[0].clientX - eventList[1].clientX),
Math.abs(eventList[0].clientY - eventList[1].clientY)
);
this.useX === undefined && (this.useX = curDiff.x > curDiff.y);
const curVector = new Vector2(
eventList[1].clientX - eventList[0].clientX,
eventList[1].clientY - eventList[0].clientY
);
if (this.prevDiff.x > 0 && this.prevDiff.y > 0) {
this.triggerEvent(InteractiveEvent.Pinch, event, {
scale: this.useX ? curDiff.x / this.startDiff.x : curDiff.y / this.startDiff.y,
deltaScale: this.useX ? curDiff.x / this.prevDiff.x : curDiff.y / this.prevDiff.y,
eventCache: eventList,
});
} else {
this.triggerEvent(InteractiveEvent.PinchStart, event, {
eventCache: eventList,
});
}
if (this.prevVector.x === 0 && this.prevVector.y === 0) {
this.triggerEvent(InteractiveEvent.RotateStart, event, {
eventCache: eventList,
});
} else {
const degree = curVector.angleTo(this.startVector) * MathUtils.RAD2DEG;
const deltaDegree = curVector.angleTo(this.prevVector) * MathUtils.RAD2DEG;
this.triggerEvent(InteractiveEvent.Rotate, event, {
rotate: degree,
deltaRotate: deltaDegree,
eventCache: eventList,
});
}
this.prevDiff = curDiff;
this.prevVector = curVector;
}
} else if (
this.mouseDealType === MouseDealType.Drag ||
(this.mouseDownInfo && this.isMouseMoved(this.mouseDownInfo, event, 4))
) {
window.clearTimeout(this.pressTimer);
this.mouseDealType = MouseDealType.Drag;
if (this.dragStatus === DragStatus.Ready) {
this.dragStatus = DragStatus.Dragging;
this.triggerEvent(InteractiveEvent.DragStart, event, {
mouseDownInfo: {
x: this.mouseDownInfo?.x || 0,
y: this.mouseDownInfo?.y || 0,
},
});
} else if (this.dragStatus === DragStatus.Dragging) {
this.triggerEvent(InteractiveEvent.DragMove, event);
}
}
};
private onMouseMove = (event: PointerEvent) => {
event.preventDefault();
if (this.isMouseDown) {
if (this.mouseDealType === MouseDealType.MultiTouch) {
this.updateEventCache(event);
}
this.moveHandler(event, this.eventCache);
} else {
this.triggerEvent(InteractiveEvent.Hover, event);
this.triggerEvent(InteractiveEvent.CarriageMove, event);
}
};
private onTouchMove = (event: TouchEvent) => {
const eventArr = Array.from(event.targetTouches || event.touches);
if (!eventArr.length) {
return;
}
if (this.isMouseDown) {
// 多点触控下,当前用于识别的事件先默认取第一个
if (eventArr[0].target === this.canvas) {
event.preventDefault();
}
this.moveHandler(eventArr[0], eventArr);
}
};
private upHandler = <E extends PointerEvent | Touch>(event: E, eventList: E[]) => {
this.useX = undefined;
if (eventList.length === 0) {
this.isMouseDown = false;
window.clearTimeout(this.pressTimer);
if (this.mouseDealType === MouseDealType.Drag) {
this.triggerEvent(InteractiveEvent.DragEnd, event);
} else if (this.mouseDealType === MouseDealType.Click) {
if (MaterialDragSystem.touchableDevice) {
this.triggerEvent(InteractiveEvent.Click, event);
} else {
const tempEvent = event as PointerEvent;
if (tempEvent.button === 0) {
this.triggerEvent(InteractiveEvent.Click, event);
} else if (tempEvent.button === 2) {
this.triggerEvent(InteractiveEvent.RightClick, event);
}
}
} else if (this.mouseDealType === MouseDealType.MultiTouch) {
this.triggerEvent(InteractiveEvent.PinchEnd, event, {
eventCache: eventList,
});
this.triggerEvent(InteractiveEvent.RotateEnd, event, {
eventCache: eventList,
});
} else if (this.mouseDealType === MouseDealType.Press) {
this.triggerEvent(InteractiveEvent.PressUp, event);
}
this.triggerEvent(InteractiveEvent.CarriageEnd, event);
this.mouseDealType = undefined;
this.dragStatus = undefined;
this.mouseDownInfo = undefined;
this.args = [];
} else if (this.mouseDealType === MouseDealType.MultiTouch) {
if (eventList.length < 2) {
this.prevDiff = new Vector2(-1, -1);
this.startDiff = new Vector2(-1, -1);
this.prevVector = new Vector2();
this.startVector = new Vector2();
}
}
};
private onMouseUp = (event: PointerEvent) => {
event.preventDefault();
this.removeEventCache(event);
this.upHandler(event, this.eventCache);
};
private onTouchUp = (event: TouchEvent) => {
const eventArr = Array.from(event.targetTouches || event.touches);
// 多点触控下,当前用于识别的事件先默认取第一个
if (event.changedTouches[0].target === this.canvas) {
event.preventDefault();
}
this.upHandler(event.changedTouches[0], eventArr);
};
}