meshDetection.js 8.03 KB
import * as THREE from 'three';
import { CONTAINED, INTERSECTED, NOT_INTERSECTED } from 'three-mesh-bvh';
import { getConvexHull } from './getConvexHull';
import { pointRayCrossesSegments, lineCrossesLine } from './supportCalculations';

// Detect the intersecting portions of the target mesh and a shape drawn on the camera
export const detectIntersectionByShape = (camera, targetMesh, shapePoints, isSelectModel = true) => {
    // Calculated the contained triangles
    const toScreenSpaceMatrix = new THREE.Matrix4();
    const boxPoints = new Array(8).fill().map(() => new THREE.Vector3());
    const boxLines = new Array(12).fill().map(() => new THREE.Line3());
    const lassoSegments = [];
    const perBoundsSegments = [];

    targetMesh.updateMatrixWorld(true);
    toScreenSpaceMatrix
        .copy(targetMesh.matrixWorld)
        .premultiply(camera.matrixWorldInverse)
        .premultiply(camera.projectionMatrix);

    // create scratch points and lines to use for selection
    while (lassoSegments.length < shapePoints.length) {
        lassoSegments.push(new THREE.Line3());
    }

    lassoSegments.length = shapePoints.length;

    for (let s = 0, l = shapePoints.length; s < l; s += 3) {
        const line = lassoSegments[s];
        const sNext = (s + 3) % l;
        line.start.x = shapePoints[s];
        line.start.y = shapePoints[s + 1];

        line.end.x = shapePoints[sNext];
        line.end.y = shapePoints[sNext + 1];
    }

    const indices = [];
    targetMesh.geometry.boundsTree.shapecast(
        // targetMesh, // MeshBVH: The function signature for "shapecast" has changed and no longer takes Mesh.
        {
            intersectsBounds: (box, isLeaf, score, depth) => {
                // Get the bounding box points
                const { min, max } = box;
                let index = 0;

                let minY = Infinity;
                let maxY = -Infinity;
                let minX = Infinity;
                for (let x = 0; x <= 1; x++) {
                    for (let y = 0; y <= 1; y++) {
                        for (let z = 0; z <= 1; z++) {
                            const v = boxPoints[index];
                            v.x = x === 0 ? min.x : max.x;
                            v.y = y === 0 ? min.y : max.y;
                            v.z = z === 0 ? min.z : max.z;
                            v.w = 1;
                            v.applyMatrix4(toScreenSpaceMatrix);
                            index++;

                            if (v.y < minY) minY = v.y;
                            if (v.y > maxY) maxY = v.y;
                            if (v.x < minX) minX = v.x;
                        }
                    }
                }

                // Find all the relevant segments here and cache them in the above array for
                // subsequent child checks to use.
                const parentSegments = perBoundsSegments[depth - 1] || lassoSegments;
                const segmentsToCheck = perBoundsSegments[depth] || [];
                segmentsToCheck.length = 0;
                perBoundsSegments[depth] = segmentsToCheck;
                for (let i = 0, l = parentSegments.length; i < l; i++) {
                    const line = parentSegments[i];
                    const sx = line.start.x;
                    const sy = line.start.y;
                    const ex = line.end.x;
                    const ey = line.end.y;
                    if (sx < minX && ex < minX) continue;

                    const startAbove = sy > maxY;
                    const endAbove = ey > maxY;
                    if (startAbove && endAbove) continue;

                    const startBelow = sy < minY;
                    const endBelow = ey < minY;
                    if (startBelow && endBelow) continue;

                    segmentsToCheck.push(line);
                }

                if (segmentsToCheck.length === 0) {
                    return NOT_INTERSECTED;
                }

                // Get the screen space hull lines
                const hull = getConvexHull(boxPoints);
                // if (hull == null) console.log(new Set(boxPoints));
                if (hull == null) return NOT_INTERSECTED; // 2021.08.11: Randomly gets a null at the end in the first few edits.
                const lines = hull.map((p, i) => {
                    const nextP = hull[(i + 1) % hull.length];
                    const line = boxLines[i];
                    line.start.copy(p);
                    line.end.copy(nextP);
                    return line;
                });

                // If a lasso point is inside the hull then it's intersected and cannot be contained
                if (pointRayCrossesSegments(segmentsToCheck[0].start, lines) % 2 === 1) {
                    return INTERSECTED;
                }

                // check if the screen space hull is in the lasso
                let crossings = 0;
                for (let i = 0, l = hull.length; i < l; i++) {
                    const v = hull[i];
                    const pCrossings = pointRayCrossesSegments(v, segmentsToCheck);

                    if (i === 0) {
                        crossings = pCrossings;
                    }

                    // if two points on the hull have different amounts of crossings then
                    // it can only be intersected
                    if (crossings !== pCrossings) {
                        return INTERSECTED;
                    }
                }

                // check if there are any intersections
                for (let i = 0, l = lines.length; i < l; i++) {
                    const boxLine = lines[i];
                    for (let s = 0, ls = segmentsToCheck.length; s < ls; s++) {
                        if (lineCrossesLine(boxLine, segmentsToCheck[s])) {
                            return INTERSECTED;
                        }
                    }
                }
                return crossings % 2 === 0 ? NOT_INTERSECTED : CONTAINED;
            },
            intersectsTriangle: (tri, index, contained, depth) => {
                const i3 = index * 3;
                const a = i3 + 0;
                const b = i3 + 1;
                const c = i3 + 2;

                // if the parent bounds were marked as contained
                if (contained) {
                    indices.push(a, b, c);
                    return isSelectModel;
                }

                if (perBoundsSegments.length === 0) return false;

                // check all the segments if using no bounds tree
                const segmentsToCheck = true ? perBoundsSegments[depth] : lassoSegments;

                // get the projected vertices
                const vertices = [tri.a, tri.b, tri.c];

                for (let j = 0; j < 3; j++) {
                    const v = vertices[j];
                    v.applyMatrix4(toScreenSpaceMatrix);

                    const crossings = pointRayCrossesSegments(v, segmentsToCheck);
                    if (crossings % 2 === 1) {
                        indices.push(a, b, c);
                        return isSelectModel;
                    }
                }

                // get the lines for the triangle
                const lines = [boxLines[0], boxLines[1], boxLines[2]];

                lines[0].start.copy(tri.a);
                lines[0].end.copy(tri.b);

                lines[1].start.copy(tri.b);
                lines[1].end.copy(tri.c);

                lines[2].start.copy(tri.c);
                lines[2].end.copy(tri.a);

                for (let i = 0; i < 3; i++) {
                    const l = lines[i];
                    for (let s = 0, sl = segmentsToCheck.length; s < sl; s++) {
                        if (lineCrossesLine(l, segmentsToCheck[s])) {
                            indices.push(a, b, c);
                            return isSelectModel;
                        }
                    }
                }
                return false;
            }
        }
    );

    const indexAttr = targetMesh.geometry.index;
    const updatedIndices = [];
    indices.forEach((index) => {
        updatedIndices.push(indexAttr.getX(index));
    });

    return new Set(updatedIndices);
};