docs: update mesh, viewers, and selection decisions

Document the separated front/back/walls extrusion, material-level back
flips, CardID sprite selection from the parent deck, index-path tree
selection, and the Bounds-inside-Suspense camera fit.
This commit is contained in:
2026-08-08 18:18:22 +08:00
parent ead2ba8c57
commit 12418898d0
3 changed files with 105 additions and 18 deletions
+3 -3
View File
@@ -52,8 +52,8 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared
(image → vector shape) over HTTP.
- **`apps/web``packages/extract`** — uses `buildTree` / `collectRefs`
to analyze a loaded `TTSMod` in the browser (tree sidebar + asset refs).
- **`apps/web``packages/mesh`** — extrudes 2D shapes into 3D geometry for
the tile and token viewers.
- **`apps/web``packages/mesh`** — extrudes 2D shapes into 3D geometry
(`{ front, back, walls }`) for the tile, token, and card viewers.
- **`apps/proxy``packages/tts`** — calls `fetchMod` / `getFileName` to serve
item requests.
- **`apps/proxy``packages/shared`** — uses shared types and zod schemas for
@@ -93,7 +93,7 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared
| `zod` | Runtime validation | `apps/proxy`, `packages/shared` |
| `three` | 3D rendering | `apps/web` |
| `@react-three/fiber` | React renderer for three.js | `apps/web` |
| `@react-three/drei` | three.js helpers (controls, textures) | `apps/web` |
| `@react-three/drei` | three.js helpers (controls, textures, bounds) | `apps/web` |
| `@react-three/postprocessing` | Post-processing effects | `apps/web` |
### Runtime constraints
+76
View File
@@ -184,3 +184,79 @@ tracing (the default) is the common case for token/tile art. For shape
inset/outset, `clipper-lib` (Angus Johnson's Clipper ported to JS) was chosen
over the `polygon-offset` package because the latter crashes on degenerate
cases (collapse, hole closure) via a bug in its pinned Martinez dependency.
## D14 — Extrusion exposes separated front/back/walls
**Decision:** `extrudeShapeParts` returns `{ front, back, walls }` as separate
geometries, and `tessellate.ts` exposes `frontFaces` / `backFaces` (with
`capFaces` kept as a merged convenience).
**Context:** Cards and tiles need distinct materials on the front and back
faces, and the back must be flipped so it isn't mirrored when viewed from
behind. Splitting the caps into front/back at the mesh level lets each viewer
apply its own material without post-hoc geometry-group splitting.
**Alternatives considered:** Returning a single merged caps geometry and
splitting it in the viewer (the previous approach for cards). Rejected —
required manual triangle-group bookkeeping in the component.
## D15 — Back faces are flipped on the material, not the geometry
**Decision:** The back face is un-mirrored by flipping the texture on the
material (`flipTexture.ts` negates `repeat.x` and shifts `offset.x`), rather
than by transforming the geometry's UVs.
**Context:** The back cap maps with the same planar UVs as the front, so
without a flip it appears mirrored. Flipping on the material keeps the mesh
geometry simple and shared, and works for both a full texture and a sprite
cell.
**Alternatives considered:** Flipping the UVs in `backFaces`. Rejected —
would bake the flip into the shared mesh package, forcing it on every consumer
rather than letting viewers opt in.
## D16 — Card sprite selection via `CardID` and the parent deck
**Decision:** A card's face/back sprite is selected from the deck sheet by
`CardID` (deck index in the hundreds place, 0-based card number in the last
two digits). The sheet config (grid, face/back URLs, `UniqueBack`) is resolved
from the containing deck's `CustomDeck[deckIndex]`, which is authoritative
over the card's own `CustomDeck` (often keyed differently or absent).
**Context:** Deck images are sheets divided into a `NumWidth` x `NumHeight`
grid. The card's own `CustomDeck` field is unreliable — in the Wingspan dump a
card with `CardID 1605` carries `CustomDeck: {14: ...}` even though its deck
index is 16 — so the parent deck is the source of truth.
**Alternatives considered:** Using the card's own `CustomDeck`. Rejected —
produces the wrong sprite for cards whose own field is mis-keyed or missing.
## D17 — Tree selection by index path, not GUID
**Decision:** The object tree selects nodes by their unique index path (e.g.
`0-3-1`) rather than by `GUID`.
**Context:** Cards in a deck share the deck's GUID — in the Wingspan dump, 292
of 606 objects carry a duplicate GUID. GUID-based selection highlighted every
card with that GUID and rendered the first match, so the wrong sprite could
show.
**Alternatives considered:** Using `GUID` (the previous approach). Rejected —
ambiguous for decked cards.
## D18 — Camera fit via drei `Bounds` inside Suspense
**Decision:** The viewer camera is fitted to the object's bounds using drei's
`Bounds` component, placed inside the scene's Suspense boundary so it mounts
only after the (suspending) content has loaded.
**Context:** Viewers load content asynchronously (textures suspend, tokens
trace via the proxy, models stream in). `Bounds` fits on mount, so it must
mount after the content is present. The token viewer was converted from
`useEffect` + state to a suspending resource so it participates in the same
boundary.
**Alternatives considered:** A polling `CameraFit` that waited for non-empty
bounds each frame; hand-rolled camera math. Rejected — Suspense already
signals content readiness, so `Bounds` inside the boundary fits the loaded
geometry directly.
+25 -14
View File
@@ -205,16 +205,20 @@ Isomorphic analysis of a parsed `TTSMod`. No Node-specific APIs.
`scaleShape` and `signedArea`.
- `tessellate.ts`
- `triangulate(shape)` — earcut triangulation (same as three.js).
- `capFaces(shape, height, uvScale, uvBounds)` — top/bottom faces. UVs map
the shape's bounding box (or `uvBounds` framing) to the unit square; the
bottom face uses the same planar xy mapping as the top (no mirror).
- `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 caps + walls as one geometry.
- `extrudeShapeParts(shape, options)` — caps and walls as separate
geometries (for distinct materials).
- `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`.
@@ -278,7 +282,8 @@ proxy API and `packages/extract` directly for analysis.
- `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.
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.
@@ -286,13 +291,19 @@ proxy API and `packages/extract` directly for analysis.
`@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`/`Deck`/`Custom_Deck` (thin box with
face/back textures), and `Custom_Model`/`Custom_Model_Bag`/
`Custom_Model_Infinite_Bag` (GLTF/OBJ/FBX from `CustomMesh.MeshURL`).
`Scene.tsx` is a shared canvas with lighting, orbit controls, contact
shadows, and subtle bloom/vignette. `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.
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, ...);