add undo and redo

This commit is contained in:
isaprettycoolguy@protonmail.com 2026-03-23 22:26:50 -04:00
parent cbe1b01823
commit 632e514c3e
6 changed files with 524 additions and 186 deletions

View file

@ -11,6 +11,6 @@ cd /mnt/c/Users/markr/Documents/KB/journal/.obsidian/plugins/duckmage-plugin &&
Run this in the background — it is a long-running process. Tell the user the watcher has started, then remind them:
- `main.js` rebuilds automatically on every save to `main.ts`
- To reload the plugin in Obsidian after a rebuild: open the developer console (Ctrl+Shift+I) and run:
`app.plugins.disablePlugin('duckmage-plugin'); app.plugins.enablePlugin('duckmage-plugin');`
- To reload the plugin in Obsidian after a rebuild, run:
`powershell.exe -Command "obsidian plugin:reload id=duckmage-plugin"`
- Or use `/rebuild` instead for a one-off production build (includes type-checking).

View file

@ -1,129 +1,129 @@
import type { TerrainColor, DuckmagePluginSettings } from "./types";
export const VIEW_TYPE_HEX_MAP = "duckmage-hex-map";
export const VIEW_TYPE_HEX_TABLE = "duckmage-hex-table";
export const VIEW_TYPE_RANDOM_TABLES = "duckmage-random-tables";
export const DEFAULT_TERRAIN_PALETTE: TerrainColor[] = [
// Water
{ name: "ocean", color: "#29507f" },
{ name: "shallows", color: "#4a82a5" },
{ name: "water", color: "#60a5fa" },
// Open
{ name: "grass", color: "#638c7b", icon: "bw-grassland.png" },
{ name: "hills", color: "#e0e8a1", icon: "bw-hills.png" },
{ name: "foothills", color: "#81c191", icon: "bw-hills.png" },
{ name: "snow", color: "#e0f2fe" },
{ name: "beach", color: "#cac181", icon: "bw-grassland.png" },
{ name: "urban", color: "#888888" },
// Desert
{ name: "desert", color: "#ecdba2" },
{ name: "desert rocky", color: "#e6bc60", icon: "bw-desert-rocky.png" },
{ name: "dunes", color: "#eccd7e", icon: "bw-dunes.png" },
{ name: "cactus", color: "#e2b75a", icon: "bw-cactus.png" },
{ name: "cactus heavy", color: "#ddb869", icon: "bw-cactus-heavy.png" },
{ name: "badlands", color: "#c2410c", icon: "bw-badlands.png" },
{ name: "brokenlands", color: "#92400e", icon: "bw-brokenlands.png" },
{ name: "cliffs", color: "#a86f1f", icon: "bw-brokenlands.png" },
{ name: "Salt Flats", color: "#f7eaba", icon: "bw-dunes.png" },
// Forest
{ name: "forest", color: "#2d9553", icon: "bw-forest-heavy.png" },
{ name: "forest heavy", color: "#15803d", icon: "bw-forest-heavy.png" },
{ name: "forested hills", color: "#22c55e", icon: "bw-forested-hills.png" },
{ name: "mixed forest", color: "#16a34a", icon: "bw-forest-mixed.png" },
{
name: "mixed forest heavy",
color: "#15803d",
icon: "bw-forest-mixed-heavy.png",
},
{
name: "mixed forest hills",
color: "#22c55e",
icon: "bw-forest-mixed-hills.png",
},
// Evergreen
{ name: "evergreen", color: "#428a5e", icon: "bw-evergreen.png" },
{ name: "evergreen heavy", color: "#257445", icon: "bw-evergreen-heavy.png" },
{ name: "evergreen hills", color: "#328651", icon: "bw-evergreen-hills.png" },
// Jungle
{ name: "jungle", color: "#15803d", icon: "bw-jungle.png" },
{ name: "jungle heavy", color: "#14532d", icon: "bw-jungle-heavy.png" },
{ name: "jungle hills", color: "#4ade80", icon: "bw-jungle-hills.png" },
// Mountains
{ name: "mountain", color: "#a77649", icon: "bw-mountain.png" },
{ name: "peak", color: "#78716c", icon: "bw-mountain.png" },
{ name: "mountains snow", color: "#bfdbfe", icon: "bw-mountains-snow.png" },
{
name: "forested mountain",
color: "#6b9e7c",
icon: "bw-forested-mountain.png",
},
{
name: "forested mountains",
color: "#5e8c6a",
icon: "bw-forested-mountains.png",
},
{
name: "mixed forest mountain",
color: "#6b9e7c",
icon: "bw-forest-mixed-mountain.png",
},
{
name: "mixed forest mountains",
color: "#5e8c6a",
icon: "bw-forest-mixed-mountains.png",
},
{
name: "evergreen mountain",
color: "#6b806b",
icon: "bw-evergreen-mountain.png",
},
{
name: "evergreen mountains",
color: "#8c9d80",
icon: "bw-evergreen-mountains.png",
},
{ name: "jungle mountain", color: "#4d7c0f", icon: "bw-jungle-mountain.png" },
{
name: "jungle mountains",
color: "#3f6212",
icon: "bw-jungle-mountains.png",
},
// Volcanic
{ name: "volcano", color: "#b91c1c", icon: "bw-volcano.png" },
{ name: "volcano dormant", color: "#78350f", icon: "bw-volcano-dormant.png" },
// Wetlands
{ name: "marsh", color: "#4d7c0f", icon: "bw-marsh.png" },
{ name: "swamp", color: "#365314", icon: "bw-swamp.png" },
{ name: "bog", color: "#432e6b", icon: "bw-swamp.png" },
];
export const DEFAULT_SETTINGS: DuckmagePluginSettings = {
mySetting: "default",
worldFolder: "world",
hexFolder: "world/hexes",
townsFolder: "",
dungeonsFolder: "",
questsFolder: "",
featuresFolder: "",
iconsFolder: "",
templatePath: "",
hexGap: "0.15",
terrainPalette: DEFAULT_TERRAIN_PALETTE,
gridSize: { cols: 20, rows: 16 },
gridOffset: { x: 0, y: 0 },
zoomLevel: 1,
roadChains: [],
riverChains: [],
roadColor: "#a16207",
riverColor: "#3b82f6",
hexOrientation: "flat",
tablesFolder: "world/tables",
factionsFolder: "",
defaultTableDice: 100,
hexEditorTerrainCollapsed: false,
hexEditorFeaturesCollapsed: false,
hexEditorNotesCollapsed: false,
rollTableExcludedFolders: ["terrain"],
encounterTableExcludedFolders: ["terrain"],
};
import type { TerrainColor, DuckmagePluginSettings } from "./types";
export const VIEW_TYPE_HEX_MAP = "duckmage-hex-map";
export const VIEW_TYPE_HEX_TABLE = "duckmage-hex-table";
export const VIEW_TYPE_RANDOM_TABLES = "duckmage-random-tables";
export const DEFAULT_TERRAIN_PALETTE: TerrainColor[] = [
// Water
{ name: "ocean", color: "#29507f" },
{ name: "shallows", color: "#4a82a5" },
{ name: "water", color: "#60a5fa" },
// Open
{ name: "grass", color: "#638c7b", icon: "bw-grassland.png" },
{ name: "hills", color: "#e0e8a1", icon: "bw-hills.png" },
{ name: "foothills", color: "#81c191", icon: "bw-hills.png" },
{ name: "snow", color: "#e0f2fe" },
{ name: "beach", color: "#cac181", icon: "bw-grassland.png" },
{ name: "urban", color: "#888888" },
// Desert
{ name: "desert", color: "#ecdba2" },
{ name: "desert rocky", color: "#e6bc60", icon: "bw-desert-rocky.png" },
{ name: "dunes", color: "#eccd7e", icon: "bw-dunes.png" },
{ name: "cactus", color: "#e2b75a", icon: "bw-cactus.png" },
{ name: "cactus heavy", color: "#ddb869", icon: "bw-cactus-heavy.png" },
{ name: "badlands", color: "#c2410c", icon: "bw-badlands.png" },
{ name: "brokenlands", color: "#92400e", icon: "bw-brokenlands.png" },
{ name: "cliffs", color: "#a86f1f", icon: "bw-brokenlands.png" },
{ name: "salt flats", color: "#f7eaba", icon: "bw-dunes.png" },
// Forest
{ name: "forest", color: "#2d9553", icon: "bw-forest-heavy.png" },
{ name: "forest heavy", color: "#15803d", icon: "bw-forest-heavy.png" },
{ name: "forested hills", color: "#22c55e", icon: "bw-forested-hills.png" },
{ name: "mixed forest", color: "#16a34a", icon: "bw-forest-mixed.png" },
{
name: "mixed forest heavy",
color: "#15803d",
icon: "bw-forest-mixed-heavy.png",
},
{
name: "mixed forest hills",
color: "#22c55e",
icon: "bw-forest-mixed-hills.png",
},
// Evergreen
{ name: "evergreen", color: "#428a5e", icon: "bw-evergreen.png" },
{ name: "evergreen heavy", color: "#257445", icon: "bw-evergreen-heavy.png" },
{ name: "evergreen hills", color: "#328651", icon: "bw-evergreen-hills.png" },
// Jungle
{ name: "jungle", color: "#15803d", icon: "bw-jungle.png" },
{ name: "jungle heavy", color: "#14532d", icon: "bw-jungle-heavy.png" },
{ name: "jungle hills", color: "#4ade80", icon: "bw-jungle-hills.png" },
// Mountains
{ name: "mountain", color: "#a77649", icon: "bw-mountain.png" },
{ name: "peak", color: "#78716c", icon: "bw-mountain.png" },
{ name: "mountains snow", color: "#bfdbfe", icon: "bw-mountains-snow.png" },
{
name: "forested mountain",
color: "#6b9e7c",
icon: "bw-forested-mountain.png",
},
{
name: "forested mountains",
color: "#5e8c6a",
icon: "bw-forested-mountains.png",
},
{
name: "mixed forest mountain",
color: "#6b9e7c",
icon: "bw-forest-mixed-mountain.png",
},
{
name: "mixed forest mountains",
color: "#5e8c6a",
icon: "bw-forest-mixed-mountains.png",
},
{
name: "evergreen mountain",
color: "#6b806b",
icon: "bw-evergreen-mountain.png",
},
{
name: "evergreen mountains",
color: "#8c9d80",
icon: "bw-evergreen-mountains.png",
},
{ name: "jungle mountain", color: "#4d7c0f", icon: "bw-jungle-mountain.png" },
{
name: "jungle mountains",
color: "#3f6212",
icon: "bw-jungle-mountains.png",
},
// Volcanic
{ name: "volcano", color: "#b91c1c", icon: "bw-volcano.png" },
{ name: "volcano dormant", color: "#78350f", icon: "bw-volcano-dormant.png" },
// Wetlands
{ name: "marsh", color: "#4d7c0f", icon: "bw-marsh.png" },
{ name: "swamp", color: "#365314", icon: "bw-swamp.png" },
{ name: "bog", color: "#432e6b", icon: "bw-swamp.png" },
];
export const DEFAULT_SETTINGS: DuckmagePluginSettings = {
mySetting: "default",
worldFolder: "world",
hexFolder: "world/hexes",
townsFolder: "",
dungeonsFolder: "",
questsFolder: "",
featuresFolder: "",
iconsFolder: "",
templatePath: "",
hexGap: "0.15",
terrainPalette: DEFAULT_TERRAIN_PALETTE,
gridSize: { cols: 20, rows: 16 },
gridOffset: { x: 0, y: 0 },
zoomLevel: 1,
roadChains: [],
riverChains: [],
roadColor: "#a16207",
riverColor: "#3b82f6",
hexOrientation: "flat",
tablesFolder: "world/tables",
factionsFolder: "",
defaultTableDice: 100,
hexEditorTerrainCollapsed: false,
hexEditorFeaturesCollapsed: false,
hexEditorNotesCollapsed: false,
rollTableExcludedFolders: ["terrain"],
encounterTableExcludedFolders: ["terrain"],
};

