mirror of
https://github.com/sbuffkin/hexmaker.git
synced 2026-07-22 14:30:24 +00:00
fix: overlay pattern editor — hex preview, palette refresh, thin outline
Three refinements to the 1.3.0 overlay pattern editor based on user feedback: - Preview: was a 120×36 rect at the pattern's own scale. Now a hex polygon at the live on-map hex size (read from .duckmage-hex, falling back to 96 px), in the configured orientation — so the preview shows exactly what the overlay will look like on the map. - Palette tiles: were never updating on Save because the picker re-read style from the metadata cache, which lags Obsidian's frontmatter write by a tick. The editor's onSaved callback now passes the new OverlayStyle directly, so the picker renders from the editor's in-memory state and updates immediately. - Outline width: was hardcoded to gapPx*2+2 (often 14-22 px), which made overlay borders read as solid blobs. Now per-overlay, defaulting to 1.5 px, with a slider (0-20, step 0.5) in the editor. Persisted as faction-outline-width / region-outline-width frontmatter keys. Crank it up to recover the old thick-blob look.
This commit is contained in:
parent
f9e292c5f0
commit
06bb93c91d
7 changed files with 220 additions and 80 deletions
|
|
@ -5,21 +5,25 @@ import {
|
|||
DEFAULT_OVERLAY_PATTERN_KEY,
|
||||
DEFAULT_OVERLAY_PATTERN_SCALE,
|
||||
DEFAULT_OVERLAY_PATTERN_OPACITY,
|
||||
DEFAULT_OVERLAY_OUTLINE_WIDTH,
|
||||
normalizeOverlayPatternKey,
|
||||
normalizeOverlayPatternScale,
|
||||
normalizeOverlayPatternOpacity,
|
||||
normalizeOverlayOutlineWidth,
|
||||
} from "./overlayPatterns";
|
||||
|
||||
export interface OverlayStyle {
|
||||
pattern: OverlayPatternKey;
|
||||
scale: number;
|
||||
opacity: number;
|
||||
outlineWidth: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_OVERLAY_STYLE: OverlayStyle = {
|
||||
pattern: DEFAULT_OVERLAY_PATTERN_KEY,
|
||||
scale: DEFAULT_OVERLAY_PATTERN_SCALE,
|
||||
opacity: DEFAULT_OVERLAY_PATTERN_OPACITY,
|
||||
outlineWidth: DEFAULT_OVERLAY_OUTLINE_WIDTH,
|
||||
};
|
||||
|
||||
export interface Frontmatter {
|
||||
|
|
@ -38,10 +42,12 @@ export interface Frontmatter {
|
|||
"faction-pattern"?: string;
|
||||
"faction-pattern-scale"?: string;
|
||||
"faction-pattern-opacity"?: string;
|
||||
"faction-outline-width"?: string;
|
||||
"region-color"?: string;
|
||||
"region-pattern"?: string;
|
||||
"region-pattern-scale"?: string;
|
||||
"region-pattern-opacity"?: string;
|
||||
"region-outline-width"?: string;
|
||||
}
|
||||
|
||||
export function getFrontMatter(app: App, path: string) {
|
||||
|
|
@ -102,6 +108,7 @@ export function getFactionStyleFromFile(app: App, path: string): OverlayStyle {
|
|||
pattern: normalizeOverlayPatternKey(fm?.["faction-pattern"]),
|
||||
scale: normalizeOverlayPatternScale(fm?.["faction-pattern-scale"]),
|
||||
opacity: normalizeOverlayPatternOpacity(fm?.["faction-pattern-opacity"]),
|
||||
outlineWidth: normalizeOverlayOutlineWidth(fm?.["faction-outline-width"]),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +132,10 @@ export async function setFactionStyleInFile(
|
|||
if (style.opacity === DEFAULT_OVERLAY_PATTERN_OPACITY) delete fm["faction-pattern-opacity"];
|
||||
else fm["faction-pattern-opacity"] = String(style.opacity);
|
||||
}
|
||||
if (style.outlineWidth !== undefined) {
|
||||
if (style.outlineWidth === DEFAULT_OVERLAY_OUTLINE_WIDTH) delete fm["faction-outline-width"];
|
||||
else fm["faction-outline-width"] = String(style.outlineWidth);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
@ -179,6 +190,7 @@ export function getRegionStyleFromFile(app: App, path: string): OverlayStyle {
|
|||
pattern: normalizeOverlayPatternKey(fm?.["region-pattern"]),
|
||||
scale: normalizeOverlayPatternScale(fm?.["region-pattern-scale"]),
|
||||
opacity: normalizeOverlayPatternOpacity(fm?.["region-pattern-opacity"]),
|
||||
outlineWidth: normalizeOverlayOutlineWidth(fm?.["region-outline-width"]),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -202,6 +214,10 @@ export async function setRegionStyleInFile(
|
|||
if (style.opacity === DEFAULT_OVERLAY_PATTERN_OPACITY) delete fm["region-pattern-opacity"];
|
||||
else fm["region-pattern-opacity"] = String(style.opacity);
|
||||
}
|
||||
if (style.outlineWidth !== undefined) {
|
||||
if (style.outlineWidth === DEFAULT_OVERLAY_OUTLINE_WIDTH) delete fm["region-outline-width"];
|
||||
else fm["region-outline-width"] = String(style.outlineWidth);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,28 @@ import {
|
|||
setFactionColorInFile,
|
||||
getFactionStyleFromFile,
|
||||
setFactionStyleInFile,
|
||||
type OverlayStyle,
|
||||
} from "../frontmatter";
|
||||
import { addOverlayPatternControls } from "./overlayPatternControls";
|
||||
import { addOverlayPatternControls, renderHexPreview } from "./overlayPatternControls";
|
||||
|
||||
// ── Faction editor sub-modal ─────────────────────────────────────────────────
|
||||
|
||||
class FactionEditorModal extends HexmakerModal {
|
||||
constructor(
|
||||
app: App,
|
||||
private plugin: HexmakerPlugin,
|
||||
private file: TFile,
|
||||
private initialColor: string | null,
|
||||
/** Called on save with the (possibly updated) color and basename. */
|
||||
private onSaved: (color: string | null, newBasename: string) => void,
|
||||
/**
|
||||
* Called on save with the (possibly updated) color, basename, and full
|
||||
* overlay style. We pass the style directly because the metadata cache
|
||||
* may not yet reflect the just-written frontmatter.
|
||||
*/
|
||||
private onSaved: (
|
||||
color: string | null,
|
||||
newBasename: string,
|
||||
style: OverlayStyle,
|
||||
) => void,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
|
@ -83,6 +93,7 @@ class FactionEditorModal extends HexmakerModal {
|
|||
contentEl,
|
||||
initialStyle,
|
||||
() => pendingColor,
|
||||
this.plugin.settings.hexOrientation,
|
||||
);
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -107,7 +118,7 @@ class FactionEditorModal extends HexmakerModal {
|
|||
// this.file.path is updated in-place by Obsidian after rename
|
||||
await setFactionColorInFile(this.app, this.file.path, pendingColor);
|
||||
await setFactionStyleInFile(this.app, this.file.path, styleState);
|
||||
this.onSaved(pendingColor, newBasename);
|
||||
this.onSaved(pendingColor, newBasename, styleState);
|
||||
this.close();
|
||||
})();
|
||||
});
|
||||
|
|
@ -120,7 +131,7 @@ class FactionEditorModal extends HexmakerModal {
|
|||
clearBtn.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
await setFactionColorInFile(this.app, this.file.path, null);
|
||||
this.onSaved(null, this.file.basename);
|
||||
this.onSaved(null, this.file.basename, styleState);
|
||||
this.close();
|
||||
})();
|
||||
});
|
||||
|
|
@ -190,14 +201,18 @@ export class FactionPickerModal extends HexmakerModal {
|
|||
});
|
||||
}
|
||||
|
||||
const orientation = this.plugin.settings.hexOrientation;
|
||||
const TILE_HEX_PX = 48;
|
||||
|
||||
for (const file of files) {
|
||||
let color = this.pendingColorOverrides.get(file.path) ?? getFactionColorFromFile(this.app, file.path);
|
||||
let style = getFactionStyleFromFile(this.app, file.path);
|
||||
|
||||
const tile = grid.createDiv({ cls: "duckmage-faction-tile" });
|
||||
|
||||
const preview = tile.createDiv({ cls: "duckmage-faction-tile-preview" });
|
||||
if (color) {
|
||||
preview.setCssProps({ "--duckmage-tile-color": color });
|
||||
renderHexPreview(preview, { color, style, orientation, hexSizePx: TILE_HEX_PX });
|
||||
} else {
|
||||
preview.addClass("duckmage-faction-tile-preview-empty");
|
||||
}
|
||||
|
|
@ -219,15 +234,22 @@ export class FactionPickerModal extends HexmakerModal {
|
|||
e.stopPropagation();
|
||||
new FactionEditorModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
file, // TFile mutated in-place by Obsidian on rename
|
||||
color,
|
||||
(newColor, newBasename) => {
|
||||
(newColor, newBasename, newStyle) => {
|
||||
color = newColor;
|
||||
style = newStyle;
|
||||
preview.replaceChildren();
|
||||
if (newColor) {
|
||||
preview.setCssProps({ "--duckmage-tile-color": newColor });
|
||||
preview.removeClass("duckmage-faction-tile-preview-empty");
|
||||
renderHexPreview(preview, {
|
||||
color: newColor,
|
||||
style: newStyle,
|
||||
orientation,
|
||||
hexSizePx: TILE_HEX_PX,
|
||||
});
|
||||
} else {
|
||||
preview.setCssProps({ "--duckmage-tile-color": "" });
|
||||
preview.addClass("duckmage-faction-tile-preview-empty");
|
||||
}
|
||||
nameEl.setText(newBasename);
|
||||
|
|
@ -280,6 +302,7 @@ export class FactionPickerModal extends HexmakerModal {
|
|||
|
||||
new FactionEditorModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
file,
|
||||
null,
|
||||
(newColor) => {
|
||||
|
|
@ -289,6 +312,8 @@ export class FactionPickerModal extends HexmakerModal {
|
|||
).open();
|
||||
}
|
||||
|
||||
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,17 +7,23 @@ import {
|
|||
setRegionColorInFile,
|
||||
getRegionStyleFromFile,
|
||||
setRegionStyleInFile,
|
||||
type OverlayStyle,
|
||||
} from "../frontmatter";
|
||||
import { addOverlayPatternControls } from "./overlayPatternControls";
|
||||
import { addOverlayPatternControls, renderHexPreview } from "./overlayPatternControls";
|
||||
|
||||
// ── Region editor sub-modal ───────────────────────────────────────────────────
|
||||
|
||||
class GeoRegionEditorModal extends HexmakerModal {
|
||||
constructor(
|
||||
app: App,
|
||||
private plugin: HexmakerPlugin,
|
||||
private file: TFile,
|
||||
private initialColor: string | null,
|
||||
private onSaved: (color: string | null, newBasename: string) => void,
|
||||
private onSaved: (
|
||||
color: string | null,
|
||||
newBasename: string,
|
||||
style: OverlayStyle,
|
||||
) => void,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
|
@ -80,6 +86,7 @@ class GeoRegionEditorModal extends HexmakerModal {
|
|||
contentEl,
|
||||
initialStyle,
|
||||
() => pendingColor,
|
||||
this.plugin.settings.hexOrientation,
|
||||
);
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -100,7 +107,7 @@ class GeoRegionEditorModal extends HexmakerModal {
|
|||
}
|
||||
await setRegionColorInFile(this.app, this.file.path, pendingColor);
|
||||
await setRegionStyleInFile(this.app, this.file.path, styleState);
|
||||
this.onSaved(pendingColor, newBasename);
|
||||
this.onSaved(pendingColor, newBasename, styleState);
|
||||
this.close();
|
||||
})();
|
||||
});
|
||||
|
|
@ -113,7 +120,7 @@ class GeoRegionEditorModal extends HexmakerModal {
|
|||
clearBtn.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
await setRegionColorInFile(this.app, this.file.path, null);
|
||||
this.onSaved(null, this.file.basename);
|
||||
this.onSaved(null, this.file.basename, styleState);
|
||||
this.close();
|
||||
})();
|
||||
});
|
||||
|
|
@ -183,14 +190,18 @@ export class GeoRegionPickerModal extends HexmakerModal {
|
|||
});
|
||||
}
|
||||
|
||||
const orientation = this.plugin.settings.hexOrientation;
|
||||
const TILE_HEX_PX = 48;
|
||||
|
||||
for (const file of files) {
|
||||
let color = this.pendingColorOverrides.get(file.path) ?? getRegionColorFromFile(this.app, file.path);
|
||||
let style = getRegionStyleFromFile(this.app, file.path);
|
||||
|
||||
const tile = grid.createDiv({ cls: "duckmage-faction-tile" });
|
||||
|
||||
const preview = tile.createDiv({ cls: "duckmage-faction-tile-preview" });
|
||||
if (color) {
|
||||
preview.setCssProps({ "--duckmage-tile-color": color });
|
||||
renderHexPreview(preview, { color, style, orientation, hexSizePx: TILE_HEX_PX });
|
||||
} else {
|
||||
preview.addClass("duckmage-faction-tile-preview-empty");
|
||||
}
|
||||
|
|
@ -210,15 +221,22 @@ export class GeoRegionPickerModal extends HexmakerModal {
|
|||
e.stopPropagation();
|
||||
new GeoRegionEditorModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
file,
|
||||
color,
|
||||
(newColor, newBasename) => {
|
||||
(newColor, newBasename, newStyle) => {
|
||||
color = newColor;
|
||||
style = newStyle;
|
||||
preview.replaceChildren();
|
||||
if (newColor) {
|
||||
preview.setCssProps({ "--duckmage-tile-color": newColor });
|
||||
preview.removeClass("duckmage-faction-tile-preview-empty");
|
||||
renderHexPreview(preview, {
|
||||
color: newColor,
|
||||
style: newStyle,
|
||||
orientation,
|
||||
hexSizePx: TILE_HEX_PX,
|
||||
});
|
||||
} else {
|
||||
preview.setCssProps({ "--duckmage-tile-color": "" });
|
||||
preview.addClass("duckmage-faction-tile-preview-empty");
|
||||
}
|
||||
nameEl.setText(newBasename);
|
||||
|
|
@ -268,7 +286,7 @@ export class GeoRegionPickerModal extends HexmakerModal {
|
|||
const file = await this.app.vault.create(filePath, "");
|
||||
void this.plugin.ensureRegionTable(name);
|
||||
|
||||
new GeoRegionEditorModal(this.app, file, null, (newColor) => {
|
||||
new GeoRegionEditorModal(this.app, this.plugin, file, null, (newColor) => {
|
||||
if (newColor) this.pendingColorOverrides.set(file.path, newColor);
|
||||
this.buildPalette();
|
||||
}).open();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { HexEditorModal } from "./HexEditorModal";
|
|||
import { TerrainPickerModal } from "./TerrainPickerModal";
|
||||
import { IconPickerModal } from "./IconPickerModal";
|
||||
import { addLinkToSection, getLinksInSection, removeLinkFromSection } from "../sections";
|
||||
import { getFactionColorFromFile, getRegionColorFromFile, getFactionStyleFromFile, getRegionStyleFromFile, setHexRegionInFile, getSubmapFromFile, setSubmapInFile } from "../frontmatter";
|
||||
import { getFactionColorFromFile, getRegionColorFromFile, getFactionStyleFromFile, getRegionStyleFromFile, setHexRegionInFile, getSubmapFromFile, setSubmapInFile, type OverlayStyle } from "../frontmatter";
|
||||
import { buildSvgPattern, colorToIdToken, type OverlayPatternKey } from "../overlayPatterns";
|
||||
import {
|
||||
VIEW_TYPE_HEX_MAP,
|
||||
|
|
@ -3344,7 +3344,7 @@ export class HexMapView extends ItemView {
|
|||
// ── Build faction color + style maps ──────────────────────────────────────
|
||||
const folder = normalizeFolder(this.plugin.settings.factionsFolder);
|
||||
const factionColorMap = new Map<string, string>();
|
||||
const factionStyleMap = new Map<string, { pattern: OverlayPatternKey; scale: number; opacity: number }>();
|
||||
const factionStyleMap = new Map<string, OverlayStyle>();
|
||||
for (const f of this.app.vault.getMarkdownFiles()) {
|
||||
if (folder && !f.path.startsWith(folder + "/")) continue;
|
||||
const color = getFactionColorFromFile(this.app, f.path);
|
||||
|
|
@ -3423,7 +3423,7 @@ export class HexMapView extends ItemView {
|
|||
for (const [factionName, hexKeys] of factionHexKeys) {
|
||||
const color = factionColorMap.get(factionName);
|
||||
if (!color) continue;
|
||||
const style = factionStyleMap.get(factionName) ?? { pattern: "solid" as OverlayPatternKey, scale: 16, opacity: 0.45 };
|
||||
const style = factionStyleMap.get(factionName) ?? { pattern: "solid" as OverlayPatternKey, scale: 16, opacity: 0.45, outlineWidth: 1.5 };
|
||||
const patternId = ensurePattern(style.pattern, color, style.scale);
|
||||
const fillVal = patternId ? `url(#${patternId})` : color;
|
||||
|
||||
|
|
@ -3509,7 +3509,7 @@ export class HexMapView extends ItemView {
|
|||
path.setAttribute("d", d);
|
||||
path.setAttribute("fill", fillVal);
|
||||
path.setAttribute("stroke", color);
|
||||
path.setAttribute("stroke-width", String(gapPx * 2 + 2));
|
||||
path.setAttribute("stroke-width", String(style.outlineWidth));
|
||||
path.setAttribute("stroke-linejoin", "round");
|
||||
path.setAttribute("stroke-linecap", "round");
|
||||
path.setAttribute("paint-order", "stroke fill");
|
||||
|
|
@ -3717,7 +3717,7 @@ export class HexMapView extends ItemView {
|
|||
// ── Region color + style maps ───────────────────────────────────────────
|
||||
const folder = normalizeFolder(this.plugin.settings.regionsFolder);
|
||||
const regionColorMap = new Map<string, string>();
|
||||
const regionStyleMap = new Map<string, { pattern: OverlayPatternKey; scale: number; opacity: number }>();
|
||||
const regionStyleMap = new Map<string, OverlayStyle>();
|
||||
for (const f of this.app.vault.getMarkdownFiles()) {
|
||||
if (f.basename.startsWith("_")) continue;
|
||||
if (folder && !f.path.startsWith(folder + "/")) continue;
|
||||
|
|
@ -3782,7 +3782,7 @@ export class HexMapView extends ItemView {
|
|||
for (const [regionName, hexKeys] of regionHexKeys) {
|
||||
const color = regionColorMap.get(regionName);
|
||||
if (!color) continue;
|
||||
const style = regionStyleMap.get(regionName) ?? { pattern: "solid" as OverlayPatternKey, scale: 16, opacity: 0.45 };
|
||||
const style = regionStyleMap.get(regionName) ?? { pattern: "solid" as OverlayPatternKey, scale: 16, opacity: 0.45, outlineWidth: 1.5 };
|
||||
const patternId = ensureRegionPattern(style.pattern, color, style.scale);
|
||||
const fillVal = patternId ? `url(#${patternId})` : color;
|
||||
|
||||
|
|
@ -3884,9 +3884,8 @@ export class HexMapView extends ItemView {
|
|||
const path = activeDocument.createElementNS(svgNS, "path");
|
||||
path.setAttribute("d", d);
|
||||
path.setAttribute("fill", fillVal);
|
||||
// Stroke bridges the inter-hex gap; round joins/caps smooth blob edges
|
||||
path.setAttribute("stroke", color);
|
||||
path.setAttribute("stroke-width", String(gapPx * 2 + 2));
|
||||
path.setAttribute("stroke-width", String(style.outlineWidth));
|
||||
path.setAttribute("stroke-linejoin", "round");
|
||||
path.setAttribute("stroke-linecap", "round");
|
||||
path.setAttribute("paint-order", "stroke fill");
|
||||
|
|
|
|||
|
|
@ -7,74 +7,124 @@ import {
|
|||
MAX_PATTERN_SCALE,
|
||||
MIN_PATTERN_OPACITY,
|
||||
MAX_PATTERN_OPACITY,
|
||||
MIN_OUTLINE_WIDTH,
|
||||
MAX_OUTLINE_WIDTH,
|
||||
buildSvgPattern,
|
||||
} from "../overlayPatterns";
|
||||
import type { OverlayStyle } from "../frontmatter";
|
||||
import { hexPolygonPoints } from "./hexGeometry";
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const PREVIEW_W = 120;
|
||||
const PREVIEW_H = 36;
|
||||
|
||||
/** Returns the live on-map hex width in CSS pixels; falls back to 96 if no map view is open. */
|
||||
export function getOnMapHexSizePx(): number {
|
||||
const hex = activeDocument.querySelector(".duckmage-hex");
|
||||
if (hex) {
|
||||
const r = hex.getBoundingClientRect();
|
||||
if (r.width > 0) return r.width;
|
||||
}
|
||||
return 96;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a pattern dropdown + scale + opacity sliders + live preview to `containerEl`.
|
||||
* Returns the live state object; callers read this on Save and write to frontmatter.
|
||||
* Render a hex-shaped preview into `container` showing what the overlay will
|
||||
* look like on the map. Replaces any existing children.
|
||||
*/
|
||||
export function renderHexPreview(
|
||||
container: HTMLElement,
|
||||
opts: {
|
||||
color: string;
|
||||
style: OverlayStyle;
|
||||
orientation: "flat" | "pointy";
|
||||
hexSizePx: number;
|
||||
},
|
||||
): void {
|
||||
container.replaceChildren();
|
||||
const { color, style, orientation, hexSizePx } = opts;
|
||||
|
||||
const W = hexSizePx;
|
||||
let radius: number;
|
||||
let H: number;
|
||||
if (orientation === "flat") {
|
||||
radius = W / 2;
|
||||
H = W * Math.sqrt(3) / 2;
|
||||
} else {
|
||||
radius = W / Math.sqrt(3);
|
||||
H = 2 * radius;
|
||||
}
|
||||
|
||||
const PAD = 3;
|
||||
const svgW = W + 2 * PAD;
|
||||
const svgH = H + 2 * PAD;
|
||||
const cx = svgW / 2;
|
||||
const cy = svgH / 2;
|
||||
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("width", String(svgW));
|
||||
svg.setAttribute("height", String(svgH));
|
||||
svg.classList.add("duckmage-hex-preview-svg");
|
||||
|
||||
// Pattern def (only if non-solid)
|
||||
let fillVal: string = color;
|
||||
if (style.pattern !== "solid") {
|
||||
const defs = document.createElementNS(SVG_NS, "defs");
|
||||
const id = `dm-prev-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const pat = buildSvgPattern(document, {
|
||||
id,
|
||||
pattern: style.pattern,
|
||||
color,
|
||||
scale: style.scale,
|
||||
});
|
||||
if (pat) {
|
||||
defs.appendChild(pat);
|
||||
svg.appendChild(defs);
|
||||
fillVal = `url(#${id})`;
|
||||
}
|
||||
}
|
||||
|
||||
const pts = hexPolygonPoints(cx, cy, orientation, radius - 1);
|
||||
const poly = document.createElementNS(SVG_NS, "polygon");
|
||||
poly.setAttribute("points", pts.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" "));
|
||||
poly.setAttribute("fill", fillVal);
|
||||
poly.setAttribute("stroke", color);
|
||||
poly.setAttribute("stroke-width", String(style.outlineWidth));
|
||||
poly.setAttribute("opacity", String(style.opacity));
|
||||
svg.appendChild(poly);
|
||||
|
||||
container.appendChild(svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a pattern dropdown + scale + opacity sliders + hex-shaped live preview
|
||||
* (rendered at the actual on-map hex size) to `containerEl`. Returns the live
|
||||
* state object; callers read it on Save and write to frontmatter.
|
||||
*/
|
||||
export function addOverlayPatternControls(
|
||||
containerEl: HTMLElement,
|
||||
initial: OverlayStyle,
|
||||
getColor: () => string,
|
||||
orientation: "flat" | "pointy",
|
||||
): { state: OverlayStyle; refreshPreview: () => void } {
|
||||
const state: OverlayStyle = { ...initial };
|
||||
|
||||
// ── Live preview ──────────────────────────────────────────────────────────
|
||||
const previewWrap = containerEl.createDiv({ cls: "duckmage-pattern-preview-wrap" });
|
||||
previewWrap.createSpan({
|
||||
text: "Preview",
|
||||
cls: "duckmage-pattern-preview-label",
|
||||
});
|
||||
const previewSvg = document.createElementNS(SVG_NS, "svg");
|
||||
previewSvg.setAttribute("width", String(PREVIEW_W));
|
||||
previewSvg.setAttribute("height", String(PREVIEW_H));
|
||||
previewSvg.classList.add("duckmage-pattern-preview-svg");
|
||||
previewWrap.appendChild(previewSvg);
|
||||
const previewHost = previewWrap.createDiv({ cls: "duckmage-pattern-preview-host" });
|
||||
|
||||
const hexSizePx = getOnMapHexSizePx();
|
||||
const refreshPreview = () => {
|
||||
previewSvg.replaceChildren();
|
||||
const color = getColor();
|
||||
if (state.pattern === "solid") {
|
||||
const rect = document.createElementNS(SVG_NS, "rect");
|
||||
rect.setAttribute("x", "0");
|
||||
rect.setAttribute("y", "0");
|
||||
rect.setAttribute("width", String(PREVIEW_W));
|
||||
rect.setAttribute("height", String(PREVIEW_H));
|
||||
rect.setAttribute("fill", color);
|
||||
rect.setAttribute("opacity", String(state.opacity));
|
||||
previewSvg.appendChild(rect);
|
||||
return;
|
||||
}
|
||||
const defs = document.createElementNS(SVG_NS, "defs");
|
||||
const id = `dm-preview-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const pat = buildSvgPattern(document, {
|
||||
id,
|
||||
pattern: state.pattern,
|
||||
color,
|
||||
scale: state.scale,
|
||||
renderHexPreview(previewHost, {
|
||||
color: getColor(),
|
||||
style: state,
|
||||
orientation,
|
||||
hexSizePx,
|
||||
});
|
||||
if (pat) defs.appendChild(pat);
|
||||
previewSvg.appendChild(defs);
|
||||
const bg = document.createElementNS(SVG_NS, "rect");
|
||||
bg.setAttribute("x", "0");
|
||||
bg.setAttribute("y", "0");
|
||||
bg.setAttribute("width", String(PREVIEW_W));
|
||||
bg.setAttribute("height", String(PREVIEW_H));
|
||||
bg.setAttribute("fill", `url(#${id})`);
|
||||
bg.setAttribute("opacity", String(state.opacity));
|
||||
previewSvg.appendChild(bg);
|
||||
};
|
||||
|
||||
refreshPreview();
|
||||
|
||||
// ── Pattern dropdown ──────────────────────────────────────────────────────
|
||||
new Setting(containerEl).setName("Pattern").addDropdown((dd) => {
|
||||
for (const key of OVERLAY_PATTERN_KEYS) {
|
||||
dd.addOption(key, OVERLAY_PATTERN_LABELS[key]);
|
||||
|
|
@ -85,7 +135,6 @@ export function addOverlayPatternControls(
|
|||
});
|
||||
});
|
||||
|
||||
// ── Scale slider ──────────────────────────────────────────────────────────
|
||||
new Setting(containerEl).setName("Pattern scale").addSlider((sl) =>
|
||||
sl
|
||||
.setLimits(MIN_PATTERN_SCALE, MAX_PATTERN_SCALE, 1)
|
||||
|
|
@ -97,7 +146,6 @@ export function addOverlayPatternControls(
|
|||
}),
|
||||
);
|
||||
|
||||
// ── Opacity slider ────────────────────────────────────────────────────────
|
||||
new Setting(containerEl).setName("Opacity").addSlider((sl) =>
|
||||
sl
|
||||
.setLimits(MIN_PATTERN_OPACITY, MAX_PATTERN_OPACITY, 0.05)
|
||||
|
|
@ -109,5 +157,16 @@ export function addOverlayPatternControls(
|
|||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl).setName("Outline width").addSlider((sl) =>
|
||||
sl
|
||||
.setLimits(MIN_OUTLINE_WIDTH, MAX_OUTLINE_WIDTH, 0.5)
|
||||
.setValue(state.outlineWidth)
|
||||
.setDynamicTooltip()
|
||||
.onChange((v) => {
|
||||
state.outlineWidth = v;
|
||||
refreshPreview();
|
||||
}),
|
||||
);
|
||||
|
||||
return { state, refreshPreview };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,11 +40,14 @@ export const OVERLAY_PATTERN_LABELS: Record<OverlayPatternKey, string> = {
|
|||
export const DEFAULT_OVERLAY_PATTERN_KEY: OverlayPatternKey = "solid";
|
||||
export const DEFAULT_OVERLAY_PATTERN_SCALE = 16;
|
||||
export const DEFAULT_OVERLAY_PATTERN_OPACITY = 0.45;
|
||||
export const DEFAULT_OVERLAY_OUTLINE_WIDTH = 1.5;
|
||||
|
||||
export const MIN_PATTERN_SCALE = 8;
|
||||
export const MAX_PATTERN_SCALE = 48;
|
||||
export const MIN_PATTERN_OPACITY = 0.05;
|
||||
export const MAX_PATTERN_OPACITY = 1.0;
|
||||
export const MIN_OUTLINE_WIDTH = 0;
|
||||
export const MAX_OUTLINE_WIDTH = 20;
|
||||
|
||||
export function isOverlayPatternKey(v: unknown): v is OverlayPatternKey {
|
||||
return (
|
||||
|
|
@ -79,6 +82,17 @@ export function normalizeOverlayPatternOpacity(v: unknown): number {
|
|||
return Math.min(MAX_PATTERN_OPACITY, Math.max(MIN_PATTERN_OPACITY, n));
|
||||
}
|
||||
|
||||
export function normalizeOverlayOutlineWidth(v: unknown): number {
|
||||
const n =
|
||||
typeof v === "number"
|
||||
? v
|
||||
: typeof v === "string"
|
||||
? parseFloat(v)
|
||||
: NaN;
|
||||
if (!Number.isFinite(n)) return DEFAULT_OVERLAY_OUTLINE_WIDTH;
|
||||
return Math.min(MAX_OUTLINE_WIDTH, Math.max(MIN_OUTLINE_WIDTH, n));
|
||||
}
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/**
|
||||
|
|
|
|||
29
styles.css
29
styles.css
|
|
@ -3718,11 +3718,16 @@ tr:hover .duckmage-rt-entry-copy-btn:hover {
|
|||
}
|
||||
|
||||
.duckmage-faction-tile-preview {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--duckmage-tile-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.duckmage-faction-tile-preview svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Empty / no-color state: diagonal stripe */
|
||||
|
|
@ -3833,9 +3838,9 @@ tr:hover .duckmage-rt-entry-copy-btn:hover {
|
|||
.duckmage-pattern-preview-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 10px 0 4px;
|
||||
padding: 6px 10px;
|
||||
gap: 14px;
|
||||
margin: 10px 0 6px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
|
|
@ -3846,10 +3851,14 @@ tr:hover .duckmage-rt-entry-copy-btn:hover {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
.duckmage-pattern-preview-svg {
|
||||
.duckmage-pattern-preview-host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.duckmage-hex-preview-svg {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
background: var(--background-primary);
|
||||
}
|
||||
|
||||
/* Region overlay SVG */
|
||||
|
|
|
|||
Loading…
Reference in a new issue