diff --git a/package-lock.json b/package-lock.json
index f6e8d0bf..03a2a729 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"next": "^15.5.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
+ "react-error-boundary": "^6.0.0",
"three": "^0.180.0",
"unzipper": "^0.12.3"
},
@@ -2220,6 +2221,18 @@
"react": "^19.1.1"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
+ "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/react-reconciler": {
"version": "0.31.0",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz",
diff --git a/package.json b/package.json
index 8afa6744..a483e240 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"next": "^15.5.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
+ "react-error-boundary": "^6.0.0",
"three": "^0.180.0",
"unzipper": "^0.12.3"
},
diff --git a/src/components/Item.tsx b/src/components/Item.tsx
new file mode 100644
index 00000000..1b26287e
--- /dev/null
+++ b/src/components/Item.tsx
@@ -0,0 +1,79 @@
+import { Suspense, useMemo } from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import {
+ ConsoleObject,
+ getPosition,
+ getProperty,
+ getRotation,
+ getScale,
+} from "../mission";
+import { shapeToUrl } from "../loaders";
+import { useGLTF } from "@react-three/drei";
+
+const dataBlockToShapeName = {
+ RepairPack: "pack_upgrade_repair.dts",
+};
+
+/**
+ * Load a .glb file that was converted from a .dts, used for static shapes.
+ */
+function useStaticShape(shapeName: string) {
+ const url = shapeToUrl(shapeName);
+ return useGLTF(url);
+}
+
+function ShapeModel({ shapeName }: { shapeName: string }) {
+ const { nodes } = useStaticShape(shapeName);
+
+ return (
+ <>
+ {Object.entries(nodes)
+ .filter(
+ ([name, node]: [string, any]) =>
+ !node.material || !node.material.name.match(/\.\d+$/)
+ )
+ .map(([name, node]: [string, any]) => (
+
+
+
+ ))}
+ >
+ );
+}
+
+function ShapePlaceholder({ color }: { color: string }) {
+ return (
+
+
+
+
+ );
+}
+
+export function Item({ object }: { object: ConsoleObject }) {
+ const dataBlock = getProperty(object, "dataBlock").value;
+
+ const [z, y, x] = useMemo(() => getPosition(object), [object]);
+ const [scaleX, scaleY, scaleZ] = useMemo(() => getScale(object), [object]);
+ const q = useMemo(() => getRotation(object, true), [object]);
+
+ const shapeName = dataBlockToShapeName[dataBlock];
+
+ return (
+
+ {shapeName ? (
+ }>
+ }>
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/components/StaticShape.tsx b/src/components/StaticShape.tsx
new file mode 100644
index 00000000..01895097
--- /dev/null
+++ b/src/components/StaticShape.tsx
@@ -0,0 +1,80 @@
+import { Suspense, useMemo } from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import {
+ ConsoleObject,
+ getPosition,
+ getProperty,
+ getRotation,
+ getScale,
+} from "../mission";
+import { shapeToUrl } from "../loaders";
+import { useGLTF } from "@react-three/drei";
+
+const dataBlockToShapeName = {
+ StationInventory: "station_inv_human.dts",
+ SensorLargePulse: "sensor_pulse_large.dts",
+};
+
+/**
+ * Load a .glb file that was converted from a .dts, used for static shapes.
+ */
+function useStaticShape(shapeName: string) {
+ const url = shapeToUrl(shapeName);
+ return useGLTF(url);
+}
+
+function ShapeModel({ shapeName }: { shapeName: string }) {
+ const { nodes } = useStaticShape(shapeName);
+
+ return (
+ <>
+ {Object.entries(nodes)
+ .filter(
+ ([name, node]: [string, any]) =>
+ !node.material || !node.material.name.match(/\.\d+$/)
+ )
+ .map(([name, node]: [string, any]) => (
+
+
+
+ ))}
+ >
+ );
+}
+
+function ShapePlaceholder({ color }: { color: string }) {
+ return (
+
+
+
+
+ );
+}
+
+export function StaticShape({ object }: { object: ConsoleObject }) {
+ const dataBlock = getProperty(object, "dataBlock").value;
+
+ const [z, y, x] = useMemo(() => getPosition(object), [object]);
+ const [scaleX, scaleY, scaleZ] = useMemo(() => getScale(object), [object]);
+ const q = useMemo(() => getRotation(object, true), [object]);
+
+ const shapeName = dataBlockToShapeName[dataBlock];
+
+ return (
+
+ {shapeName ? (
+ }>
+ }>
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/components/TSStatic.tsx b/src/components/TSStatic.tsx
new file mode 100644
index 00000000..f85b1f74
--- /dev/null
+++ b/src/components/TSStatic.tsx
@@ -0,0 +1,56 @@
+import { Suspense, useMemo } from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import {
+ ConsoleObject,
+ getPosition,
+ getProperty,
+ getRotation,
+ getScale,
+} from "../mission";
+import { shapeToUrl } from "../loaders";
+import { useGLTF } from "@react-three/drei";
+
+/**
+ * Load a .glb file that was converted from a .dts, used for static shapes.
+ */
+function useStaticShape(shapeName: string) {
+ const url = shapeToUrl(shapeName);
+ return useGLTF(url);
+}
+
+function ShapeModel({ shapeName }: { shapeName: string }) {
+ const { scene } = useStaticShape(shapeName);
+
+ return ;
+}
+
+function ShapePlaceholder({ color }: { color: string }) {
+ return (
+
+
+
+
+ );
+}
+
+export function TSStatic({ object }: { object: ConsoleObject }) {
+ const shapeName = getProperty(object, "shapeName").value;
+
+ const [z, y, x] = useMemo(() => getPosition(object), [object]);
+ const [scaleX, scaleY, scaleZ] = useMemo(() => getScale(object), [object]);
+ const q = useMemo(() => getRotation(object, true), [object]);
+
+ return (
+
+ }>
+ }>
+
+
+
+
+ );
+}
diff --git a/src/components/renderObject.tsx b/src/components/renderObject.tsx
index 6a57e422..13de1dc1 100644
--- a/src/components/renderObject.tsx
+++ b/src/components/renderObject.tsx
@@ -5,14 +5,20 @@ import { SimGroup } from "./SimGroup";
import { InteriorInstance } from "./InteriorInstance";
import { Sky } from "./Sky";
import { Sun } from "./Sun";
+import { TSStatic } from "./TSStatic";
+import { StaticShape } from "./StaticShape";
+import { Item } from "./Item";
const componentMap = {
- SimGroup,
- TerrainBlock,
- WaterBlock,
InteriorInstance,
+ Item,
+ SimGroup,
Sky,
+ StaticShape,
Sun,
+ TerrainBlock,
+ TSStatic,
+ WaterBlock,
};
export function renderObject(object: ConsoleObject, key: string | number) {
diff --git a/src/loaders.ts b/src/loaders.ts
index eb299f10..8114788c 100644
--- a/src/loaders.ts
+++ b/src/loaders.ts
@@ -29,6 +29,11 @@ export function interiorToUrl(name: string) {
return difUrl.replace(/\.dif$/i, ".gltf");
}
+export function shapeToUrl(name: string) {
+ const difUrl = getUrlForPath(`shapes/${name}`);
+ return difUrl.replace(/\.dts$/i, ".glb");
+}
+
export function terrainTextureToUrl(name: string) {
name = name.replace(/^terrain\./, "");
return getUrlForPath(`textures/terrain/${name}.png`, `${BASE_URL}/black.png`);