Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
24 kB · 817 lines
TSX
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818import { Edges, Html, Line, Outlines, useGLTF } from "@react-three/drei";import { useFrame } from "@react-three/fiber";import BracketTexture from "@thorium/cards/Pilot/bracket.svg";import ReticleTexture from "@thorium/cards/Pilot/reticle.svg";import UnidentifiedTexture from "@thorium/cards/Pilot/unidentified.svg";import Explosion from "@thorium/components/Starmap/Effects/Explosion";import { useShipSprite } from "@thorium/components/Starmap/ShipSprite";import { useGetStarmapStore } from "@thorium/components/Starmap/starmapStore";import { q } from "@thorium/context/AppContext";import { useCardContext } from "@thorium/context/CardContext";import type { isPlanet, isStar } from "@thorium/ecs-components/list";import type { satellite } from "@thorium/ecs-components/satellite";import useAnimationFrame from "@thorium/hooks/useAnimationFrame";import { useStation } from "@thorium/routes/station/useStation";import { useLiveQuery } from "@thorium/utils/live-query/client/liveQueryContext";import { setCursor } from "@thorium/utils/setCursor";import { getOrbitPosition } from "@thorium/utils/starmap/getOrbitPosition";import { isObjectOccludedBySphere } from "@thorium/utils/starmap/isObjectOccludedBySphere";import { degToRad, solarRadiusToKilometers } from "@thorium/utils/unitTypes";import { Fragment, memo, type RefObject, Suspense, useMemo, useRef } from "react";import { ErrorBoundary } from "react-error-boundary";import { type BufferAttribute, CylinderGeometry, DoubleSide, type Group, type Mesh, MeshBasicMaterial, type Object3D, type OrthographicCamera, Plane, Quaternion, RingGeometry, Sphere, type Sprite, Vector3,} from "three";import type { Line2 } from "three-stdlib";import type z from "zod";
import { useCircleGridStore } from "./useCircleGridStore";import { WaypointEntity } from "./Waypoint";
export function CircleGridContacts({ onContactClick, onPlanetClick, targetedContactId, selectedContactId, onContactOcclusion, includeEntityIds, hideTorpedos,}: { onContactClick?: (id: number) => void; onPlanetClick?: (id: number) => void; onContactOcclusion?: (id: number, occluded: boolean) => void; targetedContactId?: number; selectedContactId?: number | null; includeEntityIds?: number[]; hideTorpedos?: boolean;}) { const store = useCircleGridStore(); const tilted = store((store) => store.tilt > 0); const useStarmapStore = useGetStarmapStore(); const systemId = useStarmapStore((store) => store.currentSystem); const [orbs] = q.starmapCore.entities.useNetRequest({ systemId, }); const [ships] = q.starmapCore.ships.useNetRequest({ systemId }); const [torpedos] = q.starmapCore.torpedos.useNetRequest({ systemId });
return ( <group> {orbs.map((entity) => { const { satellite, isPlanet, isStar } = entity.components; if (!satellite) return null; return ( <PlanetaryEntity key={entity.id} id={entity.id} satellite={satellite} isPlanet={isPlanet} isStar={isStar} onClick={onPlanetClick} isSelected={selectedContactId === entity.id} /> ); })} {ships.map(({ id, modelUrl, logoUrl, size, html }) => { if (!modelUrl || !logoUrl || (includeEntityIds && !includeEntityIds.includes(id))) return null; return ( <Suspense key={id} fallback={null}> <ErrorBoundary FallbackComponent={fallback} onError={onError}> <ShipEntity id={id} systemId={systemId} modelUrl={modelUrl} logoUrl={logoUrl} size={size} tilted={tilted} onClick={onContactClick} isTargeted={targetedContactId === id} isSelected={selectedContactId === id} onContactOcclusion={onContactOcclusion} html={html} /> </ErrorBoundary> </Suspense> ); })} {!hideTorpedos && torpedos.map(({ id, color, isDestroyed }) => { return ( <Suspense key={id} fallback={null}> <TorpedoEntity id={id} color={color} tilted={tilted} isDestroyed={isDestroyed} /> </Suspense> ); })} </group> );}export function CircleGridWaypoints() { const { shipId } = useStation(); const [waypoints] = q.waypoints.all.useNetRequest({ systemId: "all", active: true, shipId, }); const [autopilot] = q.pilot.autopilot.get.useNetRequest({ shipId }); return ( <group> {waypoints.map((waypoint) => ( <WaypointEntity key={waypoint.id} waypoint={waypoint} isLocked={waypoint.id === autopilot.destinationWaypointId} isFacing={waypoint.id === autopilot.facingWaypointIds[0]} /> ))} </group> );}
const onError = (err: unknown) => console.error(err);const fallback = () => <Fragment />;const zeroVector = new Vector3();const upVector = new Vector3(0, 1, 0);const playerQuaternion = new Quaternion();const plane = new Plane();const position = new Vector3();const sphere = new Sphere();export const ShipEntity = ({ id, systemId, modelUrl, logoUrl, size, tilted, onClick, isTargeted, isSelected, onContactOcclusion, html,}: { id: number; systemId: number | null; modelUrl: string; logoUrl: string; size: number; tilted?: boolean; onClick?: (id: number) => void; isTargeted?: boolean; isSelected?: boolean; onContactOcclusion?: (id: number, occluded: boolean) => void; html?: boolean;}) => { const { shipId } = useStation(); const [orbs] = q.starmapCore.entities.useNetRequest({ systemId, }); const [scanResults] = q.sensors.scanResult.useNetRequest({ shipId, objectId: id, }); const isIdentified = scanResults.identification;
// TODO: Use useGLTF.preload outside of this to preload the asset const model = useGLTF(modelUrl || "", false);
const scene = useMemo(() => { const scene: Object3D = model.scene.clone(true); if (scene.traverse) { scene.traverse((object: Object3D | Mesh) => { if ("material" in object) { (Array.isArray(object.material) ? object.material : [object.material]).forEach((mat) => mat.dispose(), ); object.material = new MeshBasicMaterial({ color: "white", wireframe: true, }); } }); }
return scene; }, [modelUrl]); const { interpolate } = useLiveQuery();
const spriteMap = useShipSprite(logoUrl); const reticleMap = useShipSprite(ReticleTexture); const unidentifiedMap = useShipSprite(UnidentifiedTexture);
const scale = 1 / 50; const mesh = useRef<Mesh>(null); const line = useRef<Line2>(null); const sprite = useRef<Sprite>(null); const reticle = useRef<Sprite>(null); const bracket = useRef<Group | null>(null); const shipRef = useRef<Group>(null); const arrowRef = useRef<Group>(null); const isOccludedRef = useRef(false); useFrame((props) => { const camera = props.camera as OrthographicCamera; const dx = (camera.right - camera.left) / (2 * camera.zoom); const ship = interpolate(id); const playerShip = interpolate(shipId); const playerPosition = playerShip || zeroVector; if (arrowRef.current) { arrowRef.current.visible = true; } if (!ship || !playerPosition || !playerShip) return; if (shipRef.current) { if (size && dx / (size / 1000) < 50) { if (sprite.current) { sprite.current.visible = false; } if (arrowRef.current) { arrowRef.current.visible = false; } shipRef.current.visible = true; } else { if (arrowRef.current) { arrowRef.current.visible = true; } if (sprite.current) { sprite.current.visible = true; } shipRef.current.visible = false; } } if (ship) { // We calculate orbs for occlusion purposes. We only want orbs that are closer // to the player ship than the current ship. let isOccluded = false; for (const orb of orbs) { const { satellite, isPlanet, isStar } = orb.components; if (!satellite) continue; const size = isPlanet ? isPlanet.radius : isStar ? solarRadiusToKilometers(isStar.radius) : 0; if (size === 0) continue;
const position = getOrbitPosition({ semiMajorAxis: satellite.semiMajorAxis, eccentricity: satellite.eccentricity, orbitalArc: satellite.orbitalArc, inclination: satellite.inclination, });
sphere.set(sphere.center.set(position.x, position.y, position.z), size); isOccluded = isObjectOccludedBySphere(playerPosition, ship, sphere);
if (isOccluded) break; }
if (isOccluded) { if (shipRef.current) { shipRef.current.visible = false; } if (sprite.current) { sprite.current.visible = false; } if (arrowRef.current) { arrowRef.current.visible = false; } if (mesh.current && line.current) { mesh.current.visible = false; line.current.visible = false; } if (isOccludedRef.current === false) { onContactOcclusion?.(id, true); }
isOccludedRef.current = true; return; } if (isOccludedRef.current === true) { onContactOcclusion?.(id, false); } isOccludedRef.current = false;
position.set(ship.x - playerPosition.x, ship.y - playerPosition.y, ship.z - playerPosition.z); // Since the sensor grid needs to be oriented at 0,0,0 // to properly tilt, we reposition the contacts relative // to the player ship's position. sprite.current?.position.copy(position); reticle.current?.position.copy(position); shipRef.current?.position.copy(position); arrowRef.current?.position.copy(position);
shipRef.current?.scale.setScalar(size / 1000 || 0.5); // This scale is helpful if we want to see the ships orientation in space. arrowRef.current?.scale.setScalar((dx * 20) / 1000); if (ship.r) { if (shipRef.current) { shipRef.current?.quaternion.set(ship.r.x, ship.r.y, ship.r.z, ship.r.w); } arrowRef.current?.quaternion.set(ship.r.x, ship.r.y, ship.r.z, ship.r.w); }
// Draw the vertical line from the sensor plane to the ship if (playerShip.r && mesh.current?.position) { playerQuaternion.set(playerShip.r.x, playerShip.r.y, playerShip.r.z, playerShip.r.w);
const planeVector = upVector.set(0, 1, 0).applyQuaternion(playerQuaternion); plane.set(planeVector, 0); plane.projectPoint(position, mesh.current.position); const positions = [...position.toArray(), ...mesh.current.position.toArray()]; line.current?.geometry.setPositions(positions); if (mesh.current && line.current) { if (tilted) { mesh.current.visible = true; line.current.visible = true; } else { mesh.current.visible = false; line.current.visible = false; } } } } sprite.current?.scale.setScalar(dx * 3 * scale); reticle.current?.scale.setScalar(dx * 4 * scale); bracket.current?.children.forEach((child, i) => { const bracketPosition = getBracketPosition(size / 1000, i, playerQuaternion); child.position.copy(position).add(bracketPosition); child.scale.setScalar(dx / 15); });
mesh.current?.scale.setScalar(dx * 3); if (playerShip.r) { mesh.current?.quaternion.set(playerShip.r.x, playerShip.r.y, playerShip.r.z, playerShip.r.w); mesh.current?.rotateX(Math.PI / 2); } });
const eventHandlers = { onPointerDown: () => onClick?.(id), onPointerOver: () => { if (onClick) { setCursor("pointer"); } }, onPointerOut: () => { setCursor("auto"); }, }; return ( <Fragment> <group ref={shipRef} {...eventHandlers}> <primitive object={scene} rotation={[Math.PI / 2, Math.PI, 0]} /> {html ? ( <Html className={`contact-entity entity-${id} pointer-events-none h-4 w-4 -translate-1/2`} ></Html> ) : null} </group> {id !== shipId && ( <Fragment> <sprite ref={sprite} {...eventHandlers}> <spriteMaterial attach="material" map={isIdentified ? spriteMap : unidentifiedMap} color={"white"} sizeAttenuation={true} /> </sprite> {/* This could be an instance mesh for every contact if we wanted to be really fancy... */} <group ref={arrowRef} {...eventHandlers}> <mesh position={[0, 0, 3]} rotation={[Math.PI / 2, 0, 0]}> <coneGeometry args={[1, 1, 2]} /> <meshBasicMaterial color={0xffffff} /> </mesh> </group>
<sprite ref={reticle} visible={isTargeted}> <spriteMaterial depthTest={false} attach="material" map={reticleMap} color={"red"} sizeAttenuation={true} /> </sprite> <SensorsBracket bracket={bracket} isSelected={isSelected} />
<Line ref={line} points={[ [0, 0, 0], [0, 0, 0], ]} color={"white"} lineWidth={1} /> <mesh ref={mesh}> <planeGeometry args={[0.01, 0.01]} attach="geometry" /> <meshBasicMaterial attach="material" color="white" side={DoubleSide} /> </mesh> </Fragment> )} </Fragment> );};interface PlanetaryEntityProps { id: number; satellite: z.infer<typeof satellite>; isPlanet?: z.infer<typeof isPlanet>; isStar?: z.infer<typeof isStar>; onClick?: (id: number) => void; isSelected?: boolean;}
export const PlanetaryEntity = memo( ({ id, satellite, isPlanet, isStar, onClick, isSelected }: PlanetaryEntityProps) => { const { shipId } = useStation(); const { interpolate } = useLiveQuery();
const useStarmapStore = useGetStarmapStore(); const systemId = useStarmapStore((store) => store.currentSystem); const [orbs] = q.starmapCore.entities.useNetRequest({ systemId, }); const parentEntity = useMemo( () => orbs.find((o) => o.id === satellite.parentId), [orbs, satellite.parentId], );
const bracket = useRef<Group>(null); const size = isPlanet ? isPlanet.radius : isStar ? solarRadiusToKilometers(isStar.radius) : 0; const position = useMemo( () => getOrbitPosition({ semiMajorAxis: satellite.semiMajorAxis, eccentricity: satellite.eccentricity, orbitalArc: satellite.orbitalArc, inclination: satellite.inclination, origin: parentEntity ? parentEntity.components.satellite ? getOrbitPosition(parentEntity.components.satellite) : parentEntity.components.position ? new Vector3( parentEntity.components.position.x, parentEntity.components.position.y, parentEntity.components.position.z, ) : undefined : undefined, }), [satellite, parentEntity], );
const store = useCircleGridStore();
const sensorRange = store((store) => store.zoomMax);
const ref = useRef<Group>(null); useFrame((props) => { const camera = props.camera as OrthographicCamera; const dx = (camera.right - camera.left) / (2 * camera.zoom);
const playerShip = interpolate(shipId); if (!playerShip || (!isPlanet && !isStar) || !satellite) return;
ref.current?.position.set( position.x - playerShip.x, position.y - playerShip.y, position.z - playerShip.z, ); if (playerShip.r) { playerQuaternion.set(playerShip.r.x, playerShip.r.y, playerShip.r.z, playerShip.r.w); } bracket.current?.children.forEach((child, i) => { const bracketPosition = getBracketPosition(size, i, playerQuaternion); child.position .set(position.x - playerShip.x, position.y - playerShip.y, position.z - playerShip.z) .add(bracketPosition); child.scale.setScalar(dx / 15); }); }); if ((!isPlanet && !isStar) || !satellite) return null;
return ( <> <group ref={ref} scale={[size, size, size]} rotation={[0, 0, degToRad(satellite.axialTilt)]} > <mesh onPointerDown={() => onClick?.(id)} onPointerOver={() => { if (onClick) { setCursor("pointer"); } }} onPointerOut={() => { setCursor("auto"); }} > <icosahedronGeometry args={[1, 3]} attach="geometry" /> <meshBasicMaterial wireframe color="white" attach="material" /> </mesh> {isPlanet?.ringMapAsset && <BasicRings />} </group> <SensorsBracket bracket={bracket} isSelected={isSelected} /> {isPlanet ? ( <OcclusionCone size={size} id={id} sensorRange={sensorRange} satellite={satellite} /> ) : null} </> ); },);
const position1 = new Vector3();const position2 = new Vector3();const direction = new Vector3();function OcclusionCone({ size, sensorRange, satellite: sat,}: { id: number; sensorRange: number; size: number; satellite: z.infer<typeof satellite>;}) { const { cardLoaded } = useCardContext(); const useStarmapStore = useGetStarmapStore(); const currentSystem = useStarmapStore((store) => store.currentSystem);
const [starmapEntities] = q.starmapCore.entities.useNetRequest({ systemId: currentSystem, });
const origin = useMemo(() => { const parent = starmapEntities.find((s) => s.id === sat.parentId)?.components.satellite; if (!parent) return undefined; return getOrbitPosition(parent); }, [starmapEntities, sat.parentId]);
const position = getOrbitPosition({ semiMajorAxis: sat.semiMajorAxis, eccentricity: sat.eccentricity, orbitalArc: sat.orbitalArc, inclination: sat.inclination, origin, });
const { shipId } = useStation(); const { interpolate } = useLiveQuery(); const ref = useRef<Group>(null); const cylinderRef = useRef<Mesh>(null);
useAnimationFrame(() => { const playerShip = interpolate(shipId); if (!playerShip) return; position1.set(playerShip.x, playerShip.y, playerShip.z); position2.set(position.x, position.y, position.z); direction.subVectors(position2, position1); const distance = direction.length(); ref.current?.position.copy(direction); direction.normalize(); upVector.set(0, -1, 0); ref.current?.quaternion.setFromUnitVectors(upVector, direction);
const distantRadius = (sensorRange * Math.sin(Math.atan2(size, distance))) / size; const geometry = new CylinderGeometry( 1, // radius at top distantRadius, // radius at bottom sensorRange / size, // height 32, // radial segments ); if (cylinderRef.current) { cylinderRef.current.geometry.dispose(); cylinderRef.current.geometry = geometry; } }, cardLoaded); return ( <group ref={ref} scale={[size, size, size]}> <mesh position={[0, -sensorRange / size / 2, 0]} renderOrder={-1} ref={cylinderRef}> <cylinderGeometry args={[1, 1, sensorRange / size, 8]} /> <meshBasicMaterial color={0x333333} depthTest={false} side={DoubleSide} /> </mesh> <mesh position={[0, 0, 0]} renderOrder={-1}> <sphereGeometry args={[1]} /> <meshBasicMaterial color={0x333333} depthTest={false} side={DoubleSide} /> </mesh> </group> );}
function getBracketPosition(positionScalar: number, index: number, quaternion: Quaternion) { return [ new Vector3(positionScalar, 0, positionScalar).applyQuaternion(quaternion), new Vector3(positionScalar, 0, -positionScalar).applyQuaternion(quaternion), new Vector3(-positionScalar, 0, -positionScalar).applyQuaternion(quaternion), new Vector3(-positionScalar, 0, positionScalar).applyQuaternion(quaternion), ][index];}
const SensorsBracket = ({ bracket, isSelected,}: { bracket: RefObject<Group | null>; isSelected?: boolean;}) => { const bracketMap = useShipSprite(BracketTexture); const positionScalar = 0.05; const scaleScalar = 0.15; return ( <group ref={bracket} visible={isSelected}> <sprite scale={scaleScalar} position={[positionScalar, 0, positionScalar]}> <spriteMaterial depthTest={false} attach="material" map={bracketMap} color={0x0088ff} sizeAttenuation={false} depthWrite={false} /> </sprite> <sprite scale={scaleScalar} position={[positionScalar, 0, -positionScalar]}> <spriteMaterial rotation={Math.PI / 2} depthTest={false} attach="material" map={bracketMap} color={0x0088ff} sizeAttenuation={false} depthWrite={false} /> </sprite> <sprite scale={scaleScalar} position={[-positionScalar, 0, -positionScalar]}> <spriteMaterial rotation={Math.PI} depthTest={false} attach="material" map={bracketMap} color={0x0088ff} sizeAttenuation={false} depthWrite={false} /> </sprite> <sprite scale={scaleScalar} position={[-positionScalar, 0, positionScalar]}> <spriteMaterial rotation={-Math.PI / 2} depthTest={false} attach="material" map={bracketMap} color={0x0088ff} sizeAttenuation={false} depthWrite={false} /> </sprite> </group> );};
PlanetaryEntity.displayName = "PlanetaryEntity";function BasicRings() { const geo = useMemo(() => { const geometry = new RingGeometry(1.5, 3, 64); const pos = geometry.attributes.position as BufferAttribute; const v3 = new Vector3(); for (let i = 0; i < pos.count; i++) { v3.fromBufferAttribute(pos, i); if ("setXY" in geometry.attributes.uv) { geometry.attributes.uv.setXY(i, v3.length() < 2 ? 0 : 1, 1); } } return geometry; }, []); return ( <mesh rotation={[Math.PI / 2, 0, 0]} scale={[0.7, 0.7, 0.7]} geometry={geo}> <meshBasicMaterial color={16777215} side={DoubleSide} wireframe transparent opacity={0.8} attach="material" /> </mesh> );}
export function TorpedoEntity({ id, tilted, color, isDestroyed,}: { id: number; tilted?: boolean; color: string; isDestroyed?: { explosion: string; };}) { const { interpolate } = useLiveQuery(); const ref = useRef<Mesh>(null); const explosionRef = useRef<Group>(null); const mesh = useRef<Mesh>(null); const line = useRef<Line2>(null); const { shipId } = useStation();
useFrame((props) => { const camera = props.camera as OrthographicCamera; const dx = (camera.right - camera.left) / (2 * camera.zoom); ref.current?.scale.set(0.5, 0.2, 1).multiplyScalar(dx * 0.02);
const torpedo = interpolate(id); const playerShip = interpolate(shipId); const isDestroyed = torpedo?.f === 1; const playerPosition = playerShip || zeroVector; if (!torpedo || !playerPosition || !playerShip) return;
// Since the sensor grid needs to be oriented at 0,0,0 // to properly tilt, we reposition the contacts relative // to the player ship's position. ref.current?.position.set( torpedo.x - playerPosition.x, torpedo.y - playerPosition.y, torpedo.z - playerPosition.z, ); if (torpedo.r) { ref.current?.quaternion.set(torpedo.r.x, torpedo.r.y, torpedo.r.z, torpedo.r.w); } if ( explosionRef.current && isDestroyed && explosionRef.current.position.lengthSq() === 0 && ref.current?.position ) { explosionRef.current.position.copy(ref.current.position); } if (ref.current) { if (isDestroyed) { ref.current.visible = false; } else { ref.current.visible = true; } } // Draw the vertical line from the sensor plane to the ship if (playerShip.r && ref.current?.position && mesh.current?.position) { const planeVector = upVector .set(0, 1, 0) .applyQuaternion( playerQuaternion.set(playerShip.r.x, playerShip.r.y, playerShip.r.z, playerShip.r.w), ); plane.set(planeVector, 0); plane.projectPoint(ref.current.position, mesh.current.position); const positions = [...ref.current.position.toArray(), ...mesh.current.position.toArray()]; line.current?.geometry.setPositions(positions); if (mesh.current && line.current) if (tilted) { mesh.current.visible = true; line.current.visible = true; } else { mesh.current.visible = false; line.current.visible = false; } } }); return ( <> <mesh ref={ref} visible={false} scale={[0.5, 0.2, 1]}> <icosahedronGeometry args={[1, 1]} /> <meshBasicMaterial color="black" /> <Outlines thickness={0.2} color={color} /> <Edges color={color} threshold={15} /> </mesh> {isDestroyed ? ( <group ref={explosionRef}> <Explosion /> </group> ) : null} <Line ref={line} points={[ [0, 0, 0], [0, 0, 0], ]} color={"white"} lineWidth={1} /> <mesh ref={mesh}> <planeGeometry args={[0.01, 0.01]} attach="geometry" /> <meshBasicMaterial attach="material" color="white" side={DoubleSide} /> </mesh> </> );}