sbuffkin_hexmaker/ARCHITECTURE.md

262 lines
12 KiB
Markdown
Raw Normal View History

2026-03-11 17:39:22 +00:00
# Architecture Overview — Duckmage Plugin
2026-03-18 02:49:12 +00:00
A tabletop RPG world-building plugin for Obsidian. Three interconnected views (hex map, hex table, random tables/workflows) operate on a shared vault of Markdown notes.
2026-03-11 17:39:22 +00:00
---
## Views
### HexMapView
2026-03-18 02:49:12 +00:00
The primary interactive surface. Renders a hex grid where every cell corresponds to a `{hexFolder}/{region}/{x}_{y}.md` note on disk. Multiple regions are supported; the active region is tracked as `activeRegionName`.
2026-03-11 17:39:22 +00:00
**DOM structure:**
```
.duckmage-hex-map-container
├── .duckmage-hex-map-clip ← overflow:hidden clipping layer
│ └── .duckmage-hex-map-viewport ← transform target (pan + zoom)
│ └── .duckmage-hex × N ← individual hex cells
└── .duckmage-hex-map-controls ← pointer-events:none overlay
├── expand buttons (+)
└── drawing toolbar
```
The clip/controls separation ensures toolbar buttons are never hidden by the viewport's CSS transform.
**Drawing tools** — each follows the same pattern:
| Tool | drawingMode | Opens | Section written |
|------|-------------|-------|-----------------|
| Road | `"road"` | (direct click) | `settings.roadChains` |
| River | `"river"` | (direct click) | `settings.riverChains` |
| Terrain | `"terrain"` | `TerrainPickerModal` | frontmatter `terrain:` |
| Icon | `"icon"` | `IconPickerModal` | frontmatter `icon:` |
| Link Table | `"tableLink"` | `TablePickerModal` | `### Encounters Table` |
2026-05-10 00:49:23 +00:00
| Factions | `"factionLink"` | `FactionPickerModal` | `### Factions` |
2026-03-11 17:39:22 +00:00
**Tool lifecycle** (standard pattern):
1. Button click → `handle*Button()` opens picker modal
2. Picker callback sets `drawingMode` + paint target, calls `updateToolbarButtonStates()`
3. Hex left-click → `onHex*Click(x, y)`
4. Right-click (on any hex) → `exit*Mode()``drawingMode = null`
5. `updateToolbarButtonStates()` syncs active CSS and button labels
**Write strategies:**
- Terrain/icon: synchronous DOM patch + coalescing async write queue (`scheduleTerrainWrite` / `scheduleIconWrite`). Only the latest value per hex is flushed, preventing stale writes.
- Link tools: purely async, no DOM patch needed (badge added directly after `await addLinkToSection`).
- Roads/rivers: written to `plugin.settings` and persisted via `saveSettings()`.
**Visual feedback for link tools:**
- Persistent: accent-coloured outline + emoji badge (📋 table, ⚔️ faction)
- Transient: `.duckmage-hex-blip` — a CSS-animated circle that expands and fades, then self-removes on `animationend`
---
### HexTableView
A live spreadsheet of all hex notes found in `hexFolder`. Updates on vault `modify` events (300 ms debounce per file).
**Columns:** Hex (coords + ◎ jump button), Terrain (colour swatch), then one column per section key from `COLUMNS`.
2026-03-18 02:49:12 +00:00
Current columns in order: Description, Landmark, Towns, Dungeons, Features, Quests, Factions, Enc. Table, Hidden, Secret, Weather, Hooks & Rumors.
The **Enc. Table** cell displays the basename of the linked table file (not the full path). Clicking a populated cell opens `RandomTableView` at that file via `openTable()`.
2026-03-11 17:39:22 +00:00
**Filters (toolbar):**
- X / Y numeric range inputs
2026-03-18 02:49:12 +00:00
- Region selector (scoped to one region subfolder)
2026-03-11 17:39:22 +00:00
- Terrain multi-select modal — left-click to include, right-click to exclude (strikethrough + red)
- Has Town / Has Dungeon / Has Feature / Has Quest / Has Faction checkboxes
- Sort priority (X→Y or Y→X) and direction (Asc / Desc)
Filter state is stored as `dataset` attributes on each `<tr>` (`data-terrain`, `data-has-town`, etc.) and read by `applyFilters()` — no file I/O on filter change.
**Jump button (◎):** Calls `HexMapView.centerOnHex(x, y)` on the first open map leaf, panning the viewport to that hex.
---
### RandomTableView
2026-03-18 02:49:12 +00:00
A two-column browser with two modes toggled by tab buttons: **Tables** and **Workflows**.
**Tables mode:**
- Left: folder tree (collapsible) + search filter + new-table creator at the bottom.
- Right: selected table — entries with die-range (`dN` column) and percentage odds, die-size selector, Roll button, copy-result textarea, recent roll history. Edit button opens `RandomTableEditorModal`.
- Right-click a table in the tree → context menu with "Delete table".
2026-03-11 17:39:22 +00:00
2026-03-18 02:49:12 +00:00
**Workflows mode:**
- Left: folder tree of workflow files + new-workflow creator. New workflows created with "New workflow with this table" (from a table detail pane) pre-populate the first step including auto-derived label.
- Right: selected workflow — each step shown as a clickable link (opens that table in Tables mode) with `×N` roll count, "Roll workflow" button, Edit button opens `WorkflowEditorModal`.
2026-03-18 02:49:12 +00:00
- Right-click a workflow in the tree → context menu with "Delete workflow".
2026-03-11 17:39:22 +00:00
2026-03-18 02:49:12 +00:00
**Public API:**
- `openTable(filePath)` — switches to Tables mode, expands all ancestor folders of the target file so it is visible in the tree, refreshes the list, and loads the specified table. Called from `HexTableView` (Enc. Table cell), `HexEditorModal`, workflow step links, and the `obsidian://duckmage-roll` protocol handler.
2026-03-11 17:39:22 +00:00
---
## Modals
| Modal | Purpose |
|-------|---------|
| `HexEditorModal` | Right-click per-hex editor — terrain, all link sections, all text sections |
| `TerrainPickerModal` | Full terrain palette picker (toolbar terrain button) |
2026-03-18 02:49:12 +00:00
| `TerrainEntryEditorModal` | Edit a single palette entry — name, color, icon, icon color tint |
2026-03-11 17:39:22 +00:00
| `IconPickerModal` | Full icon picker with image previews (toolbar icon button) |
2026-03-18 02:49:12 +00:00
| `RegionModal` | Switch active region; create, rename, delete regions |
| `FileLinkSuggestModal` | Fuzzy-search file picker scoped to any folder |
2026-03-11 17:39:22 +00:00
| `RandomTableModal` | Inline roll modal — used from HexEditorModal 🎲 buttons |
2026-03-18 02:49:12 +00:00
| `RandomTableEditorModal` | Edit table entries (result + weight), linked folder, description, filter flags |
| `WorkflowEditorModal` | Edit workflow definition — steps, template, results folder. Draggable by title bar. Auto-saves on close. |
| `WorkflowWizardModal` | Execute a workflow — roll/pick each step, view filled template, save result as a vault note |
2026-03-11 17:39:22 +00:00
---
## Data model
### Hex notes
2026-03-18 02:49:12 +00:00
Path: `{hexFolder}/{region}/{x}_{y}.md`
2026-03-11 17:39:22 +00:00
```yaml
---
terrain: forest
icon: custom-castle.png # optional override
---
### Towns
[[My Town]]
### Dungeons
### Features
### Quests
### Factions
[[Iron Brotherhood]]
### Encounters Table
[[forest-encounters]]
### Description
Rolling hills...
### Landmark
### Hidden
### Secret
### Weather
### Hooks & Rumors
```
- `terrain` and `icon` live in YAML frontmatter (read via metadata cache, written via raw-text patching in `frontmatter.ts`).
- All other data lives under `###` headings (read/written via `sections.ts`).
### Link sections (`LINK_SECTIONS`)
```
"Towns" | "Dungeons" | "Features" | "Quests" | "Factions" | "Encounters Table"
```
Each has a corresponding folder setting. The heading key used in files is the section name lowercased.
### Text sections (`TEXT_SECTIONS`)
```
2026-03-18 02:49:12 +00:00
description | landmark | hidden | secret
2026-03-11 17:39:22 +00:00
```
2026-03-18 02:49:12 +00:00
Weather and Hooks & Rumors are also free-text sections under `###` headings but are not part of the `TEXT_SECTIONS` constant.
2026-03-11 17:39:22 +00:00
### Roads & rivers
Stored as `string[][]` in `settings.roadChains` / `settings.riverChains`. Each chain is an ordered array of `"x_y"` keys. Rendered as SVG polylines connecting adjacent hexes.
### Random tables
Markdown files with:
```yaml
---
dice: 100
---
| Result | Weight |
|--------|--------|
| Dragons | 1 |
| Goblins | 5 |
```
Parsed by `randomTable.ts` into `{ dice, entries: [{result, weight}] }`. Terrain tables generated at `{tablesFolder}/terrain/{name} - description.md` and `…encounters.md`.
2026-03-18 02:49:12 +00:00
### Workflows
Markdown files with YAML frontmatter + a steps table:
```yaml
---
results-folder: world/results
template-file: world/workflows/templates/My Workflow.md
---
| Table | Rolls | Label |
|-------|-------|-------|
| [[world/tables/forest-encounters]] | 2 | forest_encounters |
| [[world/tables/treasure]] | 1 | treasure |
```
Parsed/serialized by `workflow.ts`. Template files live at `{workflowsFolder}/templates/{name}.md` and use `$label` / `$label_N` placeholders (multi-roll steps). `WorkflowEditorModal` auto-syncs the template when steps change: adding a step appends its section, changing a label updates the `## heading` and all `$var`/`$var_N` references, changing roll count adds or removes `_N`-suffixed placeholders. `WorkflowWizardModal` fills placeholders with roll results and saves the output as a new note.
2026-03-18 02:49:12 +00:00
2026-03-11 17:39:22 +00:00
### Settings (`data.json`)
| Key | Purpose |
|-----|---------|
| `worldFolder` | Root vault folder — scopes file search |
2026-03-18 02:49:12 +00:00
| `hexFolder` | Where region subfolders and `x_y.md` notes live |
2026-03-11 17:39:22 +00:00
| `townsFolder` | Folder for the Towns dropdown |
| `dungeonsFolder` | Folder for the Dungeons dropdown |
| `questsFolder` | Folder for the Quests dropdown |
| `featuresFolder` | Folder for the Features dropdown |
| `factionsFolder` | Folder for the Factions dropdown |
| `tablesFolder` | Folder for random table files |
2026-03-18 02:49:12 +00:00
| `workflowsFolder` | Folder for workflow files (templates in `{workflowsFolder}/templates/`) |
2026-03-11 17:39:22 +00:00
| `iconsFolder` | User custom icons (merged with plugin `icons/`) |
| `templatePath` | Path to hex note template |
| `hexGap` | CSS gap between cells |
| `hexOrientation` | `"pointy"` or `"flat"` |
2026-03-18 02:49:12 +00:00
| `terrainPalette` | `[{name, color, icon?, iconColor?}]` |
2026-03-11 17:39:22 +00:00
| `gridSize` | `{cols, rows}` |
| `gridOffset` | `{x, y}` origin offset (adjusted by expand buttons) |
| `zoomLevel` | Current zoom |
| `roadChains` | `string[][]` road chains |
| `riverChains` | `string[][]` river chains |
| `roadColor` / `riverColor` | CSS colour strings |
| `defaultTableDice` | Die size for new table files |
2026-03-18 02:49:12 +00:00
| `regions` | `[{name}]` — named map regions (each maps to a subfolder under `hexFolder`) |
2026-03-11 17:39:22 +00:00
---
## Module responsibilities
| File | Responsibility |
|------|---------------|
| `DuckmagePlugin.ts` | Entry point, view registration, cross-view API, terrain table generation |
2026-03-18 02:49:12 +00:00
| `HexMapView.ts` | Grid rendering, all drawing tools, panning/zooming, expand, region switching |
2026-03-11 17:39:22 +00:00
| `HexEditorModal.ts` | Per-hex right-click editor |
2026-03-18 02:49:12 +00:00
| `HexTableView.ts` | Hex spreadsheet, filters, sort, jump, enc-table → RandomTableView navigation |
| `RegionModal.ts` | Region switcher and manager |
| `RandomTableView.ts` | Table + workflow browser, roll UI, public `openTable()` |
2026-03-11 17:39:22 +00:00
| `RandomTableModal.ts` | Inline roll from hex editor |
| `RandomTableEditorModal.ts` | Table entry editor |
2026-03-18 02:49:12 +00:00
| `WorkflowEditorModal.ts` | Workflow definition editor (steps, template, draggable modal) |
| `WorkflowWizardModal.ts` | Workflow execution — roll steps, fill template, save result note |
| `TerrainPickerModal.ts` | Full terrain palette picker |
| `TerrainEntryEditorModal.ts` | Single terrain palette entry editor |
| `IconPickerModal.ts` | Icon picker with previews |
| `FileLinkSuggestModal.ts` | Reusable fuzzy file picker |
2026-03-11 17:39:22 +00:00
| `randomTable.ts` | Pure parse/roll/weight logic (no Obsidian API) |
2026-03-18 02:49:12 +00:00
| `workflow.ts` | Pure workflow parse/serialize/template logic (no Obsidian API) |
2026-03-11 17:39:22 +00:00
| `frontmatter.ts` | `terrain:` and `icon:` read/write |
| `sections.ts` | `###` heading read/write (`addLinkToSection`, `getLinksInSection`, `getAllSectionData`, `setSectionContent`, `addBacklinkToFile`) |
2026-03-18 02:49:12 +00:00
| `utils.ts` | `normalizeFolder`, `getIconUrl`, `makeTableTemplate`, `createIconEl` |
2026-03-11 17:39:22 +00:00
| `types.ts` | `DuckmagePluginSettings`, `LINK_SECTIONS`, `TEXT_SECTIONS`, `TerrainColor` |
| `constants.ts` | `VIEW_TYPE_*`, `DEFAULT_TERRAIN_PALETTE`, `DEFAULT_SETTINGS` |
2026-03-18 02:49:12 +00:00
| `DuckmageSettingTab.ts` | Settings UI — "Generate folders", "Generate terrain tables", palette editor |
2026-03-11 17:39:22 +00:00
---
## Key conventions
- Folder paths normalised (no leading/trailing slashes) via `normalizeFolder()`.
- All wiki-links use `metadataCache.fileToLinktext(file, sourcePath)` for vault-relative resolution.
- `obsidian` is an esbuild external — never bundled.
- CSS classes use the `duckmage-` prefix.
- Views and modals import `DuckmagePlugin` as a type only (`import type`) to avoid circular runtime deps.
2026-03-18 02:49:12 +00:00
- Metadata-cache race after `vault.modify`: `HexEditorModal.onChanged()` defers `renderGrid()` by 300 ms when no terrain/icon overrides are passed.
- Modals that preload file content accept an optional `preloaded` / `initialContent` parameter to avoid redundant vault reads when the caller already holds the content.
- Notes whose `basename` starts with `"_"` are excluded from all dropdowns, file trees, and auto-generated lists. Pattern: `.filter(f => !f.basename.startsWith("_"))`.
- Draggable modals call `makeDraggable()` in `onOpen`, add the `duckmage-editor-modal-drag` class, and restrict drag to the title-bar area.