Files
tts-workshop/docs/decisions.md
T
hypercross 001d5eeb54 Add 3D object viewers with r3f stack
Add per-class 3D viewers for tiles, tokens, cards, and custom models
using React Three Fiber, drei, and postprocessing. Viewers are
lazy-loaded and registered through the existing viewer registry, with a
shared scene wrapper for lighting, orbit controls, and subtle effects.

Add a CORS-safe /asset proxy route so three.js loaders can fetch
Workshop-hosted textures and models, and extend TTSObject with the
CustomMesh and CustomTile/CustomToken fields the viewers read.
2026-08-08 12:56:46 +08:00

6.6 KiB

Decisions

Scope: The rationale behind key design decisions. For the system's architecture, see architecture.md. For the concrete build plan, see implementation-plan.md.

Each entry records the decision, the context, and the alternatives considered. New entries are appended; existing entries are updated only to correct facts, not to rewrite history.

D1 — Client-only, no caching

Decision: The system runs on the client only and has no caching layer.

Context: The tool is a personal, single-user utility. There is no shared state to protect and no multi-instance deployment.

Alternatives considered: In-memory TTL caching; Redis-backed caching. Both rejected as unnecessary complexity for a single client.

D2 — Separate packages/extract from packages/tts

Decision: Analysis lives in packages/extract; fetching lives in packages/tts. They are separate packages with a clean fetch-vs-analyze boundary.

Context: The user wants to analyze saves and extract objects for a future frontend. Keeping analysis separate lets it be reused independently of the fetcher.

Alternatives considered: Folding analysis into packages/tts. Rejected — couples fetch and analysis and would force the frontend to depend on the fetcher.

D3 — Traversal lives in packages/extract

Decision: traverseMod / markParent move from the fetcher into packages/extract.

Context: Traversal is analysis, not fetching. packages/tts should be fetch-only.

Alternatives considered: Keeping traversal in packages/tts. Rejected — blurs the fetch/analyze boundary.

D4 — packages/extract is isomorphic with zero runtime deps

Decision: packages/extract runs in browser and Node, using only fetch, Blob, and typed arrays. It has no external runtime dependencies.

Context: The frontend (deferred) will consume it directly, so it must be portable. Node 18+ provides global fetch and Blob.

Alternatives considered: Using Buffer and Node-only packages. Rejected — breaks browser use.

D5 — No backend traversal endpoints

Decision: The proxy exposes search and fetch only. Traversal is not exposed as HTTP endpoints.

Context: The frontend will use packages/extract directly. Exposing traversal on the backend would duplicate that logic and add endpoints with no current consumer.

Alternatives considered: Adding /items/:id/objects and similar endpoints. Deferred until a consumer exists.

D6 — Search scrapes the Workshop browse page

Decision: Search is implemented by scraping steamcommunity.com/workshop/browse/?appid=286160 with cheerio.

Context: Steam has no official Web API for searching the Workshop; the API only fetches details for known IDs.

Alternatives considered: None viable — there is no official search API. Accepted risk: scraping is fragile and may break if Steam changes its HTML.

D7 — bson npm package replaces the browser BSON global

Decision: The existing scraper's BSON.deserialize global is replaced with the bson npm package.

Context: The scraper was written for the browser; the proxy runs on Node.

Alternatives considered: Keeping a browser-only global. Rejected — not available in Node.

D8 — Download output type is Blob

Decision: packages/extract's downloadAsset / downloadAll return Blob.

Context: The primary consumer is a frontend that renders assets in <img> / <object> elements.

Alternatives considered: Raw ArrayBuffer. Rejected — less convenient for frontend rendering.

D9 — Frontend stack: React + React Router + Tailwind v4 + Zustand

Decision: The frontend (apps/web) uses React with React Router for routing, Tailwind CSS v4 for styling, and Zustand for state, built with Vite.

Context: The frontend consumes the proxy API for search/fetch and packages/extract directly for analysis. React is the de-facto standard for this kind of tool; React Router provides declarative routes (/ and /mod/:id); Tailwind v4 is the current major version and integrates via @tailwindcss/vite; Zustand is a minimal, unopinionated store that fits the small amount of client state (search + mod).

Alternatives considered: Next.js (heavier than needed for a client-only tool); Redux Toolkit (more boilerplate than warranted); CSS Modules (no utility styling).

D10 — Object inspection via a viewer registry

Decision: The mod page shows a containment-tree sidebar (mirroring how TTS nests objects) and an inspector pane. The inspector resolves a per-class viewer from a registry (registerViewer / resolveViewer), falling back to a DefaultViewer that renders an object's raw fields.

Context: The user wants to visualize objects in a save. A flat list of all objects and assets is hard to navigate, so the UI was reworked around a tree and a selected-object inspector. Custom viewers per object class (Card, Bag, ...) are expected later; a registry keeps that extension point explicit without coupling the page to any one viewer.

Alternatives considered: A single monolithic inspector component. Rejected — would grow unboundedly as per-class viewers are added; the registry keeps each viewer isolated and swappable.

D11 — 3D viewers on the r3f/drei/postprocessing stack

Decision: Per-class 3D viewers for tiles, tokens, cards, and custom models are built on @react-three/fiber, @react-three/drei, and @react-three/postprocessing, registered through the existing viewer registry and lazy-loaded so the three.js stack is code-split out of the main bundle.

Context: The user wants to visualize objects in a save in 3D. The viewer registry (D10) already provides the extension point. Custom models load from CustomMesh.MeshURL (GLTF/OBJ/FBX); tiles/tokens/cards use their CustomImage/CustomDeck textures. Unity asset bundles are out of scope.

Alternatives considered: A single monolithic 3D inspector; a WebGL library other than three.js. Rejected — the registry keeps viewers isolated, and r3f is the de-facto React/three.js integration.

D12 — CORS-safe asset proxy

Decision: The proxy exposes GET /asset?url=..., which fetches an external asset and streams it back with a Content-Type header. Only http(s) URLs are allowed.

Context: Workshop asset hosts (steamusercontent.com, etc.) often omit CORS headers, which would block three.js TextureLoader / GLTFLoader in the browser. Routing asset fetches through the proxy makes them loadable.

Alternatives considered: Loading assets directly in the browser. Rejected — CORS failures on common Workshop hosts would break the viewers.