Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
5.6 kB · 212 lines
TSX
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213import 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<DOMElement extends HTMLElement>({ setSelectionBounds, onDragEnd, onDragStart,}: { setSelectionBounds?: (param: { x1: number; x2: number; y1: number; y2: number }) => void; onDragStart?: () => void; onDragEnd?: () => void;}) { const [dragPosition, setDragPosition] = React.useState<Box | null>(null); const [initialPosition, setInitialPosition] = React.useState<{ x: number; y: number; } | null>(null); const [ref, dimensions, , node] = useMeasure<DOMElement>();
// Create a ref that stores handler const savedHandler = React.useRef<typeof setSelectionBounds>(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 ( <div className={`absolute top-0 left-0 h-8 w-8 border border-blue-500 bg-blue-500/10 ${className}`} style={{ width, height, transform: `translate(${x}px, ${y}px)`, }} /> );};
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;}