181 lines
7.8 KiB
Markdown
181 lines
7.8 KiB
Markdown
# Decisions
|
|
|
|
> **Scope:** The rationale behind key design decisions. For the system's
|
|
> architecture, see [`architecture.md`](./architecture.md). For the concrete
|
|
> build plan, see [`implementation-plan.md`](./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.
|
|
|
|
## D13 — Server-side image tracing to a mesh-ready shape
|
|
|
|
**Decision:** The proxy exposes `GET /trace?url=...`, which fetches an image,
|
|
traces it into a vector shape, and returns the result BSON-encoded. Tracing is
|
|
configurable: `mode` (`alpha` default, `bw`, `color`) selects how the region is
|
|
derived, and `format` (`shape` default, `svg`) selects the response. The
|
|
`shape` format returns a parsed `{ outline, holes }` polygon matching
|
|
`@tts/mesh`'s `Shape` interface.
|
|
|
|
**Context:** The user wants to build a mesh from an image (e.g. a token or tile
|
|
art) using `@tts/mesh`. vtracer only returns SVG, but the mesh package consumes
|
|
`{ outline, holes }`. Tracing on the server keeps the SVG-to-shape parsing out
|
|
of the browser and lets the BSON response carry a ready-to-extrude shape.
|
|
`sharp` decodes the image to RGBA so alpha/luminance masks can be built before
|
|
feeding vtracer's `convertPixels`.
|
|
|
|
**Alternatives considered:** Returning only the raw SVG and parsing in the web
|
|
app near `@tts/mesh`; tracing by color only (no alpha). Rejected — server-side
|
|
parsing yields a shape the mesh package can consume directly, and alpha-based
|
|
tracing (the default) is the common case for token/tile art. |