TrackballRotateControls.js 5.7 KB
import { MOUSE , Plane, Vector3, Vector2, Raycaster, Quaternion } from 'three';

const clamp = (value, min, max) => {
    return Math.min(Math.max(value, min), max);
}

class TrackballRotate {
    constructor(camera, doElement, rootObject) {

        this.STATE = {
            'NONE': - 1,
            'ROTATE': MOUSE.RIGHT,
            'PAN': MOUSE.MIDDLE
        };

        this.camera = camera;
        this.doElement = doElement;
        this.rootObject = rootObject;

        this.enabled = true;
        this.isPointerdown = false;

        this.curState = null;
        this.startV2 = { x: 0, y: 0 };

        this.insectPlane = new Plane();
        this.insectPoint = new Vector3();
        this.raycaster = new Raycaster();

        this.preV3 = new Vector3();
        this.curV3 = new Vector3();

        this.deltaV2 = { x: 0, y: 0 };

        this.rotateSpeed = 12

        this.doElement.addEventListener('mousedown', this.onPointerdown, false);
    }

    setObjects = (object) => {
        this.rootObject = object;
    }

    dispose = () => {
        this.doElement.removeEventListener('mousedown', this.onPointerdown);
        this.doElement.removeEventListener('mousemove', this.onPointermove);
        document.removeEventListener('pointerup', this.onPointerup);
    }

    onPointerdown = (e) => {
        if (!this.enabled) return;
        e.preventDefault();

        this.isPointerdown = true;

        switch (e.button) {
            case this.STATE.PAN:
                this.curState = this.STATE.PAN;
                this.updateIntersetPlane();
                this.initPan(e);
                break;
            case this.STATE.ROTATE:
                this.curState = this.STATE.ROTATE;
                this.startV2 = {
                    x: e.clientX,
                    y: e.clientY
                };
                this.preV3 = this.curV3 = this.projectOnTrackball(0, 0);
                break;
        }

        this.doElement.addEventListener('mousemove', this.onPointermove);
        document.addEventListener('pointerup', this.onPointerup);
    }

    onPointermove = (e) => {
        if (!this.enabled) return;
        if (!this.isPointerdown) return

        switch (this.curState) {
            case this.STATE.PAN:

                this.doPan(e);
                this.preV3 = this.curV3.clone();

                break;
            case this.STATE.ROTATE:

                this.deltaV2 = { x: e.clientX - this.startV2.x, y: e.clientY - this.startV2.y }
                this.doRotate();
                this.startV2 = { x: e.clientX, y: e.clientY };

                break;
        }
    }

    onPointerup = () => {
        this.isPointerdown = false;
        this.curState= null;
        this.doElement.removeEventListener('mousemove', this.onPointermove);
        document.removeEventListener('pointerup', this.onPointerup);
    }

    initPan = (e) => {
        if (!this.camera) return;

        let screen = this.getScreenPosition(e);
        let mouse = new Vector2(screen.x, screen.y);

        this.raycaster.firstHitOnly = false;
        this.raycaster.setFromCamera(mouse, this.camera);

        if (this.raycaster.ray.intersectPlane(this.insectPlane, this.insectPoint))
            this.preV3.copy(this.insectPoint)
    }

    doPan = (e) => {
        if (!this.camera) return;

        let screen = this.getScreenPosition(e);
        let mouse = new Vector2(screen.x, screen.y);

        this.raycaster.setFromCamera(mouse, this.camera);
        if (this.raycaster.ray.intersectPlane(this.insectPlane, this.insectPoint)) {

            this.curV3.copy(this.insectPoint);
            let offset = this.curV3.clone().sub(this.preV3);

            if (this.rootObject) this.rootObject.position.add(offset);
        }
    }

    doRotate = () => {
        this.curV3 = this.projectOnTrackball(this.deltaV2.x, this.deltaV2.y);
        const rotateQuaternion = this.getQuaternion(this.preV3, this.curV3);

        const curQuaternion = this.rootObject.quaternion.clone();
        curQuaternion.multiplyQuaternions(rotateQuaternion, curQuaternion);
        curQuaternion.normalize();

        this.rootObject.setRotationFromQuaternion(curQuaternion);

        this.curV3 = this.preV3;
    }

    getQuaternion = (preV3, curV3) => {
        const axis = new Vector3();
        const quaternion = new Quaternion();

        let angle = Math.acos(preV3.dot(curV3) / preV3.length() / curV3.length());
        if (angle) {
            axis.crossVectors(preV3, curV3).normalize();
            angle *= this.rotateSpeed;
            quaternion.setFromAxisAngle(axis, angle);
        }
        return quaternion
    }

    projectOnTrackball = (touchX, touchY) => {
        let mouseOnBall = new Vector3();
        mouseOnBall.set(
            clamp(touchX / this.doElement.clientWidth * 0.5, -1, 1),
            clamp(-touchY / this.doElement.clientHeight * 0.5, -1, 1),
            0.0
        );
        const length = mouseOnBall.length();
        if (length > 1.0)
            mouseOnBall.normalize();
        else
            mouseOnBall.z = Math.sqrt(1.0 - length * length);
        return mouseOnBall;
    }

    getScreenPosition = (e) => {
        if (!this.doElement) return;
        let rect = this.doElement.getBoundingClientRect();
        let mouse = { x: 0, y: 0 };
        mouse.x = ((e.clientX - rect.left) / (this.doElement.clientWidth)) * 2 - 1;
        mouse.y = - ((e.clientY - rect.top) / (this.doElement.clientHeight)) * 2 + 1;
        return mouse;
    }

    updateIntersetPlane = () => {
        if (!this.insectPlane) return;
        if (!this.camera) return;

        this.insectPlane.setFromNormalAndCoplanarPoint(
            this.camera.getWorldDirection(this.insectPlane.normal),
            new Vector3()
        );
    }

}
export default TrackballRotate;