# Frontend Rebuild Guide Everything under `src/three/*` and `src/ui/*` is a **replaceable prototype**. A new frontend only needs the three stable interfaces below; it never touches `engine.ts` or the pipeline. ## 1. Runtime data API — `src/data/load.ts` ```ts loadData(base = "/data"): Promise // Fetches charset.json + poets.index.json + manifest.json, builds the real // PoetryDataset, and calls provider.setDataset() — engine math goes live. // Call once at boot; gate the 3D scene on completion (store.loaded). getPoets(): PoetRow[] // all ~32,657 poets, sorted by poemCount desc getPoet(id): PoetRow | undefined loadPoetPoems(id): Promise // lazy — HTTP Range-fetches just this poet's slice // (byte-offset sidecar poems/{b}.idx.json); per-poet cache. // Falls back to the whole bucket if no sidecar / no 206. searchPoets(query, limit?): PoetRow[] // substring name match, ranked by output searchByLine(query): Promise // 诗句 search — ALL-lines index → real poems loadGifts(): Promise // 赠诗 edges [fromId,toId,weight] (lazy, cached) getManifest(): DataManifest | null // {n, poetCount, poemCount, buckets, dynCounts} ``` `searchByLine` now matches **ANY line** (not just openings): the pipeline indexes every line into `public/data/lines/{bucket}.json` (256 shards, sharded by `hashStr(line)&0xff` == pipeline `lineBucket`). 疑是地上霜 → 李白《静夜思》 (a non-first line) now resolves. A `LineHit` carries `{poetId, poemIdx, title, form, firstLine, poet}` → open the poet & surface `poems[i]`. ```ts type PoetRow = { id; name; dynasty; poemCount; clusterSize } type PoemRecord = { t: title; f: "wujue"|"qijue"|"wulu"|"qilu"|"other"; p: lines[] } ``` ## 2. Engine API — `src/engine/engineApi.ts` ```ts pullAt(form: PullForm, [x,y,z], {lushiOnly?, commonK?}): PulledPoem // void-pull at a point // PullForm = FormId | "ziyou"; form="ziyou" → variable-length 自由格式/词 (splitFree lines) pointForBabelIndex(form, b, R?): Vec3 // 3D location of a known index (fly-to) textBabelIndex(form, hanText): {index, digits} | null // ↑ a REAL poem's catalog index (null unless its length matches the form & chars ∈ 字库) halfIndex(form, han) / halfIndexAuto(han): HalfIndex | null // 半编号 of a typed opening babelCardinality(form) / regulatedCardinality(form): bigint // ── 反查 (编号 → 诗): the other direction of the bijection ── pullByIndex(form: PullForm, indexStr): IndexPoem | null // unrank a decimal index back to its poem // IndexPoem = {form, lines, index, digits, inRange, cardinalityDigits}; powers the 编号反查 tab. // inRange=false ⇒ the number is past |catalog| (UI says "共 … 首"). SearchPanel cross-checks the // line index + full text and reports if the number is a REAL existing poem. pulledFromIndex(form: PullForm, indexStr): PulledPoem | null // rebuild a full pull at its canonical // scattered point (for permalink restore) — a shared #p link drops you onto the same star. ``` See [ENGINE_API.md](ENGINE_API.md). **First char = most-significant digit** ⇒ a known opening line pins the high-order index (basis for 半编号 prefix search). ## 3. Star geometry — `src/three/PoetStars.tsx` ```ts poetPosition(p: PoetRow): [x,y,z] // deterministic galaxy position (dynasty shell + hash) ``` Used to place a star, a label, or a fly-to target. Dynasty layout/colour come from `src/data/dynasties.ts` (`DYNASTIES`, `bandRadius`, `DYNASTY_BY_KEY`). Famous poets in `src/data/famousPoets.ts` (`FAMOUS_POETS`, now incl. modern names) drive **landmark emphasis** — those stars render at 2.4× size with a gilded glow so the cloud has named anchors. ## 4. UI state — `src/state/store.ts` (zustand) | field | meaning | |---|---| | `loaded` | data ready | | `form` | active poem form (五绝…七律) for void-pulls | | `hidden: Set` | dynasties filtered out (`toggleDynasty`, `showOnly`, `showAllDynasties`) | | `selected: PulledPoem` | the last void-pull (random poem) → `PoemPanel` | | `selectedPoet` / `poetPoems` | clicked/searched poet + their lazy-loaded poems → `PoetPanel` | | `hoverPoetId` | poet under cursor (shows a label) | | `speed` | camera speed multiplier (HUD readout) | | `flyTarget` | `[x,y,z]` the camera tweens toward, then auto-clears | | `pulls` | recent void-pull markers (`PulledStars`); each `Pull` now has a stable `id` so markers animate their own birth/death. `MAX_PULLS=24`, `PulledStars` caps ALIVE at 20 | | `quality` | render quality `"high" \| "low"` (`toggleQuality`) — `low` halves galaxy counts (~166k→~59k) + disables bloom, for weak GPUs | | `uiHidden` | hide ALL overlay UI for screenshots (`toggleUI`) — corner 隐藏界面 button + the **H** hotkey (`App` keydown, ignored while typing) | Transient camera transform lives in `FlyControls` refs, NOT the store (no 60fps re-renders). ## 5. Interaction contract (current shell — reimplement as you like) - **Pointer drag** = look; **WASD / Space / Shift** = fly; **wheel** = speed; **H** = hide/show all overlay UI (screenshot mode). Keys are ignored while an ``/`