docs: reorganize docs into bgm and status folders
Group the bgm spec cluster under docs/bgm and move dev logs and plans under docs/status, add an overview index, and update cross-references in the docs, README, and source comments.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# bgm Loader — Status
|
||||
|
||||
> WIP. What's built, what works, what's missing, and the known issues.
|
||||
> Spec: [`../bgm/format.md`](../bgm/format.md).
|
||||
|
||||
## What's built
|
||||
|
||||
### `packages/bgm` — the loader core (new)
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `src/types.ts` | Roles (`Package`, `Part`, `Surface`, `Setup`, `Route`, `Stacking`, `SurfaceMount`, …), `SerializedPackage` (the JSON the plugin emits), `BgmError`, `DefFile`, `ParsedDef` |
|
||||
| `src/schemas.ts` | zod schemas + `validate*` for each role |
|
||||
| `src/markdown.ts` | Virtual def files from markdown code blocks, via **`marked`**. `file=` naming + content-hash auto-naming (`./<hash>.yaml`); multiple blocks may share a `file=` name |
|
||||
| `src/parse.ts` | yaml/json/toml → def objects (`yaml`, `smol-toml`); real-file walker (incl. `.csv`) |
|
||||
| `src/variants.ts` | `$variants` expansion via **`typed-csv`** (`typed-csv/csv-loader`). Inline (newline) vs path; paths resolve against the virtual def map |
|
||||
| `src/collect.ts` | `loadDefs` (real + virtual, virtual wins), `collectPackages` (package decl → `include` globs via **picomatch** → parts/surfaces/setups, `type#id` uniqueness, `$variants` on defs and route candidates). Sets each part's `baseUrl` to its source file's directory for resolving relative asset paths |
|
||||
| `src/vite.ts` | The **vite plugin** (`bgm()`): resolves `virtual:bgm/packages` (all packages) and `virtual:bgm/package/<id>` (one package) imports to `export default <json>`, watches source files for reload. Serializes the package's `Map`s to objects (`SerializedPackage`); throws on unknown packages; re-collects per `load` (no stale cache). |
|
||||
| `src/*.test.ts` | **23 tests, all passing** — markdown extractor, typed-csv parsing (incl. the spec's empty-array + crop tuple cases), full harbor collection, plugin unit tests (resolve/load/shape/watch), and a real `vite build` integration test covering two packages |
|
||||
|
||||
Deps: `marked`, `typed-csv`, `yaml`, `smol-toml`, `picomatch`, `zod`, `vite`, `@types/picomatch`.
|
||||
|
||||
### `games/harbor/harbor.md` — example game (new)
|
||||
|
||||
Exercises the format end-to-end: package decl, two `file=parts/tokens.yaml` blocks, a table surface with `mount`/`children` and `candidates: $variants` against a virtual csv block, a child player surface, and a setup declaring its enabled `surfaces`. Same content duplicated as the vitest fixture under `packages/bgm/src/__fixtures__/harbor/`.
|
||||
|
||||
### `games/poker/poker.md` — example game (new)
|
||||
|
||||
A real 52-card deck: a single `card` part expanded by `$variants` into 52 cards, each picking a cell from a `13×4` face sheet (`cards-13x4.jpg`) and a shared `4×1` back sheet (`back-4x1.png`). Assets are stored in Git LFS (`.gitattributes`).
|
||||
|
||||
### `packages/tabletop` — rendering library (new)
|
||||
|
||||
A standalone r3f library that renders bgm parts. `PartView`/`PartMesh` build a mesh from a `Part` definition via `@tts/mesh` (size/fillet, face/back sprite UVs, traced `shape` or rect fallback). Proxy calls (`/asset`, `/trace`) default to `@tts/http` handlers and are overridable via `TabletopProvider`. Plan: [`bgm-tabletop.md`](./bgm-tabletop.md).
|
||||
|
||||
### `packages/http` — shared proxy HTTP (new)
|
||||
|
||||
CORS-safe proxy HTTP helpers shared by the web app and `@tts/tabletop`: `assetUrl`/`resolveAssetUrl` (route external assets through `/asset`) and `traceImage` (image → vector shape via `/trace`, BSON-deserialized). The trace-to-shape geometry conversion lives in `@tts/mesh` (`traceToShape`/`traceToUvBounds`), and `ErrorBoundary` is shared via `@tts/tabletop`.
|
||||
|
||||
### `apps/proxy` — local game assets (new)
|
||||
|
||||
The proxy serves local game assets from the `games` root: `GAMES_ROOT` (env or default) is set at startup (`config.ts`) and read by the `/asset` and `/trace` routes to serve relative paths (e.g. `poker/parts/assets/cards.png`) from disk, alongside the existing http(s) proxy path.
|
||||
|
||||
### `apps/web` — consumer (new)
|
||||
|
||||
- `vite.config.ts` — wired with `bgm({ root: <repo>/games })`, importing the plugin from `@tts/bgm`.
|
||||
- `src/vite-env.d.ts` — ambient `declare module 'virtual:bgm/packages'` (all packages) and `'virtual:bgm/package/*'` (one package).
|
||||
- `src/pages/BgmPage.tsx` — list page: imports `virtual:bgm/packages` and shows every discovered package. Routed at `/bgm`.
|
||||
- `src/pages/BgmPackagePage.tsx` — detail page: looks up a package by id from `bgm` and renders its parts/surfaces/setups. Routed at `/bgm/:id`.
|
||||
|
||||
The vite plugin itself lives in `packages/bgm/src/vite.ts` (exported from `@tts/bgm`), not in the web app — so the loader's plugin is tested in isolation from the web project.
|
||||
|
||||
## Works
|
||||
|
||||
- `pnpm --filter @tts/bgm build` and `typecheck` pass.
|
||||
- Root `pnpm test`: **191 pass**.
|
||||
- `pnpm --filter @tts/web build` succeeds; config warnings fixed.
|
||||
- `src/vite.test.ts` runs a **real `vite build`** against a self-contained fixture (`src/__fixtures__/vite-build/`) and asserts the bundled output contains the package data — the plugin is proven end-to-end without touching the web app.
|
||||
- `pnpm --filter @tts/tabletop build` / `test` pass; `pnpm --filter @tts/proxy typecheck` passes.
|
||||
|
||||
## Known issues
|
||||
|
||||
1. ~~Plugin emits empty maps~~ — fixed: `load` serializes `Map`s via `Object.fromEntries`.
|
||||
2. ~~Plugin `load` uses `this.error`~~ — fixed: throws instead.
|
||||
3. ~~HMR cache not invalidated~~ — fixed: dropped the closure cache; re-collects per `load`.
|
||||
4. ~~`tinyglobby` leftover dep~~ — removed.
|
||||
5. ~~Package-level test script finds no files~~ — added `packages/bgm/vitest.config.ts`.
|
||||
|
||||
## Not yet done
|
||||
|
||||
- ~~No consumer yet~~ — `apps/web/src/pages/BgmPage.tsx` lists packages from `virtual:bgm/packages`; `BgmPackagePage.tsx` shows one at `/bgm/:id`.
|
||||
- **`$variants` URL paths** — spec mentions file/URL; URLs deferred.
|
||||
- **zod `SerializedPackage` shape for the emitted JSON** — the plugin emits `SerializedPackage` objects; a zod schema for the emitted module would give runtime validation beyond the ambient `declare module`.
|
||||
- **`setup` value expansion** — `type` without `id` → all parts of that type is documented but not implemented in the loader (it's a game-state init concern; noted as future).
|
||||
- **Surface mounting is validated but not resolved** — `mount`/`children`/`surfaces` are parsed and validated, but the loader doesn't resolve child→parent relationships or enforce that a setup's `surfaces`/a surface's `children` reference existing surfaces. That's a game-state/rendering concern (see `../bgm/tabletop.md`).
|
||||
- Docs for the loader itself (this file is the start).
|
||||
@@ -0,0 +1,193 @@
|
||||
# bgm-tabletop — Implementation Plan / Status
|
||||
|
||||
> **Scope:** A standalone r3f component library that renders
|
||||
> [bgm](../bgm/format.md) board games: a state store, surface mounting, part
|
||||
> placement with stacking, and per-part meshes. Design:
|
||||
> [`../bgm/tabletop.md`](../bgm/tabletop.md).
|
||||
> **Status:** items 1–8 implemented and the full tabletop scene is wired into
|
||||
> the web app's setup detail route (`/bgm/:id/setups/:type/:setup`). The
|
||||
> part-inspection route renders `PartView` from the library.
|
||||
|
||||
## Goal
|
||||
|
||||
A library (new `packages/tabletop`) that takes a bgm package and renders it as
|
||||
an interactive 3D table: enabled surfaces mounted in world/HUD space, parts
|
||||
placed on their routes, stacked per the format's stacking strategy. The web
|
||||
app's bgm inspector routes are one consumer; the library must not depend on the
|
||||
web app.
|
||||
|
||||
## Stack
|
||||
|
||||
`react`, `react-router` (types only), `tailwind` (styles only), `r3f`
|
||||
(`@react-three/fiber`), `drei`, `postprocessing`, `zustand`, `three`,
|
||||
`@tts/bgm` (types), `@tts/mesh` (geometry).
|
||||
|
||||
## Package layout
|
||||
|
||||
```
|
||||
packages/tabletop/
|
||||
package.json # @tts/tabletop
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts # public exports
|
||||
state.ts # zustand store + derived render state
|
||||
setup.ts # SetupLoader: seed state from a setup
|
||||
mount.ts # resolve surface mount tree (table/hud/child)
|
||||
stacking.ts # useStacking hook
|
||||
placement.ts # PartPlacement
|
||||
partView.tsx # PartView: mesh from a part definition
|
||||
surfaces/
|
||||
WorldSurfaceView.tsx
|
||||
HudSurfaceView.tsx
|
||||
*.test.ts # colocated unit tests
|
||||
```
|
||||
|
||||
## Work items
|
||||
|
||||
### 1. Package scaffold ✅
|
||||
|
||||
- New `packages/tabletop` workspace package (`pnpm-workspace.yaml` already
|
||||
globs `packages/*`).
|
||||
- Deps: `@tts/bgm`, `@tts/mesh`, `three`, `@react-three/fiber`, `@react-three/drei`,
|
||||
`@react-three/postprocessing`, `zustand`. Dev: `vitest`, `typescript`,
|
||||
`@types/three`.
|
||||
- `tsconfig.json` mirroring `packages/bgm`'s (strict, ESM, `dist` output).
|
||||
|
||||
### 2. Part meshes + export + web integration ✅
|
||||
|
||||
First deliverable: `PartView` renders a single part's mesh from its definition,
|
||||
reusing `@tts/mesh` geometry (not the web app's viewers). This is the smallest
|
||||
useful slice and unblocks the web app's part inspection route immediately.
|
||||
|
||||
- `PartView` (`partView.tsx`): creates a mesh from a `Part` definition:
|
||||
- `size` → world dimensions; `fillet` → corner radius.
|
||||
- `face`/`faceCrop`/`back`/`backCrop` → textures (drei `useTexture`), sprite
|
||||
UVs from `faceCrop`/`backCrop` (a `[col,row,cols,rows]` grid cell).
|
||||
- `shape` → traced silhouette (via the proxy `/trace`, like the web token
|
||||
viewer) or a fallback rect/rounded-rect.
|
||||
- `extrudeShapeParts` from `@tts/mesh` for the mesh.
|
||||
- Shared geometry/material caching (module-level `Map`s) so repeated parts
|
||||
reuse buffers, mirroring the web viewers' `sharedResources`.
|
||||
- Export `PartView` from `index.ts`.
|
||||
- **Web integration**: replace the web app's part inspection route
|
||||
(`/bgm/:id/parts/:type/:part`) to render `PartView` from the library, proving
|
||||
it end-to-end.
|
||||
|
||||
### 3. State store (`state.ts`) ✅
|
||||
|
||||
Source-of-truth game state per `../bgm/tabletop.md` §2:
|
||||
|
||||
```ts
|
||||
interface GameState {
|
||||
surfaces: Record<string, boolean>; // enabled per surface id
|
||||
parts: Record<string, PartState>; // part id -> placement state
|
||||
}
|
||||
|
||||
interface PartState {
|
||||
path: string; // the path key this part is on
|
||||
index: number; // the part's position in its path's stack
|
||||
facing: 'face' | 'back' | 'standing'; // how the part is oriented on the board
|
||||
}
|
||||
```
|
||||
|
||||
- A zustand store holding `GameState`.
|
||||
- **Derived render state**: `game state + surface routes => map of piece id to
|
||||
`{ surface, route, candidate, index, stackSize, face }``, per enabled surface.
|
||||
Computed with a selector/memo so the render list is stable. A path's ordered
|
||||
children (for stacking) are derived from the parts map by sorting on `index`.
|
||||
- **Assumption**: each piece id is unique on the board (documented in
|
||||
`../bgm/tabletop.md`); the render map is keyed by piece id.
|
||||
|
||||
### 4. Setup seeding (`setup.ts`) ✅
|
||||
|
||||
- `SetupLoader`: side-effect-only component that seeds the store from a
|
||||
`Setup` — enables its `surfaces` (or all when omitted) and applies its
|
||||
ordered `setup` placements (each moves its `parts` to a `path`).
|
||||
- `setup` value expansion: a bare `type` (no id) expands to all parts of that
|
||||
type (documented in `../bgm/format.md` §3; the loader doesn't do this — it's a
|
||||
game-state init concern, so it lives here).
|
||||
|
||||
### 5. Surface mounting (`mount.ts`) ✅
|
||||
|
||||
- Resolve the surface mount tree from `Surface.mount` + `Surface.children`:
|
||||
- `kind: table` — root, world space.
|
||||
- `kind: hud` — HUD area (`mount.area`).
|
||||
- `kind: child` — mounted relative to a parent that lists it in `children`.
|
||||
- `WorldSurfaceView` / `HudSurfaceView` mount an enabled surface; a disabled
|
||||
surface isn't rendered. Child surfaces mount relative to their parent's
|
||||
anchor (`x`/`y`/`rotation`).
|
||||
|
||||
### 6. Part placement (`placement.ts`) ✅
|
||||
|
||||
- `PartPlacement`: stable per-part component that positions a part on a surface
|
||||
location from the derived render state (route anchor + candidate anchor).
|
||||
- Applies the route's stacking strategy via `useStacking`.
|
||||
|
||||
### 7. Stacking (`stacking.ts`) ✅
|
||||
|
||||
- `useStacking(route.stacking, index, stackSize)` → `{ x, y, rotation, z, tilt }`.
|
||||
- Implements the format's positioning process (`../bgm/format.md` §4): step
|
||||
length from curve length / `max(steps, count-1)`, alignment (`start`/`end`/
|
||||
`center`), and `limit` (`0` all, `n` first n, `-n` last n).
|
||||
- `z` ramps linearly from `zStart` to `zEnd` across the curve's span; `tilt`
|
||||
rotates each shown part about its local Y (long) axis.
|
||||
- Curve length from an SVG path string (small helper; no new dep).
|
||||
|
||||
### 8. Public API (`index.ts`) ✅
|
||||
|
||||
Export `SetupLoader`, `WorldSurfaceView`, `HudSurfaceView`, `PartPlacement`,
|
||||
`PartView`, `useStacking`, and the store hooks. The web app consumes these; the
|
||||
library never imports from `apps/*`.
|
||||
|
||||
## Reuse from `@tts/mesh`
|
||||
|
||||
- `extrudeShapeParts` / `extrudeShape` — front/back/walls geometry.
|
||||
- `rectShape`, `roundedRectShape`, `circleShape`, `polygonShape`, `hexShape`,
|
||||
`frameShape`, `scaleShape` — shape generators for parts without a `shape`
|
||||
sprite.
|
||||
- `shapeFromThree` — author shapes with the three.js path API.
|
||||
- `ExtrudedGeometry` / `UVBounds` — raw typed arrays + UV framing.
|
||||
|
||||
The web viewers (`TokenViewer`/`CardViewer`) contain logic we'll mirror rather
|
||||
than import: trace-to-shape conversion, sprite UV math, texture flipping. These
|
||||
are candidates to lift into `@tts/mesh` or `@tts/tabletop` later so both
|
||||
consumers share them (see Open decisions).
|
||||
|
||||
## Testing
|
||||
|
||||
- `state.ts` — derived render state: enabled surfaces, route matching,
|
||||
candidate selection, stacking index/stackSize.
|
||||
- `stacking.ts` — positioning process: step length, alignment, limit, z ramp,
|
||||
tilt.
|
||||
- `setup.ts` — seeding + bare-type expansion.
|
||||
- `mount.ts` — mount tree resolution (table/hud/child, children refs).
|
||||
- `partView.tsx` — geometry from a part def (size/fillet/crop), sprite UVs.
|
||||
- A real `vite build` integration test (mirroring `packages/bgm/src/vite.test.ts`)
|
||||
proving the library bundles against a fixture package.
|
||||
|
||||
## Validation
|
||||
|
||||
- `pnpm --filter @tts/tabletop build` / `typecheck` / `test`.
|
||||
- Root `pnpm test` stays green.
|
||||
- `pnpm --filter @tts/web build` — the part inspection route renders `PartView`
|
||||
from the library (work item 2), proving it end-to-end.
|
||||
|
||||
## Commands (not yet implemented)
|
||||
|
||||
Scripted interaction is designed in [`../bgm/commands.md`](../bgm/commands.md):
|
||||
async commands with `ok`/`cancel`/`error` results, per-invocation run
|
||||
contexts, fire-and-forget vs self-managed waiting, and tap interaction with
|
||||
part-local trigger points. Implementation order: types + run-context manager,
|
||||
tap detection, then the first commands (`wait: tap`, `focus`).
|
||||
|
||||
## Open decisions (defaults in bold)
|
||||
|
||||
- **Where the trace/sprite helpers live** — **lift into `@tts/mesh`** (shared
|
||||
by web viewers + tabletop) vs duplicate in `@tts/tabletop`. Lifting is
|
||||
cleaner but touches the web viewers; decide when `PartView` (work item 2)
|
||||
needs them.
|
||||
- **HUD rendering** — **drei `Html`/orthographic overlay** vs a second
|
||||
`Canvas`. Default to an overlay so world + HUD share one scene.
|
||||
- **Curve length** — **small internal SVG-path length helper** vs a dependency
|
||||
(e.g. `svg-path-properties`). Prefer the helper to avoid a dep.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Full Setup View — Plan
|
||||
|
||||
> **Scope:** A new page that renders every loadable object in a save into a
|
||||
> single shared 3D scene, sharing materials and geometry where possible. This
|
||||
> complements the existing per-object inspection on the mod page.
|
||||
|
||||
## Motivation
|
||||
|
||||
The mod page (`apps/web/src/pages/ModPage.tsx`) inspects one object at a time
|
||||
inside a shared `Scene`. A "full setup" view renders the whole save at once —
|
||||
every tile, token, card, and custom model laid out on a virtual table — so a
|
||||
user can see the entire scene at a glance.
|
||||
|
||||
## Current architecture (relevant pieces)
|
||||
|
||||
- Each viewer owns its own `<Scene>` wrapper (`components/viewers/Scene.tsx`),
|
||||
which provides the camera, lights, orbit controls, contact shadows, and
|
||||
post-processing. The mesh content lives inside each viewer:
|
||||
- `TileViewer` → `<TileMesh>` (extruded tile, textured top face)
|
||||
- `TokenViewer` → `<TokenMesh>` (alpha-traced extruded token)
|
||||
- `CardViewer` → `<CardMesh>` (rounded-rect card, sprite UVs)
|
||||
- `CustomModelViewer` → `<Model>` (GLTF/OBJ/FBX via `FlexibleModelLoader`)
|
||||
- `@tts/extract` provides `flattenObjects(mod)` / `traverseMod` to enumerate
|
||||
every object in a save.
|
||||
- `@tts/mesh` provides the shape + extrusion helpers used by the viewers.
|
||||
- Every object in a save carries a `Transform` (position/rotation/scale in TTS
|
||||
world units), which the proxy passes through in the raw BSON. The shared
|
||||
type now models it as `TTSObjectTransform`.
|
||||
|
||||
## Plan
|
||||
|
||||
### 1. Refactor viewers to expose their mesh content (no `Scene`)
|
||||
|
||||
Each viewer currently wraps its mesh in `<Scene>`. To compose everything into
|
||||
one scene, extract and export the inner mesh components, keeping the existing
|
||||
viewers as thin wrappers:
|
||||
|
||||
- `TileViewer` → export `TileObjectMesh` (accepts a `TTSObject`)
|
||||
- `TokenViewer` → export `TokenObjectMesh`
|
||||
- `CardViewer` → export `CardObjectMesh`
|
||||
- `CustomModelViewer` → export `CustomModelMesh` (the `<Model>` + fallback box)
|
||||
|
||||
Each viewer now renders `<Scene><XxxObjectMesh object={…} /></Scene>`, so the
|
||||
object-facing wrapper is the single source of truth for both the per-object
|
||||
view and the full-setup view.
|
||||
|
||||
### 2. New page `FullSetupPage.tsx` at `/mod/:id/setup`
|
||||
|
||||
- Reuse `useModStore` (same load path as `ModPage`).
|
||||
- `flattenObjects(mod)` → filter to renderable classes (those registered in
|
||||
`components/viewers/register.ts`: `Tile`, `Custom_Tile`, `Custom_Token`,
|
||||
`Card`, `CardCustom`, `Deck`, `DeckCustom`, `Custom_Deck`, `Custom_Model*`).
|
||||
- Render **one** `<Scene>` containing all renderable objects as
|
||||
`<group position={…}>` entries, dispatching to the right `*Mesh` by `Name`.
|
||||
- Show a summary header (total objects, rendered count, skipped count) and
|
||||
loading/error states matching `ModPage`.
|
||||
|
||||
### 3. Layout
|
||||
|
||||
- Place each object at its real position from the save's `Transform` instead of
|
||||
a generated grid.
|
||||
- `components/viewers/transform.ts` converts a TTS transform to three.js:
|
||||
TTS is left-handed (Y up, +Z toward the player), three.js is right-handed,
|
||||
so Z is reflected in position and rotation. Each class also gets a base-size
|
||||
correction (our viewer meshes are authored for inspection, in arbitrary
|
||||
units) and a "lay flat" rotation for the extruded tile/token/card meshes.
|
||||
- `Scene`'s `Bounds fit` auto-fits the camera to the full layout, so no camera
|
||||
work is needed.
|
||||
- Non-renderable objects (bags, dice, boards without assets) are skipped and
|
||||
counted, not dropped silently.
|
||||
|
||||
### 4. Share materials & geometry
|
||||
|
||||
- **Geometry cache (module-level `Map<string, BufferGeometry>`):** key by a
|
||||
canonical string — `card:{w}:{h}:{t}`, `tile:{type}:{aspect}:{t}`,
|
||||
`token:{url}:{t}`, `model:{meshUrl}`. All cards of the same size, or tiles
|
||||
of the same type/size, reuse one geometry instead of rebuilding per object.
|
||||
Token geometry is keyed by the source image URL (the trace is cached per
|
||||
URL, so the silhouette is deterministic).
|
||||
- **Material cache (module-level `Map<string, Material>`):** key by
|
||||
`textureUrl + color + roughness`. drei already caches textures by URL
|
||||
globally, so sharing the material on top avoids per-object material
|
||||
allocation for tiles/tokens with the same image.
|
||||
- **Cards:** the face/back textures are shared (drei caches them by URL) and
|
||||
the sprite cell is selected via a per-material UV transform injected into the
|
||||
shader (`cardMaterial.ts`), so cards share texture, shader, and geometry —
|
||||
only the material uniforms differ. Materials are cached per card id + tint.
|
||||
- Dispose shared resources on page unmount, or accept a module-level cache for
|
||||
the session (see Open decisions).
|
||||
|
||||
### 5. Routing & navigation
|
||||
|
||||
- Add `<Route path="/mod/:id/setup" element={<FullSetupPage />} />` in
|
||||
`apps/web/src/App.tsx`.
|
||||
- Add a "Full setup" link/button on `ModPage` next to the download button.
|
||||
|
||||
### 6. Edge cases
|
||||
|
||||
- Objects with no asset (no `ImageURL`/`MeshURL`) render as neutral-colored
|
||||
placeholders (matching the current viewers' fallback behavior).
|
||||
- Token tracing suspends per URL (already cached in `TokenViewer`); the shared
|
||||
`Scene` Suspense boundary handles it.
|
||||
- Large saves: the grid + shared geometry keeps it performant, but cap or warn
|
||||
on very large object counts if needed.
|
||||
|
||||
## Files touched
|
||||
|
||||
- `apps/web/src/components/viewers/{Tile,Token,Card,CustomModel}Viewer.tsx` —
|
||||
export object-facing mesh wrappers
|
||||
- `apps/web/src/components/viewers/sharedResources.ts` — new geometry/material
|
||||
caches
|
||||
- `apps/web/src/components/viewers/transform.ts` — new TTS→three.js placement
|
||||
conversion
|
||||
- `apps/web/src/components/viewers/sharedResources.ts` — `objectTint`/
|
||||
`tintedColor` helpers for the per-object `ColorDiffuse` tint
|
||||
- `packages/shared/src/types.ts` — add `TTSObjectTransform` and `ColorDiffuse`
|
||||
- `apps/web/src/pages/FullSetupPage.tsx` — new
|
||||
- `apps/web/src/App.tsx` — route
|
||||
- `apps/web/src/pages/ModPage.tsx` — nav link
|
||||
- Possibly a small `geometryCache`/`materialCache` helper under
|
||||
`components/viewers/`
|
||||
|
||||
## Open decisions (defaults in bold)
|
||||
|
||||
- **Layout style** — **real `Transform` placement** (was a grouped grid before
|
||||
the transform data was wired in).
|
||||
- **Geometry/material cache lifetime** — **session-level module cache**
|
||||
(simplest, consistent with drei's global texture cache) vs dispose-on-unmount.
|
||||
@@ -0,0 +1,415 @@
|
||||
# TTS Workshop — Implementation Plan
|
||||
|
||||
> **Scope:** The concrete build plan — files, endpoints, dependencies, build
|
||||
> order. For the system's architecture and dependency graph, see
|
||||
> [`../architecture.md`](../architecture.md). For the rationale behind key decisions,
|
||||
> see [`../decisions.md`](../decisions.md).
|
||||
|
||||
A lightweight, client-only pnpm monorepo for searching the Tabletop Simulator
|
||||
Steam Workshop, fetching full TTS save files, and analyzing their contents.
|
||||
|
||||
## Goals
|
||||
|
||||
- Search the TTS Workshop to discover item IDs (Steam has no official search API,
|
||||
so this scrapes the browse page).
|
||||
- Fetch a full TTS save (`TTSMod`) for a given item ID via the Steam Web API +
|
||||
BSON deserialization.
|
||||
- Extract and inspect objects and asset references from a save, in an isomorphic
|
||||
package reusable from a future frontend.
|
||||
- Client-only, lightweight, no caching layer.
|
||||
|
||||
## Non-goals (for now)
|
||||
|
||||
- Backend traversal endpoints (deferred by design).
|
||||
- Caching / Redis / multi-instance concerns.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
apps/web ──► apps/proxy ──► packages/tts ──► packages/shared
|
||||
│ │ │
|
||||
│ │ └──► (fetchMod → TTSMod)
|
||||
│ ▼
|
||||
├──► packages/mesh ──► packages/shared (types)
|
||||
└──► packages/extract ──► packages/shared (types)
|
||||
(isomorphic, used by the frontend)
|
||||
```
|
||||
|
||||
- **`apps/web`** — React frontend (search + mod pages).
|
||||
- **`apps/proxy`** — Hono server. Search + fetch only. No traversal endpoints.
|
||||
- **`packages/tts`** — low-level fetcher: Steam API call, BSON parse, filename
|
||||
derivation. Returns raw parsed `TTSMod`.
|
||||
- **`packages/extract`** — analysis: flatten/filter objects, extract asset refs,
|
||||
download assets. Isomorphic (browser + Node).
|
||||
- **`packages/mesh`** — 2D shapes + extrusion into 3D geometry for the
|
||||
frontend viewers. Isomorphic (browser + Node).
|
||||
- **`packages/shared`** — shared types + zod schemas.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
tts-workshop/
|
||||
├── pnpm-workspace.yaml
|
||||
├── package.json # root scripts (dev, build, lint)
|
||||
├── tsconfig.base.json
|
||||
├── .npmrc
|
||||
├── .env.example # STEAM_API_KEY, PORT
|
||||
├── docs/
|
||||
│ ├── overview.md
|
||||
│ ├── architecture.md
|
||||
│ ├── decisions.md
|
||||
│ ├── bgm/
|
||||
│ │ ├── format.md
|
||||
│ │ ├── engine.md
|
||||
│ │ ├── commands.md
|
||||
│ │ └── tabletop.md
|
||||
│ └── status/
|
||||
│ └── implementation-plan.md # this file
|
||||
├── apps/
|
||||
│ ├── proxy/
|
||||
│ │ ├── package.json
|
||||
│ │ ├── tsconfig.json
|
||||
│ │ └── src/
|
||||
│ │ ├── index.ts # Hono app + @hono/node-server
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── search.ts # GET /search?q=...&page=1
|
||||
│ │ │ ├── items.ts # GET /items/:id, /items/:id/file
|
||||
│ │ │ ├── asset.ts # GET /asset?url=... (CORS-safe asset proxy)
|
||||
│ │ │ ├── trace.ts # GET /trace?url=... (image → vector shape)
|
||||
│ │ │ ├── svgShape.ts # parse vtracer SVG into { outline, holes }
|
||||
│ │ │ └── health.ts # GET /health
|
||||
│ │ └── env.ts # zod env validation
|
||||
│ └── web/
|
||||
│ ├── package.json
|
||||
│ ├── tsconfig.json
|
||||
│ ├── vite.config.ts # dev proxy → localhost:3000
|
||||
│ ├── index.html
|
||||
│ └── src/
|
||||
│ ├── main.tsx # React root + router
|
||||
│ ├── App.tsx # layout + routes
|
||||
│ ├── api.ts # fetch wrappers for /search, /items
|
||||
│ ├── index.css # tailwind v4 entry
|
||||
│ ├── pages/
|
||||
│ │ ├── SearchPage.tsx
|
||||
│ │ └── ModPage.tsx
|
||||
│ ├── components/
|
||||
│ │ ├── SearchResults.tsx
|
||||
│ │ ├── ObjectTree.tsx # containment-tree sidebar
|
||||
│ │ ├── viewers.tsx # viewer registry + default viewer
|
||||
│ │ ├── viewers/ # 3D viewers (r3f/drei/postprocessing)
|
||||
│ │ │ ├── register.ts # registers per-class 3D viewers (lazy)
|
||||
│ │ │ ├── Scene.tsx # shared Canvas: lights, controls, post
|
||||
│ │ │ ├── TileViewer.tsx
|
||||
│ │ │ ├── TokenViewer.tsx
|
||||
│ │ │ ├── CardViewer.tsx
|
||||
│ │ │ ├── CustomModelViewer.tsx
|
||||
│ │ │ └── assetUrl.ts # route asset URLs through the proxy
|
||||
│ │ ├── objectIcons.tsx # class → icon mapping
|
||||
│ │ ├── objectIconsData.ts # generated icon subset (do not edit)
|
||||
│ │ └── objectIcons.test.ts
|
||||
│ └── stores/
|
||||
│ ├── searchStore.ts # zustand
|
||||
│ └── modStore.ts # zustand
|
||||
└── packages/
|
||||
├── shared/
|
||||
│ └── src/
|
||||
│ ├── types.ts # TTSMod, TTSObject, WorkshopItem, SearchResult
|
||||
│ └── schemas.ts
|
||||
├── tts/
|
||||
│ └── src/
|
||||
│ ├── index.ts # fetchMod, getFileName
|
||||
│ └── errors.ts
|
||||
├── mesh/
|
||||
│ └── src/
|
||||
│ ├── index.ts # public API barrel
|
||||
│ ├── types.ts # FaceGeometry, ExtrudedGeometry, UVBounds
|
||||
│ ├── shapes.ts # Shape + shape generators (rect, circle, ...)
|
||||
│ ├── tessellate.ts # triangulate + cap faces
|
||||
│ ├── walls.ts # side walls
|
||||
│ └── extrude.ts # extrudeShape / extrudeShapeParts
|
||||
└── extract/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
├── index.ts # public API barrel
|
||||
├── objects.ts # flatten/filter/traverse helpers
|
||||
├── refs.ts # extract asset references
|
||||
├── download.ts # fetch + decode referenced assets
|
||||
└── types.ts # extracted-object / asset types
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
### `packages/shared`
|
||||
|
||||
Shared types and validation schemas used across the monorepo.
|
||||
|
||||
- `types.ts`
|
||||
- `TTSMod`, `TTSObject` (from the existing scraper code)
|
||||
- `WorkshopItem` — metadata from the Steam API (`id`, `title`, `author`,
|
||||
`previewImageUrl`, `fileUrl`, ...)
|
||||
- `SearchResult` — `{ items: WorkshopItem[], page, hasMore }`
|
||||
- `schemas.ts`
|
||||
- zod schemas for env vars, query params, and response shapes.
|
||||
|
||||
### `packages/tts`
|
||||
|
||||
Low-level fetcher, extracted from the existing scraper.
|
||||
|
||||
- `index.ts`
|
||||
- `fetchMod(id: string): Promise<TTSMod>` — Steam API call to
|
||||
`ISteamRemoteStorage/GetPublishedFileDetails/v1` to get `file_url`, then
|
||||
download + BSON-deserialize into `TTSMod`.
|
||||
- `fetchModFromUrl(fileUrl)` — download + BSON-deserialize from a direct URL
|
||||
(no Steam API call).
|
||||
- `fetchModFile(id, apiKey)` / `fetchModFileFromUrl(fileUrl)` — raw save
|
||||
bytes + derived filename, with and without the Steam API.
|
||||
- `getFileName(url: string): string` — derive a filename from the save URL
|
||||
path (the upstream `content-disposition` header is ignored).
|
||||
- `errors.ts`
|
||||
- Typed errors: missing `file_url`, Steam API failure, rate limit, invalid key.
|
||||
- Notes
|
||||
- Swap the browser `BSON` global for the `bson` npm package.
|
||||
- `traverseMod` / `markParent` move to `packages/extract` (traversal is
|
||||
analysis, not fetching).
|
||||
|
||||
### `packages/extract`
|
||||
|
||||
Isomorphic analysis of a parsed `TTSMod`. No Node-specific APIs.
|
||||
|
||||
- `objects.ts`
|
||||
- `flattenObjects(mod)` — all objects in the tree.
|
||||
- `filterObjects(mod, predicate)` — filter by name, GUID, type, etc.
|
||||
- `findObject(mod, guid)` — lookup by GUID.
|
||||
- `buildTree(mod)` — containment tree (`{ object, label, children }`),
|
||||
mirroring how TTS nests objects; `label` is `Nickname` when present,
|
||||
else the class `Name`.
|
||||
- `traverseMod` / `markParent` (moved from `tts`).
|
||||
- Returns lightweight graph shapes: `{ guid, name, type, parentGuid,
|
||||
childrenGuids, refs }`.
|
||||
- `refs.ts`
|
||||
- `extractRefs(object)` — walk one object, pull every external URL:
|
||||
`CustomPDF.PDFUrl`, `CustomDeck[*].FaceURL`/`BackURL`,
|
||||
`CustomImage.ImageURL`/`ImageSecondaryURL`.
|
||||
- `collectRefs(mod)` — all refs across the save, deduped by URL.
|
||||
- `AssetRef` type: `{ kind, url, ownerGuid }`.
|
||||
- `download.ts`
|
||||
- `downloadAsset(url)` — `fetch` → `Blob`.
|
||||
- `downloadAll(refs, { concurrency, onProgress })` — batched downloads with
|
||||
progress callback.
|
||||
- `guessMimeType(url)` — infer mime from extension.
|
||||
- Design constraints
|
||||
- No `Buffer` — use `ArrayBuffer` / `Uint8Array` / `Blob` (Node 18+).
|
||||
- No Node-only packages (`cheerio` stays in the backend search only).
|
||||
- Pure, deterministic functions where possible.
|
||||
|
||||
### `packages/mesh`
|
||||
|
||||
2D shape + extrusion library used by the frontend viewers to build 3D geometry.
|
||||
|
||||
- `shapes.ts`
|
||||
- `Shape` — `{ outline, holes }`, the minimal interface the tessellator and
|
||||
wall generator need. Shape generators: `rectShape`, `polygonShape`,
|
||||
`hexShape`, `circleShape`, `roundedRectShape`, `frameShape`, plus
|
||||
`scaleShape` and `signedArea`.
|
||||
- `tessellate.ts`
|
||||
- `triangulate(shape)` — earcut triangulation (same as three.js).
|
||||
- `frontFaces(shape, height, uvScale, uvBounds)` — top face, normal +Z.
|
||||
- `backFaces(shape, height, uvScale, uvBounds)` — bottom face, normal -Z.
|
||||
UVs map the shape's bounding box (or `uvBounds` framing) to the unit
|
||||
square; the back uses the same planar xy mapping as the front (no mirror).
|
||||
- `capFaces(...)` — front + back merged into one geometry (front vertices
|
||||
first, then back).
|
||||
- `walls.ts`
|
||||
- `wallFaces(shape, height, uvScale, uvBounds)` — side walls with outward
|
||||
normals and planar xy UVs (z-independent).
|
||||
- `extrude.ts`
|
||||
- `extrudeShape(shape, options)` — merged front + back + walls as one
|
||||
geometry.
|
||||
- `extrudeShapeParts(shape, options)` — `{ front, back, walls }` as separate
|
||||
geometries, so each face can carry its own material.
|
||||
- `ExtrudeOptions` — `height`, `capUvScale`, `wallUvScale`, `uvBounds`.
|
||||
- `types.ts`
|
||||
- `FaceGeometry`, `ExtrudedGeometry`, `UVBounds`.
|
||||
|
||||
### `apps/proxy`
|
||||
|
||||
Hono server exposing search + fetch.
|
||||
|
||||
- `index.ts` — Hono app + `@hono/node-server` bootstrap, CORS via `@hono/cors`.
|
||||
- `routes/search.ts`
|
||||
- `GET /search?q=...&page=1` — scrape
|
||||
`steamcommunity.com/workshop/browse/?appid=286160&searchtext=...` with
|
||||
`cheerio`, extract `{ id, title, author, previewImageUrl }` from the result
|
||||
grid. Supports `pagenum` pagination. No API key required.
|
||||
- `routes/items.ts`
|
||||
- `GET /items/:id` — full parsed `TTSMod`. Accepts an optional `fileUrl`
|
||||
query param to download the save directly (no Steam API key needed);
|
||||
otherwise resolves via the Steam API.
|
||||
- `GET /items/:id/file` — raw save bytes, filename from `getFileName` (the
|
||||
URL path). Also accepts `fileUrl`.
|
||||
- `routes/asset.ts`
|
||||
- `GET /asset?url=...` — fetch an external asset (texture, model) and stream
|
||||
it back with a `Content-Type` header. Workshop hosts often omit CORS
|
||||
headers, which would block three.js loaders in the browser; routing through
|
||||
the proxy makes those assets loadable. Only `http(s)` URLs are allowed.
|
||||
- `routes/trace.ts`
|
||||
- `GET /trace?url=...&mode=alpha&threshold=128&format=shape&offset=...` —
|
||||
fetch an image, trace it into a vector shape, and return the result
|
||||
BSON-encoded. `mode` is `alpha` (default), `bw`, or `color`; `format` is
|
||||
`shape` (default) or `svg`; `offset` (optional) insets (negative) or
|
||||
outsets (positive) the shape in pixels. `shape` returns a parsed
|
||||
`{ outline, holes }` polygon matching `@tts/mesh`'s `Shape` interface,
|
||||
ready to extrude.
|
||||
- `routes/svgShape.ts`
|
||||
- `parseSvgShape(svg)` — parse a vtracer SVG into a `TracedShape`
|
||||
(`{ outline, holes }`): flatten beziers to polylines, split subpaths into
|
||||
rings, classify by winding (CCW outline / CW hole), and assign holes to
|
||||
their containing outline.
|
||||
- `offsetShape(shape, delta)` — inset/outset a `TracedShape` via
|
||||
`clipper-lib` (Clipper miter joins); outline and holes offset in opposite
|
||||
directions and are recombined with a boolean difference, so holes grow on
|
||||
inset and shrink on outset. Collapsed shapes return an empty outline; a
|
||||
split outline keeps the largest ring.
|
||||
- `routes/health.ts`
|
||||
- `GET /health` — liveness.
|
||||
- `env.ts` — zod validation of `STEAM_API_KEY`, `PORT`.
|
||||
|
||||
### `apps/web`
|
||||
|
||||
React frontend (Vite + React Router + Tailwind v4 + Zustand). Consumes the
|
||||
proxy API and `packages/extract` directly for analysis.
|
||||
|
||||
- `main.tsx` — React root, `BrowserRouter`.
|
||||
- `App.tsx` — app shell (header) + routes: `/` (search), `/mod/:id` (mod).
|
||||
- `api.ts` — typed `fetch` wrappers for `/search` and `/items/:id`, plus a
|
||||
`modFileUrl` helper for the raw-file download.
|
||||
- `pages/SearchPage.tsx` — search form, drives `searchStore`.
|
||||
- `pages/ModPage.tsx` — loads the mod via `modStore`, uses `@tts/extract`
|
||||
(`buildTree`, `collectRefs`) to render a containment-tree sidebar and an
|
||||
inspector pane for the selected object, and links to the raw save file.
|
||||
- `components/SearchResults.tsx` — result grid + pagination.
|
||||
- `components/ObjectTree.tsx` — recursive tree sidebar; each entry shows a
|
||||
class icon (hover for the class name) + display label, indented by depth.
|
||||
Clicking selects an object by its unique index path (not GUID — cards in a
|
||||
deck share the deck's GUID).
|
||||
- `components/viewers.tsx` — viewer registry (`registerViewer` /
|
||||
`resolveViewer`) plus a `DefaultViewer` that renders an object's fields;
|
||||
custom per-class viewers can be registered later.
|
||||
- `components/viewers/` — 3D viewers built on `@react-three/fiber`,
|
||||
`@react-three/drei`, and `@react-three/postprocessing`. `register.ts`
|
||||
registers lazy-loaded viewers for `Tile`/`Custom_Tile` (flat box),
|
||||
`Custom_Token` (shape traced from the image's alpha channel via `/trace`,
|
||||
extruded with `@tts/mesh`), `Card`/`CardCustom`/`Deck`/`DeckCustom`/
|
||||
`Custom_Deck` (thin rounded rect with face/back textures), and
|
||||
`Custom_Model`/`Custom_Model_Bag`/`Custom_Model_Infinite_Bag` (GLTF/OBJ/FBX
|
||||
from `CustomMesh.MeshURL`). Viewers build `{ front, back, walls }` geometry
|
||||
via `extrudeShapeParts`; back faces are flipped left/right on the material
|
||||
(`flipTexture.ts`) so they aren't mirrored, and card faces slice the deck
|
||||
sprite sheet via `CardID` (`cardResolution.ts`). `Scene.tsx` is a shared
|
||||
canvas with lighting, orbit controls, contact shadows, and subtle
|
||||
bloom/vignette; it fits the camera to the object's bounds via drei's
|
||||
`Bounds` inside the Suspense boundary, so it frames the loaded content.
|
||||
`assetUrl.ts` routes asset URLs through the proxy for CORS-safe loading.
|
||||
The viewers are lazy-loaded so the three.js stack is code-split out of the
|
||||
main bundle.
|
||||
- `components/objectIcons.tsx` — maps TTS object classes to one or more
|
||||
Iconify icons (`iconsForObject`); unknown classes fall back to a help icon.
|
||||
Icons may come from multiple sets (mdi, material-symbols, file-icons, ...);
|
||||
names not bundled are fetched from the Iconify CDN at runtime.
|
||||
- `components/objectIconsData.ts` — generated by
|
||||
`scripts/generate-object-icons.mjs`; registers a small mdi subset with
|
||||
Iconify's offline storage so those render without a network round-trip.
|
||||
- `stores/searchStore.ts` / `stores/modStore.ts` — Zustand stores for search
|
||||
and mod state.
|
||||
- `vite.config.ts` — dev proxy for `/search`, `/items`, `/health` →
|
||||
`http://localhost:3000`.
|
||||
- Styling: Tailwind v4 via `@tailwindcss/vite`; `index.css` imports
|
||||
`tailwindcss`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
| ------ | ------------------- | -------------------------------------------- | ---- |
|
||||
| GET | `/health` | Liveness | — |
|
||||
| GET | `/search?q=&page=` | Scrape Workshop browse, return item list | — |
|
||||
| GET | `/items/:id` | Full parsed `TTSMod` (`?fileUrl=` skips key) | key* |
|
||||
| GET | `/items/:id/file` | Raw save bytes, filename from URL path | key* |
|
||||
| GET | `/asset?url=` | CORS-safe proxy for external assets | — |
|
||||
| GET | `/trace?url=&mode=&format=&offset=` | Trace an image into a vector shape (BSON) | — |
|
||||
|
||||
\* `STEAM_API_KEY` is optional; `/items/*` works without it when a `fileUrl`
|
||||
query param is supplied.
|
||||
|
||||
## Data flow
|
||||
|
||||
```
|
||||
/search?q=wingspan → scrape browse page → list of {id, title, fileUrl, ...}
|
||||
↓
|
||||
/items/:id → Steam API (needs key) → file_url
|
||||
↓
|
||||
└─ /items/:id?fileUrl=... → download directly (no key)
|
||||
↓
|
||||
download BSON → parse → TTSMod
|
||||
↓
|
||||
packages/extract → flatten objects / extract refs / download assets
|
||||
↓
|
||||
/trace?url=... → sharp decode → vtracer → parse SVG → BSON shape
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `hono`, `@hono/node-server`, `@hono/cors` — server.
|
||||
- `bson` — BSON deserialization (save files + trace results).
|
||||
- `@visioncortex/vtracer` — raster-to-SVG vectorization (wasm).
|
||||
- `sharp` — image decoding to RGBA for tracing.
|
||||
- `svgpath` — SVG path parsing for traced shapes.
|
||||
- `cheerio` — Workshop browse page scraping (backend only).
|
||||
- `zod` — validation.
|
||||
- `react`, `react-dom`, `react-router-dom`, `zustand` — frontend.
|
||||
- `three`, `@react-three/fiber`, `@react-three/drei`,
|
||||
`@react-three/postprocessing` — 3D object viewers.
|
||||
- `@iconify/react`, `@iconify-json/mdi`, `@iconify/utils` — iconify icons for
|
||||
object class tags (subset bundled via `scripts/generate-object-icons.mjs`).
|
||||
- `vite`, `@vitejs/plugin-react`, `tailwindcss`, `@tailwindcss/vite` — frontend tooling.
|
||||
- `tsx`, `typescript`, `eslint`, `prettier` — tooling.
|
||||
|
||||
## Tooling
|
||||
|
||||
- TypeScript strict mode.
|
||||
- `tsx` for dev, `tsc` for build; `vite` for the frontend dev server/build.
|
||||
- `vitest` for unit tests, colocated as `*.test.ts` next to sources.
|
||||
- Root scripts: `pnpm dev`, `pnpm dev:web`, `pnpm build`, `pnpm test`, `pnpm lint`.
|
||||
|
||||
## Build order
|
||||
|
||||
1. Scaffold workspace (`pnpm-workspace.yaml`, root `package.json`, base tsconfig,
|
||||
`.npmrc`).
|
||||
2. `packages/shared` — types + zod schemas.
|
||||
3. `packages/tts` — fetch + parse (existing scraper code).
|
||||
4. `packages/extract` — objects, refs, download.
|
||||
5. `apps/proxy` — search + items + health routes, env validation.
|
||||
6. `apps/web` — React frontend (search + mod pages), consuming the proxy and
|
||||
`packages/extract`.
|
||||
7. Wire up root scripts, `.env.example`, README.
|
||||
|
||||
## Risks / caveats
|
||||
|
||||
- **Search scraping is fragile** — Steam can change their HTML or rate-limit.
|
||||
No official search API exists, so this is the standard approach.
|
||||
- **Members-only items** won't appear in scraped search results.
|
||||
- **`file_url` may be missing/expired** — handle cleanly with a 404-style error.
|
||||
- **BSON parsing is the expensive part** — large saves; no caching by design
|
||||
(client-only).
|
||||
- **`STEAM_API_KEY` is exposed in the client process** — acceptable for a
|
||||
personal tool; the key is only needed for the metadata call that yields
|
||||
`file_url`.
|
||||
|
||||
## Open decisions (defaults in bold)
|
||||
|
||||
- **`packages/extract` vs folding into `packages/tts`** — **separate package**
|
||||
(clean fetch vs analyze boundary).
|
||||
- **Download output type** — **`Blob`** (easier for `<img>`/`<object>` in a
|
||||
frontend) vs raw `ArrayBuffer`.
|
||||
- **Move `traverseMod`/`markParent` into `extract`** — **yes** (traversal is
|
||||
analysis, not fetching).
|
||||
Reference in New Issue
Block a user