From 06bb93c91df875bd81841ad7f2a9786d20c9d4e4 Mon Sep 17 00:00:00 2001 From: "isaprettycoolguy@protonmail.com" <6576516+nyxsys@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:22:28 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20overlay=20pattern=20editor=20=E2=80=94?= =?UTF-8?q?=20hex=20preview,=20palette=20refresh,=20thin=20outline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/frontmatter.ts | 16 +++ src/hex-map/FactionPickerModal.ts | 43 ++++++-- src/hex-map/GeoRegionPickerModal.ts | 36 +++++-- src/hex-map/HexMapView.ts | 15 ++- src/hex-map/overlayPatternControls.ts | 147 ++++++++++++++++++-------- src/overlayPatterns.ts | 14 +++ styles.css | 29 +++-- 7 files changed, 220 insertions(+), 80 deletions(-) diff --git a/src/frontmatter.ts b/src/frontmatter.ts index 933364c..7dcf4e1 100644 --- a/src/frontmatter.ts +++ b/src/frontmatter.ts @@ -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; } diff --git a/src/hex-map/FactionPickerModal.ts b/src/hex-map/FactionPickerModal.ts index b4f90ff..9263f92 100644 --- a/src/hex-map/FactionPickerModal.ts +++ b/src/hex-map/FactionPickerModal.ts @@ -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(); } diff --git a/src/hex-map/GeoRegionPickerModal.ts b/src/hex-map/GeoRegionPickerModal.ts index ef8370c..19d1e2f 100644 --- a/src/hex-map/GeoRegionPickerModal.ts +++ b/src/hex-map/GeoRegionPickerModal.ts @@ -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(); diff --git a/src/hex-map/HexMapView.ts b/src/hex-map/HexMapView.ts index 2c0ac22..0a7acf6 100644 --- a/src/hex-map/HexMapView.ts +++ b/src/hex-map/HexMapView.ts @@ -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(); - const factionStyleMap = new Map(); + const factionStyleMap = new Map(); 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(); - const regionStyleMap = new Map(); + const regionStyleMap = new Map(); 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"); diff --git a/src/hex-map/overlayPatternControls.ts b/src/hex-map/overlayPatternControls.ts index f3380c8..c4f4fa0 100644 --- a/src/hex-map/overlayPatternControls.ts +++ b/src/hex-map/overlayPatternControls.ts @@ -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 }; } diff --git a/src/overlayPatterns.ts b/src/overlayPatterns.ts index 52ba190..6bf0573 100644 --- a/src/overlayPatterns.ts +++ b/src/overlayPatterns.ts @@ -40,11 +40,14 @@ export const OVERLAY_PATTERN_LABELS: Record = { 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"; /** diff --git a/styles.css b/styles.css index 10e1ee3..0da6737 100644 --- a/styles.css +++ b/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 */