# 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. # 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=... ```