press 1-9 to select cameras, only show error placeholders in debug mode

This commit is contained in:
Brian Beck 2025-11-26 17:19:17 -08:00
parent 825b21acfd
commit 3bb3f7afbd
17 changed files with 101 additions and 6472 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
2:I[9766,[],""] 2:I[9766,[],""]
3:I[8924,[],""] 3:I[8924,[],""]
4:I[1959,[],"ClientPageRoot"] 4:I[1959,[],"ClientPageRoot"]
5:I[8519,["367","static/chunks/b536a0f1-05ee2c75df4a3b9d.js","831","static/chunks/bd904a5c-3aea2adebde6f067.js","664","static/chunks/a3cd4a83-5c5b758da206345b.js","794","static/chunks/f6211eb1-4f3105d2434536dc.js","413","static/chunks/1329d575-16915d95397758f8.js","369","static/chunks/369-34d0e374bac367b6.js","974","static/chunks/app/page-c14a97db6896095b.js"],"default"] 5:I[8519,["367","static/chunks/b536a0f1-05ee2c75df4a3b9d.js","831","static/chunks/bd904a5c-3aea2adebde6f067.js","664","static/chunks/a3cd4a83-5c5b758da206345b.js","794","static/chunks/f6211eb1-4f3105d2434536dc.js","413","static/chunks/1329d575-16915d95397758f8.js","369","static/chunks/369-34d0e374bac367b6.js","974","static/chunks/app/page-14947d32ca3dbe94.js"],"default"]
8:I[4431,[],"OutletBoundary"] 8:I[4431,[],"OutletBoundary"]
a:I[5278,[],"AsyncMetadataOutlet"] a:I[5278,[],"AsyncMetadataOutlet"]
c:I[4431,[],"ViewportBoundary"] c:I[4431,[],"ViewportBoundary"]
@ -10,7 +10,7 @@ e:I[4431,[],"MetadataBoundary"]
f:"$Sreact.suspense" f:"$Sreact.suspense"
11:I[7150,[],""] 11:I[7150,[],""]
:HL["/t2-mapper/_next/static/css/71910d47103c2b82.css","style"] :HL["/t2-mapper/_next/static/css/71910d47103c2b82.css","style"]
0:{"P":null,"b":"JRzZ4JcBdWRKU37DH9xVb","p":"/t2-mapper","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/css/71910d47103c2b82.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","searchParams":{},"params":{},"promises":["$@6","$@7"]}],null,["$","$L8",null,{"children":["$L9",["$","$La",null,{"promise":"$@b"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Lc",null,{"children":"$Ld"}],null],["$","$Le",null,{"children":["$","div",null,{"hidden":true,"children":["$","$f",null,{"fallback":null,"children":"$L10"}]}]}]]}],false]],"m":"$undefined","G":["$11",[]],"s":false,"S":true} 0:{"P":null,"b":"9vBBTV2JEIUmXNr6BPGfH","p":"/t2-mapper","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/css/71910d47103c2b82.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","searchParams":{},"params":{},"promises":["$@6","$@7"]}],null,["$","$L8",null,{"children":["$L9",["$","$La",null,{"promise":"$@b"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Lc",null,{"children":"$Ld"}],null],["$","$Le",null,{"children":["$","div",null,{"hidden":true,"children":["$","$f",null,{"fallback":null,"children":"$L10"}]}]}]]}],false]],"m":"$undefined","G":["$11",[]],"s":false,"S":true}
6:{} 6:{}
7:"$0:f:0:1:2:children:1:props:children:0:props:params" 7:"$0:f:0:1:2:children:1:props:children:0:props:params"
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]

View file

