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

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>