OrientBottom.js 5.33 KB
import {
    EventDispatcher,
    Raycaster,
    Vector3,
    Quaternion,
    Mesh,
    Vector2,
    CircleGeometry,
    MeshStandardMaterial,
    ConeGeometry,
    CylinderGeometry,
    Object3D,
    Matrix4
} from 'three';
import { acceleratedRaycast } from 'three-mesh-bvh';

Mesh.prototype.raycast = acceleratedRaycast;

const getMousePosition = (event, container) => {
    const rect = container.getBoundingClientRect();
    const mouse = { x: 0, y: 0 };
    mouse.x = ((event.clientX - rect.left) / (rect.right - rect.left)) * 2 - 1;
    mouse.y = -((event.clientY - rect.top) / (rect.bottom - rect.top)) * 2 + 1;
    return mouse;
};

const zAxis = new Vector3(0, 0, 1);
const position = new Vector3();
const scale = new Vector3(1, 1, 1);

class OrientBottom extends EventDispatcher {
    constructor(editor) {
        super();
        const { camera, scene, domElement, control, rootObject = [] } = editor;

        this._camera = camera;
        this._scene = scene;
        this._control = control;
        this._domElement = domElement;
        this.rootObjects = rootObject;

        this.raycaster = new Raycaster();
        this.raycaster.firstHitOnly = true;

        this.enabled = true;

        this.cursorMesh = null;

        this.orientToBottomMatrix = new Matrix4();
        this.doOrient = false;

        this.init();
        this.addEvents();
    }

    init = () => {
        const circleGeo = new CircleGeometry(0.8, 32);
        const material = new MeshStandardMaterial({ color: 0xec5504 });
        const circle = new Mesh(circleGeo, material);
        circle.renderOrder = 3;

        const length = 2;
        const coneGeo = new ConeGeometry(0.35, 1, 32);
        coneGeo.applyMatrix4(new Matrix4().makeRotationX(Math.PI * 0.5));
        coneGeo.applyMatrix4(new Matrix4().makeTranslation(0, 0, length));
        const cone = new Mesh(coneGeo, material);
        cone.renderOrder = 3;

        const cylinderGeo = new CylinderGeometry(0.12, 0.12, length, 32);
        cylinderGeo.applyMatrix4(new Matrix4().makeRotationX(Math.PI * 0.5));
        cylinderGeo.applyMatrix4(new Matrix4().makeTranslation(0, 0, length * 0.5));
        const cylinder = new Mesh(cylinderGeo, material);
        cylinder.renderOrder = 3;

        const mesh = new Object3D();
        mesh.add(circle).add(cone).add(cylinder);
        mesh.visible = false;
        this.cursorMesh = mesh;
        this._scene.add(this.cursorMesh);
    };

    addEvents = () => {
        this._domElement.addEventListener('pointerdown', this.onPointerdown);
        // this._domElement.addEventListener('pointerup', this.onPointerup);
        this._domElement.addEventListener('pointermove', this.onPointermove);
    };

    removeEvents = () => {
        this._domElement.removeEventListener('pointerdown', this.onPointerdown);
        // this._domElement.removeEventListener('pointerup', this.onPointerup);
        this._domElement.removeEventListener('pointermove', this.onPointermove);
    };

    dispose = () => {
        this.removeEvents();
        this._scene.remove(this.cursorMesh);
        this.cursorMesh.children.forEach((el) => {
            el.geometry.dispose();
            el.material.dispose();
        });
        this.cursorMesh = null;
    };

    setRootObjects = (objects) => {
        this.rootObjects = objects;
    };

    onPointerdown = (e) => {
        if (e.button !== 0) return;
        if (!this.doOrient) return;
        this.dispatchEvent({
            type: 'start',
            message: {
                orientToBottomMatrix: this.orientToBottomMatrix
            }
        });
    };

    onPointermove = (e) => {
        if (!this.enabled) return;
        if (!this._camera) return;
        if (!this._domElement) return;
        if (this.rootObjects.length === 0) return;

        this.doOrient = false;
        this.cursorMesh.visible = false;

        const vec2d = getMousePosition(e, this._domElement);
        const mouse = new Vector2(vec2d.x, vec2d.y);

        this._camera.near = 1;
        this.raycaster.setFromCamera(mouse, this._camera);

        const detects = this.rootObjects.filter((el) => el.visible);
        const res = this.raycaster.intersectObjects(detects);

        const normalcopy = new Vector3();

        if (res.length > 0) {
            const obejct = res[0].object;
            // update normal after change mesh matrix
            const normal = res[0].object.name.includes('mirror')
                ? res[0].face.normal.clone().multiplyScalar(-1)
                : res[0].face.normal;
            normalcopy.copy(normal);
            normalcopy.transformDirection(obejct.matrixWorld);

            this.cursorMesh.position.copy(res[0].point.clone().add(normalcopy.clone().normalize().multiplyScalar(0.2)));

            // orient cursor
            this.orientMesh(normalcopy);

            // get the orient bottom matrix
            const q = new Quaternion().setFromUnitVectors(normalcopy, new Vector3(0, 0, -1));
            this.orientToBottomMatrix = new Matrix4().compose(position, q, scale);

            this.doOrient = true;
            this.cursorMesh.visible = true;
        }

        this._camera.near = -10000;
    };

    orientMesh = (surfaceNormal) => {
        const rotationQuaternion = new Quaternion().setFromUnitVectors(zAxis, surfaceNormal);
        if (this.cursorMesh) this.cursorMesh.quaternion.copy(rotationQuaternion);
    };
}

export default OrientBottom;