From 84bcd76bd5d7ea29e6ec8eec66b1f3f064596fa8 Mon Sep 17 00:00:00 2001 From: "isaprettycoolguy@protonmail.com" <6576516+nyxsys@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:33:00 -0400 Subject: [PATCH] fix: legend swatch shows pattern at readable density (1.3.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the faction-legend swatch and shrinks the pattern tile so the pattern is legible at swatch-size rather than showing just 1-2 tile repeats. - On-screen legend: hex 28 → 32 px, pattern tile scaled by swatch/96 with a 4 px floor — yields ~6 repeats across the swatch. - PNG legend: swatch fontSize*1.1 → fontSize*1.7, lineHeight floored at swatch+4 so rows don't overlap; pattern shrunk by the same ratio. - renderHexPreview gains an optional patternScaleMultiplier param so callers (legend, palette, future small-swatch contexts) can shrink the pattern without touching the user's scale slider value. Also bundles the prior 1.3.0 follow-up fixes (hex-shaped on-map-scale preview, palette tile refresh-on-save, thin outline default + slider) since those were committed locally on top of 1.3.0. --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/export/mapPngRenderer.ts | 64 ++++++++++++++++++++++----- src/hex-map/HexMapView.ts | 22 ++++++--- src/hex-map/overlayPatternControls.ts | 13 +++++- styles.css | 11 +++-- versions.json | 3 +- 8 files changed, 94 insertions(+), 27 deletions(-) diff --git a/manifest.json b/manifest.json index 4d38ed9..7081b04 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "hexmaker", "name": "Hexmap World Creator", - "version": "1.3.0", + "version": "1.3.1", "minAppVersion": "1.12.0", "description": "Create a hex map and a guidebook level wiki for your setting with minimal fuss. Hex crawl worldbuilding toolbox. System agnostic, ready for any TRPG you care to run in it.", "author": "morkdev", diff --git a/package-lock.json b/package-lock.json index b0c15f0..4027ddb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hexmaker-plugin", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hexmaker-plugin", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "minisearch": "^7.2.0" diff --git a/package.json b/package.json index e0997e9..b298e20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexmaker-plugin", - "version": "1.3.0", + "version": "1.3.1", "description": "A plugin to power hex crawl worldbuilding and running.", "main": "main.js", "scripts": { diff --git a/src/export/mapPngRenderer.ts b/src/export/mapPngRenderer.ts index ba227b4..388a3b0 100644 --- a/src/export/mapPngRenderer.ts +++ b/src/export/mapPngRenderer.ts @@ -176,7 +176,8 @@ export async function renderMapToPngBlob( } const hexes: HexState[] = []; const iconsNeeded = new Set(); - const factionsUsed = new Map(); // name → color + interface LegendEntry { color: string; style: OverlayStyle } + const factionsUsed = new Map(); // name → entry const regionsUsed = new Map(); // name → color // Per-region hex centres, used to position the on-map region label at the // cluster centroid (with PCA-derived rotation). Same approach as the @@ -206,7 +207,7 @@ export async function renderMapToPngBlob( const s = factionStyleMap.get(fName); if (c && s) { factions.push({ color: c, style: s }); - factionsUsed.set(fName, c); + factionsUsed.set(fName, { color: c, style: s }); } } } @@ -340,6 +341,7 @@ export async function renderMapToPngBlob( fontSize, legendPadding, true, // measure pass first to anchor from the bottom + orientation, ); drawLegend( ctx, @@ -350,6 +352,7 @@ export async function renderMapToPngBlob( fontSize, legendPadding, false, + orientation, ); } @@ -483,7 +486,7 @@ function drawRegionLabel( ctx.restore(); } -function sortedEntries(m: Map): Array<[string, string]> { +function sortedEntries(m: Map): Array<[string, V]> { return Array.from(m.entries()).sort((a, b) => a[0].localeCompare(b[0], undefined, { sensitivity: "base" }), ); @@ -499,15 +502,16 @@ function drawLegend( x: number, y: number, title: string, - entries: Array<[string, string]>, + entries: Array<[string, { color: string; style: OverlayStyle }]>, fontSize: number, pad: number, measureOnly: boolean, + orientation: "flat" | "pointy" = "flat", ): { width: number; height: number } { if (entries.length === 0) return { width: 0, height: 0 }; - const lineHeight = Math.round(fontSize * 1.45); - const swatch = Math.round(fontSize * 1.1); + const swatch = Math.round(fontSize * 1.7); + const lineHeight = Math.max(Math.round(fontSize * 1.45), swatch + 4); const gap = Math.round(fontSize * 0.5); // Measure widths. We need to set the font for accurate measurements. @@ -550,13 +554,49 @@ function drawLegend( // Entry rows. ctx.font = `${fontSize}px sans-serif`; let entryY = y + pad + lineHeight; - for (const [name, color] of entries) { - // Swatch — vertically centred on the text baseline. + // Shrink pattern tile so more repeats are visible in the small swatch + // (user's scale slider is sized for on-map hexes ~96 px wide). + const LEGEND_PATTERN_MULT = swatch / 96; + const MIN_EFFECTIVE_SCALE = 4; + for (const [name, entry] of entries) { + const { color, style } = entry; const swatchY = entryY + Math.round((lineHeight - swatch) / 2); - ctx.fillStyle = color; - ctx.fillRect(x + pad, swatchY, swatch, swatch); - ctx.strokeStyle = "#888"; - ctx.strokeRect(x + pad + 0.5, swatchY + 0.5, swatch - 1, swatch - 1); + // Build pattern fill if non-solid + let fill: string | CanvasPattern = color; + if (style.pattern !== "solid") { + const effectiveScale = Math.max( + MIN_EFFECTIVE_SCALE, + style.scale * LEGEND_PATTERN_MULT, + ); + const tile = new OffscreenCanvas(effectiveScale, effectiveScale); + const tctx = tile.getContext("2d"); + if (tctx) { + drawPatternTile(tctx, { + pattern: style.pattern, + color, + scale: effectiveScale, + }); + const pat = ctx.createPattern(tile, "repeat"); + if (pat) fill = pat; + } + } + // Hex polygon swatch + const cx = x + pad + swatch / 2; + const cy = swatchY + swatch / 2; + const r = swatch / 2 - 1; + const pts = hexPolygonPoints(cx, cy, orientation, r); + ctx.beginPath(); + ctx.moveTo(pts[0].x, pts[0].y); + for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y); + ctx.closePath(); + const prevAlpha = ctx.globalAlpha; + ctx.globalAlpha = style.opacity; + ctx.fillStyle = fill; + ctx.fill(); + ctx.globalAlpha = prevAlpha; + ctx.strokeStyle = color; + ctx.lineWidth = 1; + ctx.stroke(); ctx.fillStyle = "#fff"; ctx.fillText(name, x + pad + swatch + gap, entryY); diff --git a/src/hex-map/HexMapView.ts b/src/hex-map/HexMapView.ts index 0a7acf6..ef8124a 100644 --- a/src/hex-map/HexMapView.ts +++ b/src/hex-map/HexMapView.ts @@ -16,6 +16,7 @@ import { IconPickerModal } from "./IconPickerModal"; import { addLinkToSection, getLinksInSection, removeLinkFromSection } from "../sections"; import { getFactionColorFromFile, getRegionColorFromFile, getFactionStyleFromFile, getRegionStyleFromFile, setHexRegionInFile, getSubmapFromFile, setSubmapInFile, type OverlayStyle } from "../frontmatter"; import { buildSvgPattern, colorToIdToken, type OverlayPatternKey } from "../overlayPatterns"; +import { renderHexPreview } from "./overlayPatternControls"; import { VIEW_TYPE_HEX_MAP, VIEW_TYPE_HEX_TABLE, @@ -3396,7 +3397,7 @@ export class HexMapView extends ItemView { return k1 < k2 ? `${k1}|${k2}` : `${k2}|${k1}`; }; - const activeFactions: { name: string; color: string }[] = []; + const activeFactions: { name: string; color: string; style: OverlayStyle }[] = []; let hasElements = false; // ── Shared for SVG patterns (deduped by key|color|scale) ─────────── @@ -3516,7 +3517,7 @@ export class HexMapView extends ItemView { g.appendChild(path); } - activeFactions.push({ name: factionName, color }); + activeFactions.push({ name: factionName, color, style }); hasElements = true; } @@ -3534,7 +3535,7 @@ export class HexMapView extends ItemView { } private renderFactionLegend( - factions: { name: string; color: string }[], + factions: { name: string; color: string; style: OverlayStyle }[], gridContainer: HTMLElement, ): void { if (!this.viewportEl) return; @@ -3543,10 +3544,21 @@ export class HexMapView extends ItemView { const gridRight = gridContainer.offsetLeft + gridContainer.offsetWidth; legend.style.left = `${gridRight + 24}px`; legend.style.top = `${gridContainer.offsetTop}px`; - for (const { name, color } of [...factions].sort((a, b) => a.name.localeCompare(b.name))) { + const orientation = this.plugin.settings.hexOrientation; + const LEGEND_HEX_PX = 32; + // Shrink the pattern so more repeats fit in the small swatch (the user's + // scale slider is sized for on-map hexes, ~96 px wide). + const LEGEND_PATTERN_MULT = LEGEND_HEX_PX / 96; + for (const { name, color, style } of [...factions].sort((a, b) => a.name.localeCompare(b.name))) { const row = legend.createDiv({ cls: "duckmage-faction-legend-row" }); const swatch = row.createDiv({ cls: "duckmage-faction-legend-swatch" }); - swatch.style.backgroundColor = color; + renderHexPreview(swatch, { + color, + style, + orientation, + hexSizePx: LEGEND_HEX_PX, + patternScaleMultiplier: LEGEND_PATTERN_MULT, + }); row.createSpan({ text: name, cls: "duckmage-faction-legend-name" }); } } diff --git a/src/hex-map/overlayPatternControls.ts b/src/hex-map/overlayPatternControls.ts index c4f4fa0..47cc55b 100644 --- a/src/hex-map/overlayPatternControls.ts +++ b/src/hex-map/overlayPatternControls.ts @@ -29,6 +29,11 @@ export function getOnMapHexSizePx(): number { /** * Render a hex-shaped preview into `container` showing what the overlay will * look like on the map. Replaces any existing children. + * + * `patternScaleMultiplier` shrinks the pattern tile so more repeats fit in a + * small swatch. Useful for the legend/palette where the swatch is much + * smaller than an on-map hex and the user-chosen scale would otherwise show + * just 1-2 tile repeats. */ export function renderHexPreview( container: HTMLElement, @@ -37,10 +42,16 @@ export function renderHexPreview( style: OverlayStyle; orientation: "flat" | "pointy"; hexSizePx: number; + patternScaleMultiplier?: number; }, ): void { container.replaceChildren(); const { color, style, orientation, hexSizePx } = opts; + const MIN_EFFECTIVE_SCALE = 4; + const effectiveScale = Math.max( + MIN_EFFECTIVE_SCALE, + style.scale * (opts.patternScaleMultiplier ?? 1), + ); const W = hexSizePx; let radius: number; @@ -73,7 +84,7 @@ export function renderHexPreview( id, pattern: style.pattern, color, - scale: style.scale, + scale: effectiveScale, }); if (pat) { defs.appendChild(pat); diff --git a/styles.css b/styles.css index 0da6737..73001ee 100644 --- a/styles.css +++ b/styles.css @@ -3902,13 +3902,16 @@ tr:hover .duckmage-rt-entry-copy-btn:hover { } .duckmage-faction-legend-swatch { - width: 12px; - height: 12px; - border-radius: 3px; - border: 1px solid var(--background-modifier-border); + display: flex; + align-items: center; + justify-content: center; flex-shrink: 0; } +.duckmage-faction-legend-swatch svg { + display: block; +} + .duckmage-faction-legend-name { font-size: 0.8em; color: var(--text-normal); diff --git a/versions.json b/versions.json index a4a5b31..832ce27 100644 --- a/versions.json +++ b/versions.json @@ -39,5 +39,6 @@ "1.2.1": "1.12.0", "1.2.2": "1.12.0", "1.2.3": "1.12.0", - "1.3.0": "1.12.0" + "1.3.0": "1.12.0", + "1.3.1": "1.12.0" } \ No newline at end of file