Measures.js 9.27 KB
import {
    Mesh,
    Raycaster,
    Vector2,
    Vector3,
    ConeGeometry,
    MeshStandardMaterial,
    CatmullRomCurve3,
    TubeGeometry,
    MeshBasicMaterial,
    Matrix4,
    Object3D,
    Quaternion,
    SphereGeometry,
    Color
} from 'three';

import { drawMeasuresSprite } from '@/utility/jsm/draw/drawMeasuresSprite';
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 roundDown = (number = 0.0, decimal = 2) => {
    return Math.floor(number * Math.pow(10, decimal)) / Math.pow(10, decimal);
};

class Measures {
    constructor({ camera, scene, control, domElement }) {
        this._camera = camera;
        this._scene = scene;
        this._control = control;
        this._domElement = domElement;
        this.mode = 'distance'; // 'distance','thickness'

        this.raycaster = new Raycaster();

        this.enabled = true;
        this._rootObjects = [];
        //
        this.measuresLinesName = 'measuresLine';
        this.measuresPointsName = 'measuresPoints';
        this.pointMeshFrom = null;
        this.pointMeshTo = null;

        this.thicknessNormal = null;
        this.hoverMesh = null;

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

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

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

    setMeasuresMode = (val) => {
        this.mode = val;
    };

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

    init = () => {
        const pointGeometry = new SphereGeometry(0.2, 32, 16);
        const pointMaterial = new MeshBasicMaterial({ color: 0xfe5405, depthTest: false, transparent: true });
        const pointMesh = new Mesh(pointGeometry, pointMaterial);
        pointMesh.name = this.measuresPointsName;
        pointMesh.visible = false;
        pointMesh.renderOrder = 4;
        this.pointMeshFrom = pointMesh.clone();
        this.pointMeshTo = pointMesh.clone();
        const group = new Object3D();
        group.add(this.pointMeshFrom).add(this.pointMeshTo);
        this._scene.add(group);
    };

    generateMeasureLines = (from = new Vector3(), to = new Vector3(10, 10, 10)) => {
        const direction = to.clone().sub(from).normalize();
        const distance = from.distanceTo(to);
        const length = 0.4;
        // arrow Mesh
        const geometry = new ConeGeometry(length * 0.5, length, 32);
        const coneGeometryCopy = geometry.clone();
        const material = new MeshStandardMaterial({
            color: 0x000000,
            roughness: 0.35,
            depthTest: false,
            transparent: true
        });
        const matrix = new Matrix4()
            .makeRotationZ(Math.PI * 0.5)
            .premultiply(new Matrix4().makeTranslation(length * 0.5 + length * 0.2, 0, 0));
        geometry.applyMatrix4(matrix);
        const cone = new Mesh(geometry, material);
        cone.renderOrder = 5;

        const matrixCopy = new Matrix4()
            .makeRotationZ(-Math.PI * 0.5)
            .premultiply(new Matrix4().makeTranslation(distance - length * 0.5 - length * 0.2, 0, 0));
        coneGeometryCopy.applyMatrix4(matrixCopy);
        const coneCopy = new Mesh(coneGeometryCopy, material);
        coneCopy.renderOrder = 5;

        // line Mesh
        const points = [];
        points.push(new Vector3(0, 0, 0));
        points.push(new Vector3(distance, 0, 0));

        const fittedCurve = new CatmullRomCurve3(points, true, 'centripetal', 0);
        const tubeGeometry = new TubeGeometry(fittedCurve, 256, 0.04, 64, false);
        const tubeMaterial = new MeshBasicMaterial({ color: 0x000000, depthTest: false, transparent: true });
        const tubeMesh = new Mesh(tubeGeometry, tubeMaterial);
        tubeMesh.renderOrder = 5;

        // points Mesh from/to
        const pointGeometry = new SphereGeometry(0.1, 32, 16);
        const pointMaterial = new MeshBasicMaterial({ color: 0xfe5405, depthTest: false, transparent: true });
        const pointMesh = new Mesh(pointGeometry, pointMaterial);
        pointMesh.renderOrder = 5;

        const pointMeshFrom = pointMesh.clone();
        const pointMeshTo = pointMesh.clone();

        pointMeshFrom.position.copy(from);
        pointMeshTo.position.copy(to);

        const object3d = new Object3D();
        object3d.add(tubeMesh);

        if (distance > 1) {
            object3d.add(cone).add(coneCopy);
        }

        // distance tip
        const backColor = new Color(0xffffff);
        const textColor = new Color(0x000000);
        const fontsize = 400;
        const backgroundColor = { r: backColor.r * 255, g: backColor.g * 255, b: backColor.b * 255, a: 0.9 };
        const spriteMaterial = {
            fontsize,
            backgroundColor,
            textColor: { r: textColor.r * 255, g: textColor.g * 255, b: textColor.b * 255, a: 1.0 }
        };
        const textSprite = drawMeasuresSprite(`${roundDown(distance, 2).toFixed(2)}mm`, spriteMaterial);

        const smallDistance = from.z >= to.z ? from : to;
        const center = this.mode === 'distance' ? smallDistance : from;
        textSprite.position.copy(center);

        const points3d = new Object3D();
        points3d.add(pointMeshFrom).add(pointMeshTo).add(textSprite);

        const quaternion = new Quaternion().setFromUnitVectors(new Vector3(1, 0, 0), direction);
        object3d.quaternion.copy(quaternion);
        object3d.position.copy(from);

        const groups = new Object3D();
        groups.add(points3d).add(object3d);
        groups.name = this.measuresLinesName;
        groups.attribute = {
            pointFrom: from.clone(),
            pointTo: to.clone(),
            distance
        };
        this._scene.add(groups);
    };

    removeMeasuresObjects = () => {
        if (!this._scene) return;
        const objects = this._scene.children.filter((el) => el.name === this.measuresLinesName);
        objects.forEach((el) => {
            this._scene.remove(el);
        });
        this.pointMeshFrom.visible = false;
        this.pointMeshTo.visible = false;
    };

    onPointerdown = (e) => {
        if (!this.enabled) return;
        if (e.button !== 0) return;
        if (this.mode === 'thickness') {
            this.doThickness();
        } else {
            this.doDistance();
        }
    };

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

        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 objects = this._scene.children.filter(
            (el) => el.name.includes('active:') && el.isMesh === true && el.visible && !el.name.includes('back')
        );
        const detects = this._rootObjects.length === 0 ? objects : this._rootObjects;

        const res = this.raycaster.intersectObjects(detects);
        if (res.length > 0) {
            if (this.pointMeshTo) {
                this.pointMeshTo.visible = true;
                this.pointMeshTo.position.copy(res[0].point);
            }
            if (this.mode === 'thickness') {
                this.hoverMesh = res[0].object;
                this.thicknessNormal = res[0].normal;
            }
        } else {
            if (this.pointMeshTo) {
                this.pointMeshTo.visible = false;
            }
            this.hoverMesh = null;
        }
        this._camera.near = -10000;
    };

    onPointerup = (e) => {
        if (!this.enabled) return;
    };

    doDistance = () => {
        if (this.pointMeshFrom.visible && this.pointMeshTo.visible) {
            this.generateMeasureLines(this.pointMeshFrom.position.clone(), this.pointMeshTo.position.clone());
            this.pointMeshFrom.visible = false;
        } else {
            this.pointMeshFrom.position.copy(this.pointMeshTo.position);
            this.pointMeshFrom.visible = true;
        }
    };

    doThickness = () => {
        if (!this.hoverMesh) return;
        this.pointMeshFrom.position.copy(this.pointMeshTo.position);
        this.pointMeshFrom.visible = false;
        const origin = this.pointMeshFrom.position.clone();
        origin.add(this.thicknessNormal.clone().multiplyScalar(0.01));
        this.raycaster.ray.set(origin, this.thicknessNormal.clone().multiplyScalar(-1).normalize());
        const res = this.raycaster.intersectObjects([this.hoverMesh]);
        if (res.length > 1) {
            this.generateMeasureLines(origin, res[1].point);
        }
    };
}

export default Measures;