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,[],""]
3:I[8924,[],""]
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"]
a:I[5278,[],"AsyncMetadataOutlet"]
c:I[4431,[],"ViewportBoundary"]
@ -10,7 +10,7 @@ e:I[4431,[],"MetadataBoundary"]
f:"$Sreact.suspense"
11:I[7150,[],""]
: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:{}
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"}]]

View file

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

View file

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

View file

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

View file

@ -13,6 +13,15 @@ enum Controls {
right = "right",
up = "up",
down = "down",
camera1 = "camera1",
camera2 = "camera2",
camera3 = "camera3",
camera4 = "camera4",
camera5 = "camera5",
camera6 = "camera6",
camera7 = "camera7",
camera8 = "camera8",
camera9 = "camera9",
}
const BASE_SPEED = 80;
@ -23,7 +32,7 @@ function CameraMovement() {
const { speedMultiplier, setSpeedMultiplier } = useControls();
const [subscribe, getKeys] = useKeyboardControls<Controls>();
const { camera, gl } = useThree();
const { nextCamera } = useCameras();
const { nextCamera, setCameraIndex, cameraCount } = useCameras();
const controlsRef = useRef<PointerLockControls | null>(null);
// Scratch vectors to avoid allocations each frame
@ -53,6 +62,30 @@ function CameraMovement() {
};
}, [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
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
@ -135,6 +168,15 @@ const KEYBOARD_CONTROLS = [
{ name: Controls.right, keys: ["KeyD"] },
{ name: Controls.up, keys: ["Space"] },
{ 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() {

View file

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

View file

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

View file

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