@ -21,6 +21,8 @@ interface CamerasContextValue {
registerCamera: (camera: any) => void; registerCamera: (camera: any) => void;
unregisterCamera: (camera: any) => void; unregisterCamera: (camera: any) => void;
nextCamera: () => void; nextCamera: () => void;
setCameraIndex: (index: number) => void;
cameraCount: number;
} }
const CamerasContext = createContext<CamerasContextValue | null>(null); const CamerasContext = createContext<CamerasContextValue | null>(null);
@ -52,15 +54,25 @@ export function CamerasProvider({ children }: { children: ReactNode }) {
}); });
}, []); }, []);
const cameraCount = Object.keys(cameraMap).length;
const nextCamera = useCallback(() => { const nextCamera = useCallback(() => {
setCameraIndex((prev) => { setCameraIndex((prev) => {
const cameraCount = Object.keys(cameraMap).length;
if (cameraCount === 0) { if (cameraCount === 0) {
return 0; return 0;
} }
return (prev + 1) % cameraCount; return (prev + 1) % cameraCount;
}); });
}, [cameraMap]); }, [cameraCount]);
const setCamera = useCallback(
(index: number) => {
if (index >= 0 && index < cameraCount) {
setCameraIndex(index);
}
},
[cameraCount]
);
useEffect(() => { useEffect(() => {
const cameraCount = Object.keys(cameraMap).length; const cameraCount = Object.keys(cameraMap).length;
@ -82,8 +94,10 @@ export function CamerasProvider({ children }: { children: ReactNode }) {
registerCamera, registerCamera,
unregisterCamera, unregisterCamera,
nextCamera, nextCamera,
setCameraIndex: setCamera,
cameraCount,
}), }),
[registerCamera, unregisterCamera, nextCamera] [registerCamera, unregisterCamera, nextCamera, setCamera, cameraCount]
); );
return ( return (

View file

@ -67,6 +67,11 @@ export function ShapePlaceholder({ color }: { color: string }) {
); );
} }
export function DebugPlaceholder({ color }: { color: string }) {
const { debugMode } = useDebug();
return debugMode ? <ShapePlaceholder color={color} /> : null;
}
export type StaticShapeType = "StaticShape" | "TSStatic" | "Item" | "Turret"; export type StaticShapeType = "StaticShape" | "TSStatic" | "Item" | "Turret";
export const ShapeModel = memo(function ShapeModel() { export const ShapeModel = memo(function ShapeModel() {

View file

@ -1,4 +1,5 @@
import { memo, Suspense, useMemo } from "react"; import { memo, Suspense, useMemo } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { Mesh } from "three"; import { Mesh } from "three";
import { useGLTF, useTexture } from "@react-three/drei"; import { useGLTF, useTexture } from "@react-three/drei";
import { BASE_URL, interiorTextureToUrl, interiorToUrl } from "../loaders"; import { BASE_URL, interiorTextureToUrl, interiorToUrl } from "../loaders";
@ -75,15 +76,20 @@ export const InteriorModel = memo(
} }
); );
function InteriorPlaceholder() { function InteriorPlaceholder({ color }: { color: string }) {
return ( return (
<mesh> <mesh>
<boxGeometry args={[10, 10, 10]} /> <boxGeometry args={[10, 10, 10]} />
<meshStandardMaterial color="orange" wireframe /> <meshStandardMaterial color={color} wireframe />
</mesh> </mesh>
); );
} }
function DebugInteriorPlaceholder() {
const { debugMode } = useDebug();
return debugMode ? <InteriorPlaceholder color="red" /> : null;
}
export const InteriorInstance = memo(function InteriorInstance({ export const InteriorInstance = memo(function InteriorInstance({
object, object,
}: { }: {
@ -96,9 +102,11 @@ export const InteriorInstance = memo(function InteriorInstance({
return ( return (
<group position={position} quaternion={q} scale={scale}> <group position={position} quaternion={q} scale={scale}>
<Suspense fallback={<InteriorPlaceholder />}> <ErrorBoundary fallback={<DebugInteriorPlaceholder />}>
<InteriorModel interiorFile={interiorFile} /> <Suspense fallback={<InteriorPlaceholder color="orange" />}>
</Suspense> <InteriorModel interiorFile={interiorFile} />
</Suspense>
</ErrorBoundary>
</group> </group>
); );
}); });

View file

@ -7,7 +7,7 @@ import {
getRotation, getRotation,
getScale, getScale,
} from "../mission"; } from "../mission";
import { ShapeModel, ShapePlaceholder } from "./GenericShape"; import { DebugPlaceholder, ShapeModel, ShapePlaceholder } from "./GenericShape";
import { ShapeInfoProvider } from "./ShapeInfoProvider"; import { ShapeInfoProvider } from "./ShapeInfoProvider";
import { useSimGroup } from "./SimGroup"; import { useSimGroup } from "./SimGroup";
import { FloatingLabel } from "./FloatingLabel"; import { FloatingLabel } from "./FloatingLabel";
@ -84,7 +84,7 @@ export function Item({ object }: { object: ConsoleObject }) {
<ShapeInfoProvider shapeName={shapeName} type="Item"> <ShapeInfoProvider shapeName={shapeName} type="Item">
<group position={position} quaternion={q} scale={scale}> <group position={position} quaternion={q} scale={scale}>
{shapeName ? ( {shapeName ? (
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}> <ErrorBoundary fallback={<DebugPlaceholder color="red" />}>
<Suspense fallback={<ShapePlaceholder color="pink" />}> <Suspense fallback={<ShapePlaceholder color="pink" />}>
<ShapeModel /> <ShapeModel />
{label ? ( {label ? (
@ -93,7 +93,7 @@ export function Item({ object }: { object: ConsoleObject }) {
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
) : ( ) : (
<ShapePlaceholder color="orange" /> <DebugPlaceholder color="orange" />
)} )}
</group> </group>
</ShapeInfoProvider> </ShapeInfoProvider>

View file

@ -13,6 +13,15 @@ enum Controls {
right = "right", right = "right",
up = "up", up = "up",
down = "down", down = "down",
camera1 = "camera1",
camera2 = "camera2",
camera3 = "camera3",
camera4 = "camera4",
camera5 = "camera5",
camera6 = "camera6",
camera7 = "camera7",
camera8 = "camera8",
camera9 = "camera9",
} }
const BASE_SPEED = 80; const BASE_SPEED = 80;
@ -23,7 +32,7 @@ function CameraMovement() {
const { speedMultiplier, setSpeedMultiplier } = useControls(); const { speedMultiplier, setSpeedMultiplier } = useControls();
const [subscribe, getKeys] = useKeyboardControls<Controls>(); const [subscribe, getKeys] = useKeyboardControls<Controls>();
const { camera, gl } = useThree(); const { camera, gl } = useThree();
const { nextCamera } = useCameras(); const { nextCamera, setCameraIndex, cameraCount } = useCameras();
const controlsRef = useRef<PointerLockControls | null>(null); const controlsRef = useRef<PointerLockControls | null>(null);
// Scratch vectors to avoid allocations each frame // Scratch vectors to avoid allocations each frame
@ -53,6 +62,30 @@ function CameraMovement() {
}; };
}, [camera, gl, nextCamera]); }, [camera, gl, nextCamera]);
// Handle number keys 1-9 for camera selection
useEffect(() => {
const cameraControls = [
Controls.camera1,
Controls.camera2,
Controls.camera3,
Controls.camera4,
Controls.camera5,
Controls.camera6,
Controls.camera7,
Controls.camera8,
Controls.camera9,
];
return subscribe((state) => {
for (let i = 0; i < cameraControls.length; i++) {
if (state[cameraControls[i]] && i < cameraCount) {
setCameraIndex(i);
break;
}
}
});
}, [subscribe, setCameraIndex, cameraCount]);
// Handle mousewheel for speed adjustment // Handle mousewheel for speed adjustment
useEffect(() => { useEffect(() => {
const handleWheel = (e: WheelEvent) => { const handleWheel = (e: WheelEvent) => {
@ -135,6 +168,15 @@ const KEYBOARD_CONTROLS = [
{ name: Controls.right, keys: ["KeyD"] }, { name: Controls.right, keys: ["KeyD"] },
{ name: Controls.up, keys: ["Space"] }, { name: Controls.up, keys: ["Space"] },
{ name: Controls.down, keys: ["ShiftLeft", "ShiftRight"] }, { name: Controls.down, keys: ["ShiftLeft", "ShiftRight"] },
{ name: Controls.camera1, keys: ["Digit1"] },
{ name: Controls.camera2, keys: ["Digit2"] },
{ name: Controls.camera3, keys: ["Digit3"] },
{ name: Controls.camera4, keys: ["Digit4"] },
{ name: Controls.camera5, keys: ["Digit5"] },
{ name: Controls.camera6, keys: ["Digit6"] },
{ name: Controls.camera7, keys: ["Digit7"] },
{ name: Controls.camera8, keys: ["Digit8"] },
{ name: Controls.camera9, keys: ["Digit9"] },
]; ];
export function ObserverControls() { export function ObserverControls() {

View file

@ -7,7 +7,7 @@ import {
getRotation, getRotation,
getScale, getScale,
} from "../mission"; } from "../mission";
import { ShapeModel, ShapePlaceholder } from "./GenericShape"; import { DebugPlaceholder, ShapeModel, ShapePlaceholder } from "./GenericShape";
import { ShapeInfoProvider } from "./ShapeInfoProvider"; import { ShapeInfoProvider } from "./ShapeInfoProvider";
const dataBlockToShapeName = { const dataBlockToShapeName = {
@ -61,13 +61,13 @@ export function StaticShape({ object }: { object: ConsoleObject }) {
<ShapeInfoProvider shapeName={shapeName} type="StaticShape"> <ShapeInfoProvider shapeName={shapeName} type="StaticShape">
<group position={position} quaternion={q} scale={scale}> <group position={position} quaternion={q} scale={scale}>
{shapeName ? ( {shapeName ? (
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}> <ErrorBoundary fallback={<DebugPlaceholder color="red" />}>
<Suspense fallback={<ShapePlaceholder color="yellow" />}> <Suspense fallback={<ShapePlaceholder color="yellow" />}>
<ShapeModel /> <ShapeModel />
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
) : ( ) : (
<ShapePlaceholder color="orange" /> <DebugPlaceholder color="orange" />
)} )}
</group> </group>
</ShapeInfoProvider> </ShapeInfoProvider>

View file

@ -7,7 +7,7 @@ import {
getRotation, getRotation,
getScale, getScale,
} from "../mission"; } from "../mission";
import { ShapeModel, ShapePlaceholder } from "./GenericShape"; import { DebugPlaceholder, ShapeModel, ShapePlaceholder } from "./GenericShape";
import { ShapeInfoProvider } from "./ShapeInfoProvider"; import { ShapeInfoProvider } from "./ShapeInfoProvider";
export function TSStatic({ object }: { object: ConsoleObject }) { export function TSStatic({ object }: { object: ConsoleObject }) {
@ -24,7 +24,7 @@ export function TSStatic({ object }: { object: ConsoleObject }) {
return ( return (
<ShapeInfoProvider shapeName={shapeName} type="TSStatic"> <ShapeInfoProvider shapeName={shapeName} type="TSStatic">
<group position={position} quaternion={q} scale={scale}> <group position={position} quaternion={q} scale={scale}>
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}> <ErrorBoundary fallback={<DebugPlaceholder color="red" />}>
<Suspense fallback={<ShapePlaceholder color="yellow" />}> <Suspense fallback={<ShapePlaceholder color="yellow" />}>
<ShapeModel /> <ShapeModel />
</Suspense> </Suspense>

View file

@ -7,7 +7,7 @@ import {
getRotation, getRotation,
getScale, getScale,
} from "../mission"; } from "../mission";
import { ShapeModel, ShapePlaceholder } from "./GenericShape"; import { DebugPlaceholder, ShapeModel, ShapePlaceholder } from "./GenericShape";
import { ShapeInfoProvider } from "./ShapeInfoProvider"; import { ShapeInfoProvider } from "./ShapeInfoProvider";
const dataBlockToShapeName = { const dataBlockToShapeName = {
@ -36,14 +36,16 @@ function getDataBlockShape(dataBlock: string) {
export function Turret({ object }: { object: ConsoleObject }) { export function Turret({ object }: { object: ConsoleObject }) {
const dataBlock = getProperty(object, "dataBlock").value; const dataBlock = getProperty(object, "dataBlock").value;
const initialBarrel = getProperty(object, "initialBarrel").value; const initialBarrel = getProperty(object, "initialBarrel")?.value;
const position = useMemo(() => getPosition(object), [object]); const position = useMemo(() => getPosition(object), [object]);
const q = useMemo(() => getRotation(object), [object]); const q = useMemo(() => getRotation(object), [object]);
const scale = useMemo(() => getScale(object), [object]); const scale = useMemo(() => getScale(object), [object]);
const shapeName = getDataBlockShape(dataBlock); const shapeName = getDataBlockShape(dataBlock);
const barrelShapeName = getDataBlockShape(initialBarrel); const barrelShapeName = initialBarrel
? getDataBlockShape(initialBarrel)
: undefined;
if (!shapeName) { if (!shapeName) {
console.error(`<Turret> missing shape for dataBlock: ${dataBlock}`); console.error(`<Turret> missing shape for dataBlock: ${dataBlock}`);
@ -58,24 +60,24 @@ export function Turret({ object }: { object: ConsoleObject }) {
<ShapeInfoProvider shapeName={shapeName} type="Turret"> <ShapeInfoProvider shapeName={shapeName} type="Turret">
<group position={position} quaternion={q} scale={scale}> <group position={position} quaternion={q} scale={scale}>
{shapeName ? ( {shapeName ? (
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}> <ErrorBoundary fallback={<DebugPlaceholder color="red" />}>
<Suspense fallback={<ShapePlaceholder color="yellow" />}> <Suspense fallback={<ShapePlaceholder color="yellow" />}>
<ShapeModel /> <ShapeModel />
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
) : ( ) : (
<ShapePlaceholder color="orange" /> <DebugPlaceholder color="orange" />
)} )}
<ShapeInfoProvider shapeName={barrelShapeName} type="Turret"> <ShapeInfoProvider shapeName={barrelShapeName} type="Turret">
<group position={[0, 1.5, 0]}> <group position={[0, 1.5, 0]}>
{barrelShapeName ? ( {barrelShapeName ? (
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}> <ErrorBoundary fallback={<DebugPlaceholder color="red" />}>
<Suspense fallback={<ShapePlaceholder color="yellow" />}> <Suspense fallback={<ShapePlaceholder color="yellow" />}>
<ShapeModel /> <ShapeModel />
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
) : ( ) : (
<ShapePlaceholder color="orange" /> <DebugPlaceholder color="orange" />
)} )}
</group> </group>
</ShapeInfoProvider> </ShapeInfoProvider>