View file

@ -28,9 +28,9 @@ Slash commands (invoke inside Claude Code):
Each coding session:
1. Run `npm run dev` in a terminal (or `/dev` from Claude Code) — esbuild watches for changes and rebuilds `main.js` on every save
2. After a rebuild, reload the plugin in Obsidian via the developer console (Ctrl+Shift+I):
```js
app.plugins.disablePlugin('duckmage-plugin'); app.plugins.enablePlugin('duckmage-plugin');
2. After a rebuild, reload the plugin in Obsidian:
```bash
powershell.exe -Command "obsidian plugin:reload id=duckmage-plugin"
```
3. Use `/rebuild` for a final production build before committing (runs the TypeScript type-check and tests too)

View file

@ -8,64 +8,266 @@ export const VIEW_TYPE_RANDOM_TABLES = "duckmage-random-tables";
export const DEFAULT_TERRAIN_PALETTE: TerrainColor[] = [
// Sea
{ name: "trench", color: "#14223d", category: "sea" },
{ name: "ocean", color: "#29507f", category: "sea" },
{ name: "trench", color: "#14223d", category: "sea" },
{ name: "ocean", color: "#29507f", category: "sea" },
{ name: "shallows", color: "#4a82a5", category: "sea" },
// Uncategorised
{ name: "water", color: "#60a5fa" },
{ name: "urban", color: "#888888" },
{ name: "water", color: "#60a5fa" },
{ name: "urban", color: "#888888" },
// Lowlands
{ name: "grass", color: "#69a168", icon: "bw-grassland.png", category: "lowlands" },
{ name: "hills", color: "#e0e8a1", icon: "bw-hills.png", category: "lowlands" },
{ name: "foothills", color: "#81c191", icon: "bw-hills.png", category: "lowlands" },
{
name: "grass",
color: "#69a168",
icon: "bw-grassland.png",
category: "lowlands",
},
{
name: "hills",
color: "#e0e8a1",
icon: "bw-hills.png",
category: "lowlands",
},
{
name: "foothills",
color: "#81c191",
icon: "bw-hills.png",
category: "lowlands",
},
// Snow
{ name: "snow", color: "#e0f2fe", category: "snow" },
{ name: "mountains snow", color: "#bfdbfe", icon: "bw-mountains-snow.png", category: "snow" },
{ name: "snow", color: "#e0f2fe", category: "snow" },
{
name: "mountains snow",
color: "#bfdbfe",
icon: "bw-mountains-snow.png",
category: "snow",
},
// Desert
{ name: "desert", color: "#ecdba2", category: "desert" },
{ name: "desert rocky", color: "#e6bc60", icon: "bw-desert-rocky.png", category: "desert" },
{ name: "dunes", color: "#eccd7e", icon: "bw-dunes.png", category: "desert" },
{ name: "cactus", color: "#e2b75a", icon: "bw-cactus.png", category: "desert" },
{ name: "cactus heavy", color: "#ddb869", icon: "bw-cactus-heavy.png", category: "desert" },
{ name: "badlands", color: "#c2410c", icon: "bw-badlands.png", category: "desert" },
{ name: "brokenlands", color: "#92400e", icon: "bw-brokenlands.png", category: "desert" },
{ name: "desert", color: "#ecdba2", category: "desert" },
{
name: "desert rocky",
color: "#e6bc60",
icon: "bw-desert-rocky.png",
category: "desert",
},
{ name: "dunes", color: "#eccd7e", icon: "bw-dunes.png", category: "desert" },
{
name: "cactus",
color: "#e2b75a",
icon: "bw-cactus.png",
category: "desert",
},
{
name: "cactus heavy",
color: "#ddb869",
icon: "bw-cactus-heavy.png",
category: "desert",
},
{
name: "badlands",
color: "#c2410c",
icon: "bw-badlands.png",
category: "desert",
},
{
name: "brokenlands",
color: "#92400e",
icon: "bw-brokenlands.png",
category: "desert",
},
// Forest
{ name: "forest", color: "#2d9553", icon: "bw-forest.png", category: "forest" },
{ name: "forest heavy", color: "#15803d", icon: "bw-forest-heavy.png", category: "forest" },
{ name: "forested hills", color: "#22c55e", icon: "bw-forested-hills.png", category: "forest" },
{ name: "forested mountain", color: "#6b9e7c", icon: "bw-forested-mountain.png", category: "forest" },
{ name: "forested mountains", color: "#466d46", icon: "bw-forested-mountains.png", iconColor: "#1b1d1c", category: "forest" },
{ name: "mixed forest", color: "#16a34a", icon: "bw-forest-mixed.png", category: "forest" },
{ name: "mixed forest heavy", color: "#15803d", icon: "bw-forest-mixed-heavy.png", category: "forest" },
{ name: "mixed forest hills", color: "#22c55e", icon: "bw-forest-mixed-hills.png", category: "forest" },
{ name: "mixed forest mountain", color: "#6b9e7c", icon: "bw-forest-mixed-mountain.png", category: "forest" },
{ name: "mixed forest mountains", color: "#5e8c6a", icon: "bw-forest-mixed-mountains.png", category: "forest" },
{
name: "forest",
color: "#2d9553",
icon: "bw-forest.png",
category: "forest",
},
{
name: "forest heavy",
color: "#15803d",
icon: "bw-forest-heavy.png",
category: "forest",
},
{
name: "forested hills",
color: "#22c55e",
icon: "bw-forested-hills.png",
category: "forest",
},
{
name: "forested mountain",
color: "#6b9e7c",
icon: "bw-forested-mountain.png",
category: "forest",
},
{
name: "forested mountains",
color: "#466d46",
icon: "bw-forested-mountains.png",
iconColor: "#1b1d1c",
category: "forest",
},
{
name: "mixed forest",
color: "#16a34a",
icon: "bw-forest-mixed.png",
category: "forest",
},
{
name: "mixed forest heavy",
color: "#15803d",
icon: "bw-forest-mixed-heavy.png",
category: "forest",
},
{
name: "mixed forest hills",
color: "#22c55e",
icon: "bw-forest-mixed-hills.png",
category: "forest",
},
{
name: "mixed forest mountain",
color: "#6b9e7c",
icon: "bw-forest-mixed-mountain.png",
category: "forest",
},
{
name: "mixed forest mountains",
color: "#5e8c6a",
icon: "bw-forest-mixed-mountains.png",
category: "forest",
},
// Darkwood (evergreen)
{ name: "evergreen", color: "#428a5e", icon: "bw-evergreen.png", category: "darkwood" },
{ name: "evergreen heavy", color: "#257445", icon: "bw-evergreen-heavy.png", iconColor: "#1f1e1e", category: "darkwood" },
{ name: "evergreen hills", color: "#328651", icon: "bw-evergreen-hills.png", iconColor: "#292929", category: "darkwood" },
{ name: "evergreen mountain", color: "#6b806b", icon: "bw-evergreen-mountain.png", category: "darkwood" },
{ name: "evergreen mountains", color: "#8c9d80", icon: "bw-evergreen-mountains.png", category: "darkwood" },
{
name: "evergreen",
color: "#428a5e",
icon: "bw-evergreen.png",
category: "darkwood",
},
{
name: "evergreen heavy",
color: "#257445",
icon: "bw-evergreen-heavy.png",
iconColor: "#1f1e1e",
category: "darkwood",
},
{
name: "evergreen hills",
color: "#328651",
icon: "bw-evergreen-hills.png",
iconColor: "#292929",
category: "darkwood",
},
{
name: "evergreen mountain",
color: "#6b806b",
icon: "bw-evergreen-mountain.png",
category: "darkwood",
},
{
name: "evergreen mountains",
color: "#8c9d80",
icon: "bw-evergreen-mountains.png",
category: "darkwood",
},
// Island (jungle / volcanic)
{ name: "jungle", color: "#15803d", icon: "bw-jungle.png", category: "island" },
{ name: "jungle heavy", color: "#14532d", icon: "bw-jungle-heavy.png", iconColor: "#ffffff", category: "island" },
{ name: "jungle hills", color: "#4ade80", icon: "bw-jungle-hills.png", category: "island" },
{ name: "jungle mountain", color: "#4d7c0f", icon: "bw-jungle-mountain.png", category: "island" },
{ name: "jungle mountains", color: "#3f6212", icon: "bw-jungle-mountains.png", category: "island" },
{ name: "volcano", color: "#b91c1c", icon: "bw-volcano.png", category: "island" },
{ name: "volcano dormant", color: "#78350f", icon: "bw-volcano-dormant.png", iconColor: "#ffffff", category: "island" },
{
name: "jungle",
color: "#15803d",
icon: "bw-jungle.png",
category: "island",
},
{
name: "jungle heavy",
color: "#14532d",
icon: "bw-jungle-heavy.png",
iconColor: "#ffffff",
category: "island",
},
{
name: "jungle hills",
color: "#4ade80",
icon: "bw-jungle-hills.png",
category: "island",
},
{
name: "jungle mountain",
color: "#4d7c0f",
icon: "bw-jungle-mountain.png",
category: "island",
},
{
name: "jungle mountains",
color: "#3f6212",
icon: "bw-jungle-mountains.png",
category: "island",
},
{
name: "volcano",
color: "#b91c1c",
icon: "bw-volcano.png",
category: "island",
},
{
name: "volcano dormant",
color: "#78350f",
icon: "bw-volcano-dormant.png",
iconColor: "#ffffff",
category: "island",
},
// Mountain
{ name: "cliffs", color: "#a86f1f", icon: "bw-brokenlands.png", category: "mountain" },
{ name: "mountain", color: "#a77649", icon: "bw-mountain.png", category: "mountain" },
{ name: "Mountain Ridge", color: "#c55f0d", icon: "bw-mountains.png", category: "mountain" },
{ name: "peak", color: "#78716c", icon: "bw-mountain.png", category: "mountain" },
{
name: "cliffs",
color: "#a86f1f",
icon: "bw-brokenlands.png",
category: "mountain",
},
{
name: "mountain",
color: "#a77649",
icon: "bw-mountain.png",
category: "mountain",
},
{
name: "mountain ridge",
color: "#c55f0d",
icon: "bw-mountains.png",
category: "mountain",
},
{
name: "peak",
color: "#78716c",
icon: "bw-mountain.png",
category: "mountain",
},
// Bog (wetlands)
{ name: "marsh", color: "#909f23", icon: "bw-marsh.png", category: "bog" },
{ name: "swamp", color: "#4e5214", icon: "bw-swamp.png", iconColor: "#f9fbf9", category: "bog" },
{ name: "bog", color: "#432e6b", icon: "bw-swamp.png", iconColor: "#ffffff", category: "bog" },
{ name: "marsh", color: "#909f23", icon: "bw-marsh.png", category: "bog" },
{
name: "swamp",
color: "#4e5214",
icon: "bw-swamp.png",
iconColor: "#f9fbf9",
category: "bog",
},
{
name: "bog",
color: "#432e6b",
icon: "bw-swamp.png",
iconColor: "#ffffff",
category: "bog",
},
// Coast
{ name: "beach", color: "#cac181", icon: "bw-grassland.png", category: "coast" },
{ name: "Salt Flats", color: "#f7eaba", icon: "bw-dunes.png", category: "coast" },
{
name: "beach",
color: "#cac181",
icon: "bw-grassland.png",
category: "coast",
},
{
name: "salt flats",
color: "#f7eaba",
icon: "bw-dunes.png",
category: "coast",
},
];
export const DEFAULT_SETTINGS: DuckmagePluginSettings = {
@ -79,9 +281,18 @@ export const DEFAULT_SETTINGS: DuckmagePluginSettings = {
iconsFolder: "",
templatePath: "",
hexGap: "0.15",
terrainPalettes: [{ name: DEFAULT_PALETTE_NAME, terrains: DEFAULT_TERRAIN_PALETTE }],
terrainPalettes: [
{ name: DEFAULT_PALETTE_NAME, terrains: DEFAULT_TERRAIN_PALETTE },
],
regions: [
{ name: "default", paletteName: DEFAULT_PALETTE_NAME, gridSize: { cols: 20, rows: 16 }, gridOffset: { x: 0, y: 0 }, roadChains: [], riverChains: [] },
{
name: "default",
paletteName: DEFAULT_PALETTE_NAME,
gridSize: { cols: 20, rows: 16 },
gridOffset: { x: 0, y: 0 },
roadChains: [],
riverChains: [],
},
],
zoomLevel: 1,
roadColor: "#a16207",

View file

@ -82,6 +82,13 @@ export class HexMapView extends ItemView {
>();
private flushing = new Set<string>(); // "t:<path>" or "i:<path>"
private savingIndicatorEl: HTMLElement | null = null;
// Terrain undo
private readonly TERRAIN_UNDO_DEPTH = 20;
private terrainUndoStack: Array<{ x: number; y: number; path: string; oldTerrain: string | null; newTerrain: string | null }[]> = [];
private terrainRedoStack: Array<{ x: number; y: number; path: string; oldTerrain: string | null; newTerrain: string | null }[]> = [];
private currentTerrainStroke: Map<string, { x: number; y: number; path: string; oldTerrain: string | null; newTerrain: string | null }> | null = null;
private undoBtn: HTMLButtonElement | null = null;
private redoBtn: HTMLButtonElement | null = null;
activeRegionName = "default";
private regionBtn: HTMLButtonElement | null = null;
@ -174,6 +181,7 @@ export class HexMapView extends ItemView {
if (this.drawingMode === "terrain" || this.drawingMode === "icon") {
isTerrainPainting = true;
lastPaintedKey = null;
if (this.drawingMode === "terrain") this.currentTerrainStroke = new Map();
// Paint the hex under the cursor immediately
const hexEl = (e.target as HTMLElement).closest<HTMLElement>(
".duckmage-hex",
@ -248,6 +256,7 @@ export class HexMapView extends ItemView {
});
this.registerDomEvent(document, "mouseup", () => {
if (isTerrainPainting && this.drawingMode === "terrain") this.commitTerrainStroke();
isTerrainPainting = false;
lastPaintedKey = null;
isDragging = false;
@ -366,12 +375,31 @@ export class HexMapView extends ItemView {
this.updateRegionBtnLabel();
this.regionBtn.addEventListener("click", () =>
new RegionModal(this.app, this.plugin, this, () => {
this.terrainUndoStack = [];
this.terrainRedoStack = [];
this.updateUndoButton();
this.updateRegionBtnLabel();
(this.leaf as any).updateHeader();
this.renderGrid();
}).open(),
);
this.undoBtn = controlsEl.createEl("button", {
cls: "duckmage-undo-btn-map",
text: "↩",
attr: { title: "Undo last terrain stroke (up to 20)" },
});
this.undoBtn.disabled = true;
this.undoBtn.addEventListener("click", () => this.undoLastTerrainStroke());
this.redoBtn = controlsEl.createEl("button", {
cls: "duckmage-undo-btn-map duckmage-redo-btn-map",
text: "↪",
attr: { title: "Redo last undone terrain stroke" },
});
this.redoBtn.disabled = true;
this.redoBtn.addEventListener("click", () => this.redoLastTerrainStroke());
const helpBtn = controlsEl.createEl("button", {
cls: "duckmage-help-btn",
title: "Controls & tools",
@ -577,6 +605,7 @@ export class HexMapView extends ItemView {
private exitTerrainMode(): void {
if (this.drawingMode !== "terrain") return;
this.commitTerrainStroke();
this.drawingMode = null;
this.paintTerrainName = null;
this.terrainPickMode = false;
@ -1259,6 +1288,14 @@ export class HexMapView extends ItemView {
// ── Queue background file write (coalescing per-hex) ────────────────
const path = this.plugin.hexPath(hx, hy, this.activeRegionName);
if (this.currentTerrainStroke) {
if (!this.currentTerrainStroke.has(path)) {
const oldTerrain = this.app.metadataCache.getCache(path)?.frontmatter?.terrain ?? null;
this.currentTerrainStroke.set(path, { x: hx, y: hy, path, oldTerrain, newTerrain: terrain });
} else {
this.currentTerrainStroke.get(path)!.newTerrain = terrain;
}
}
this.scheduleTerrainWrite(hx, hy, path, terrain);
}
}
@ -1380,6 +1417,81 @@ export class HexMapView extends ItemView {
// writes: the in-flight one and then the final value. No writes are lost; no
// stale intermediate value can overwrite a newer one.
private applyTerrainToHexEl(hexEl: HTMLElement, terrain: string | null): void {
const palette = this.plugin.getRegionPalette(this.activeRegionName);
const entry = terrain != null ? palette.find((p) => p.name === terrain) : undefined;
hexEl.style.backgroundColor = entry?.color ?? "";
hexEl.querySelector(".duckmage-hex-icon")?.remove();
hexEl.querySelector(".duckmage-hex-dot")?.remove();
if (entry?.icon) {
try {
const iconEl = createIconEl(hexEl, getIconUrl(this.plugin, entry.icon), entry.name, entry.iconColor, "duckmage-hex-icon");
hexEl.insertBefore(iconEl, hexEl.querySelector(".duckmage-hex-label"));
} catch (err) {
console.warn(`[hexmaker] failed to render icon for terrain "${terrain}":`, err);
}
}
if (terrain !== null) hexEl.addClass("duckmage-hex-exists");
else hexEl.removeClass("duckmage-hex-exists");
}
private commitTerrainStroke(): void {
if (!this.currentTerrainStroke || this.currentTerrainStroke.size === 0) {
this.currentTerrainStroke = null;
return;
}
const entries = [...this.currentTerrainStroke.values()];
this.terrainUndoStack.push(entries);
if (this.terrainUndoStack.length > this.TERRAIN_UNDO_DEPTH)
this.terrainUndoStack.shift();
this.terrainRedoStack = []; // new paint invalidates redo history
this.currentTerrainStroke = null;
this.updateUndoButton();
}
private undoLastTerrainStroke(): void {
const stroke = this.terrainUndoStack.pop();
if (!stroke) return;
this.terrainRedoStack.push(stroke);
this.applyStroke(stroke, "old");
this.updateUndoButton();
}
private redoLastTerrainStroke(): void {
const stroke = this.terrainRedoStack.pop();
if (!stroke) return;
this.terrainUndoStack.push(stroke);
this.applyStroke(stroke, "new");
this.updateUndoButton();
}
private applyStroke(
stroke: { x: number; y: number; path: string; oldTerrain: string | null; newTerrain: string | null }[],
which: "old" | "new",
): void {
for (const entry of stroke) {
const terrain = which === "old" ? entry.oldTerrain : entry.newTerrain;
try {
const hexEl = this.viewportEl?.querySelector<HTMLElement>(
`[data-x="${entry.x}"][data-y="${entry.y}"]`,
);
if (hexEl) this.applyTerrainToHexEl(hexEl, terrain);
} catch (err) {
console.warn(`[hexmaker] stroke visual update failed for ${entry.path}:`, err);
}
try {
this.scheduleTerrainWrite(entry.x, entry.y, entry.path, terrain);
} catch (err) {
console.warn(`[hexmaker] stroke write scheduling failed for ${entry.path}:`, err);
}
}
}
private updateUndoButton(): void {
if (this.undoBtn) this.undoBtn.disabled = this.terrainUndoStack.length === 0;
if (this.redoBtn) this.redoBtn.disabled = this.terrainRedoStack.length === 0;
}
private updateSavingIndicator(): void {
const count =
this.pendingTerrainWrites.size +

View file

@ -147,7 +147,7 @@
content: "";
position: absolute;
inset: 0;
background: rgba(255, 190, 0, 0.55);
background: rgba(255, 210, 0, 0.3);
pointer-events: none;
}
@ -1008,7 +1008,8 @@ div.duckmage-terrain-preview-icon {
}
.duckmage-table-btn,
.duckmage-rt-btn {
.duckmage-rt-btn,
.duckmage-undo-btn-map {
position: absolute;
top: 8px;
z-index: 10;
@ -1035,8 +1036,22 @@ div.duckmage-terrain-preview-icon {
left: 44px;
}
.duckmage-undo-btn-map {
left: 8px;
top: 42px;
}
.duckmage-redo-btn-map {
left: 44px;
top: 42px;
}
.duckmage-undo-btn-map:disabled {
opacity: 0.3;
cursor: default;
}
.duckmage-table-btn:hover,
.duckmage-rt-btn:hover {
.duckmage-rt-btn:hover,
.duckmage-undo-btn-map:not(:disabled):hover {
opacity: 1;
color: var(--text-normal);
}