89 lines
4.3 KiB
Markdown
89 lines
4.3 KiB
Markdown
# comic-odin / comicsroll
|
||
|
||
Dual-language repo: **React/TypeScript frontend** (`src/`) + **Odin native desktop app** (`odin/`).
|
||
|
||
## Build & Test — Odin (`odin/`)
|
||
|
||
```bash
|
||
# Build debug binary
|
||
./odin/build.sh # builds C deps + odin src/app
|
||
|
||
# Or directly:
|
||
odin build odin/src/app -target:linux_amd64 -collection:clay=$(pwd)/odin/vendor/clay/bindings/odin/clay-odin
|
||
|
||
# Run all tests
|
||
odin test odin/tests -target:linux_amd64 -collection:clay=$(pwd)/odin/vendor/clay/bindings/odin/clay-odin
|
||
|
||
# Run single test
|
||
odin test odin/tests -target:linux_amd64 -collection:clay=$(pwd)/odin/vendor/clay/bindings/odin/clay-odin -define:ODIN_TEST_NAMES=tests.<test_proc_name>
|
||
|
||
# Run binary
|
||
./bin/comic_odin gui # Raylib GUI
|
||
./bin/comic_odin tui # Terminal TUI
|
||
```
|
||
|
||
## Build & Test — React Frontend (`src/`)
|
||
|
||
```bash
|
||
npm run dev # vite dev server
|
||
npm run build # tsc -b && vite build
|
||
npm run lint # eslint . (strictTypeChecked, no any)
|
||
npm run typecheck # tsc --noEmit
|
||
npm run test # vitest run (jsdom, globals)
|
||
npm run test:ui # vitest --ui
|
||
```
|
||
|
||
## Architecture
|
||
|
||
### React (Vite 8, React 19, TypeScript 6, Tailwind 3, Zustand 5)
|
||
- **No URL routing** — manual `useState` + zustand `workflow.currentStep` drives component switching in `App.tsx`.
|
||
- **Single zustand store** (`src/store/comicStore.ts`) with `persist` middleware.
|
||
- **Two API services**: DeepSeek (via OpenAI SDK for script gen) and fal.ai (via `@fal-ai/client` for images).
|
||
- **`@` path alias** → `./src`.
|
||
- **`verbatimModuleSyntax: true`** — type-only imports need `import type`.
|
||
- **`noUnusedLocals`, `noUnusedParameters` both `true`**.
|
||
- `framer-motion`, `fabric`, `react-router-dom` installed but unused.
|
||
- No test files exist yet (vitest configured and ready).
|
||
|
||
### Odin (Raylib + Clay layout engine + osdialog)
|
||
- **Single binary**, entry: `odin/src/app/main.odin` → `run_cli_from_process_args()` → `.Gui` → `gui.run_gui_app()`
|
||
- **8 source packages**: `app` (entry), `core` (domain), `shared` (config/errors), `adapters` (DeepSeek/fal/export/storage), `ui` (controller/screens/jobs), `gui` (Raylib+Clay UI), `osdialog` (file dialogs).
|
||
- Clay imported as `import clay "clay:."` via collection `-collection:clay=vendor/clay/bindings/odin/clay-odin`.
|
||
- **ols.json** defines the `clay` collection path.
|
||
|
||
### Workflow state machine
|
||
```
|
||
Story_Input → Generating_Script → Script_Review → Character_Setup →
|
||
Generating_Panels → Layout → Speech_Bubbles → Complete
|
||
```
|
||
Defined in `core/workflow.odin` (`can_transition()`). In the React app, zustand's `workflow.currentStep` mirrors this.
|
||
|
||
## Clay Layout Gotchas (Critical)
|
||
|
||
- **`SizingPercent()` expects 0–1, not 0–100.** `SizingPercent(55)` = 5500% (off-screen). Use `SizingPercent(0.55)`.
|
||
- **`SizingFit({})` cards + `SizingGrow({})` children = invisible.** A Fit-height card computes its height from children's *initial* CloseElement sizes. Grow children have initial height 0, so the card stays tiny. If a card contains a Grow-height child (scroll list, wireframe), override the card height to `SizingGrow({})`.
|
||
- **`backgroundColor` on image elements generates a covering RECTANGLE.** Remove `backgroundColor` from image elements to let the IMAGE command be visible.
|
||
- **Texture pointers** (`rawptr(tex_ptr)`) require `reserve(&app.panel_textures, N)` after map creation to avoid pointer invalidation on reallocation.
|
||
- **Clay v0.14** is vendored at `vendor/clay/`. The Odin binding is at `vendor/clay/bindings/odin/clay-odin/clay.odin`.
|
||
|
||
## Raygui Integration
|
||
|
||
- `rl.GuiTextBox` overlays on top of Clay layout. After Clay renders, get element bounds via `clay.GetElementData(id)`.
|
||
- `GuiTextBox` returns `true` on click — NOT on text change. Compare buffer length after call to detect edits.
|
||
- `rl.EndScissorMode()` must be called before `rl.GuiTextBox` and after to clear stale scissor state.
|
||
|
||
## Memory
|
||
|
||
- **Odin**: `persistent_pool` (256KB) in `gui/runtime.odin` for strings that must survive temp allocator resets.
|
||
- **Odin**: All `delete()`'d string defaults in `new_initial_state()` must use `strings.clone()` to avoid `free(): invalid pointer` crashes.
|
||
- **React**: Zustand store persisted to localStorage (`comic-creator-storage`). `partialize` excludes ephemeral state.
|
||
|
||
## API Keys (.env)
|
||
|
||
```
|
||
DEEPSEEK_API_KEY=sk-...
|
||
FAL_API_KEY=...
|
||
VITE_DEEPSEEK_API_KEY=sk-...
|
||
VITE_FAL_KEY=...
|
||
```
|