import React from "react"; import { Frustum, type PerspectiveCamera, Vector3 } from "three"; import useEventListener from "./useEventListener"; import useMeasure from "./useMeasure"; type Box = { x: number; y: number; width: number; height: number }; export default function useDragSelect({ setSelectionBounds, onDragEnd, onDragStart, }: { setSelectionBounds?: (param: { x1: number; x2: number; y1: number; y2: number }) => void; onDragStart?: () => void; onDragEnd?: () => void; }) { const [dragPosition, setDragPosition] = React.useState(null); const [initialPosition, setInitialPosition] = React.useState<{ x: number; y: number; } | null>(null); const [ref, dimensions, , node] = useMeasure(); // Create a ref that stores handler const savedHandler = React.useRef(undefined); // Update ref.current value if handler changes. // This allows our effect below to always get latest handler ... // ... without us needing to pass it in effect deps array ... // ... and potentially cause effect to re-run every render. React.useEffect(() => { savedHandler.current = setSelectionBounds; }, [setSelectionBounds]); function calculateCoordinates({ x, y, width, height }: Box) { return { x1: x / dimensions.width, x2: (x + width) / dimensions.width, y1: y / dimensions.height, y2: (y + height) / dimensions.height, }; } useEventListener("contextmenu", (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); }); useEventListener("pointerdown", (e: MouseEvent) => { if (!e.shiftKey) return; e.preventDefault(); onDragStart?.(); if (e.target === node) { setInitialPosition({ x: e.clientX - dimensions.left, y: e.clientY - dimensions.top, }); setDragPosition({ x: e.clientX - dimensions.left, y: e.clientY - dimensions.top, width: 0, height: 0, }); } }); useEventListener("pointerup", () => { if (dragPosition) { onDragEnd?.(); } setDragPosition(null); setInitialPosition(null); }); useEventListener("pointermove", (e: MouseEvent) => { if (dragPosition && initialPosition) { const position = { x: e.clientX - dimensions.left < initialPosition.x ? e.clientX - dimensions.left : initialPosition.x, y: e.clientY - dimensions.top < initialPosition.y ? e.clientY - dimensions.top : initialPosition.y, width: Math.abs(e.clientX - initialPosition.x - dimensions.left), height: Math.abs(e.clientY - initialPosition.y - dimensions.top), }; setDragPosition(position); setSelectionBounds?.(calculateCoordinates(position)); } }); return [ref, dragPosition, node] as const; } export const DragSelection = ({ width, height, x, y, className, }: { width: number; height: number; x: number; y: number; className?: string; }) => { return (
); }; const frustum = new Frustum(); const tmpPoint = new Vector3(); const vecNear = new Vector3(); const vecTopLeft = new Vector3(); const vecTopRight = new Vector3(); const vecDownRight = new Vector3(); const vecDownLeft = new Vector3(); const vectemp1 = new Vector3(); const vectemp2 = new Vector3(); const vectemp3 = new Vector3(); const tempVec = new Vector3(); export function get3dSelectedObjects( objects: { id: number; position: { x: number; y: number; z: number; }; }[], camera: PerspectiveCamera, startPoint: Vector3, endPoint: Vector3, ) { // Avoid invalid frustum const deep = Number.MAX_VALUE; if (startPoint.x === endPoint.x) { endPoint.x += Number.EPSILON; } if (startPoint.y === endPoint.y) { endPoint.y += Number.EPSILON; } camera.updateProjectionMatrix(); camera.updateMatrixWorld(); tmpPoint.copy(startPoint); tmpPoint.x = Math.min(startPoint.x, endPoint.x); tmpPoint.y = Math.max(startPoint.y, endPoint.y); endPoint.x = Math.max(startPoint.x, endPoint.x); endPoint.y = Math.min(startPoint.y, endPoint.y); vecNear.setFromMatrixPosition(camera.matrixWorld); vecTopLeft.copy(tmpPoint); vecTopRight.set(endPoint.x, tmpPoint.y, 0); vecDownRight.copy(endPoint); vecDownLeft.set(tmpPoint.x, endPoint.y, 0); vecTopLeft.unproject(camera); vecTopRight.unproject(camera); vecDownRight.unproject(camera); vecDownLeft.unproject(camera); vectemp1.copy(vecTopLeft).sub(vecNear); vectemp2.copy(vecTopRight).sub(vecNear); vectemp3.copy(vecDownRight).sub(vecNear); vectemp1.normalize(); vectemp2.normalize(); vectemp3.normalize(); vectemp1.multiplyScalar(deep); vectemp2.multiplyScalar(deep); vectemp3.multiplyScalar(deep); vectemp1.add(vecNear); vectemp2.add(vecNear); vectemp3.add(vecNear); const planes = frustum.planes; planes[0].setFromCoplanarPoints(vecNear, vecTopLeft, vecTopRight); planes[1].setFromCoplanarPoints(vecNear, vecTopRight, vecDownRight); planes[2].setFromCoplanarPoints(vecDownRight, vecDownLeft, vecNear); planes[3].setFromCoplanarPoints(vecDownLeft, vecTopLeft, vecNear); planes[4].setFromCoplanarPoints(vecTopRight, vecDownRight, vecDownLeft); planes[5].setFromCoplanarPoints(vectemp3, vectemp2, vectemp1); planes[5].normal.multiplyScalar(-1); const collection: number[] = []; // TODO: Once we get a 'Flatten Y diimension" button in place, update this. objects.forEach((object) => { tempVec.set(object.position.x, 0, object.position.z); if (frustum.containsPoint(tempVec)) { collection.push(object.id); } }); return collection; }