From 12418898d0892f853472ba6447928af4d0bdd99b Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 8 Aug 2026 18:18:22 +0800 Subject: [PATCH] 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. --- docs/architecture.md | 6 +-- docs/decisions.md | 78 ++++++++++++++++++++++++++++++++++++- docs/implementation-plan.md | 39 ++++++++++++------- 3 files changed, 105 insertions(+), 18 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 2a84c91..13f480d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/decisions.md b/docs/decisions.md index ef59d8c..40a89d9 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -183,4 +183,80 @@ parsing yields a shape the mesh package can consume directly, and alpha-based 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index fe04eae..628ec02 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -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, ...);