From fdf0b36eb4b93806882fc0e08189a5eeb30e81b7 Mon Sep 17 00:00:00 2001 From: Daniel Fiuk Date: Tue, 14 Apr 2026 11:36:59 -0600 Subject: [PATCH] Final Touches to 1.0.0 --- src/assets/Pin.svg | 1 + src/assets/mapPinIcon_customizable.svg | 1 - src/main.ts | 4 +- src/paramaters.ts | 39 +++++++++++- src/pinInteractions.ts | 84 +++++++++++++++++++++++--- src/settings.ts | 81 +++++++++++++++++++++++-- styles.css | 4 +- 7 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 src/assets/Pin.svg delete mode 100644 src/assets/mapPinIcon_customizable.svg diff --git a/src/assets/Pin.svg b/src/assets/Pin.svg new file mode 100644 index 0000000..a2df2b0 --- /dev/null +++ b/src/assets/Pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/mapPinIcon_customizable.svg b/src/assets/mapPinIcon_customizable.svg deleted file mode 100644 index 0fde2f7..0000000 --- a/src/assets/mapPinIcon_customizable.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main.ts b/src/main.ts index 09ccc6a..40cea91 100644 --- a/src/main.ts +++ b/src/main.ts @@ -95,7 +95,7 @@ export default class FantasyMap extends Plugin { // load image to infer aspect and tile layout const mapImg = new Image(); mapImg.src = mapUrl; - mapImg.onload = () => { + mapImg.onload = async () => { const ratio = mapImg.naturalHeight / mapImg.naturalWidth; (mapWrapper as any)._mapAspectRatio = ratio; @@ -156,7 +156,7 @@ export default class FantasyMap extends Plugin { const zoomInput = toolbar.createEl("input", { cls: "fm-zoom-step", type: "number", - }) as HTMLInputElement; + }); zoomInput.value = String(parameters.defaultZoomIncrement ?? this.settings.defaultZoomIncrement); //#endregion } diff --git a/src/paramaters.ts b/src/paramaters.ts index 8568ffc..a3b40af 100644 --- a/src/paramaters.ts +++ b/src/paramaters.ts @@ -5,6 +5,7 @@ import { FantasyMapSettings } from "./settings"; export interface FantasyMapParams { map: string; mapIDs: string[]; + pinSize: string; defaultZoomIncrement?: number; defaultZoomLevel?: number; defaultLocation?: [number, number]; @@ -19,6 +20,7 @@ export interface FantasyMapParams { // Help message strings for each parameter, which will be displayed when the user includes a help keyword in the value of any parameter; these messages provide detailed instructions on how to use each parameter and what values are accepted const mapHelpMessageString = "**\"Map\"** (**required**): The name of the map asset to display - supports relative paths, absolute paths, and bare filenames (e.g. \"World Map.svg\") - the file must be located somewhere in your vault - supported file types are SVG, PNG, JPG, JPEG, WEBP, and GIF" const mapIDsHelpMessageString = "**\"Map IDs\"** (optional): A comma-separated list of IDs that define which pins with the same ID can appear on the map (e.g. \"river, mountain\"); if not specified, all pins will be displayed on the map regardless of their ID"; +const pinSizeHelpMessageString = "**\"Pin Size\"** (optional): The size of the pins on the map. Specificaly the width of the pin element, (e.g. \"24px\", \"1.5em\", etc.) – default: \"24px\", can be adjusted in the plugin settings window"; const defaultZoomIncrementHelpMessageString = "**\"Default Zoom Increment\"** (optional): The zoom increment amount – default: \"1\", can be adjusted in the plugin settings window"; const defaultZoomLevelHelpMessageString = "**\"Default Zoom Level\"** (optional): The initial zoom level when the map is first loaded – default: \"1\""; const defaultLocationHelpMessageString = "**\"Default Location\"** (optional): The initial focused coordinates for the map on load \"(latitude, longitude)\" – default: \"(0, 0)\""; @@ -48,7 +50,7 @@ function allHelpMessages(settings: FantasyMapSettings) : string[] { } // This string provides a template for users to copy and paste into their notes to create a new Fantasy-Map code block with all available parameters listed for easy reference -export const fantasyMapCopyToClipboardString = [ +export const fantasyMapCodeBlockCopyToClipboardString = [ "```Fantasy-Map", "Map:", "MapIDs:", @@ -64,6 +66,14 @@ export const fantasyMapCopyToClipboardString = [ "```" ].join("\n"); +export const fantasyMapFrontMatterCopyToClipboardString = [ + "---", + "fm-location: (lat, lng)", + "fm-id:", + "fm-pin-icon:", + "---" +].join("\n"); + // Comprehensive list of affirmative values that will be interpreted as true for boolean parameters like "repeat"; this allows for a wide range of user inputs to enable the feature without requiring strict formatting const affirmatives = [ "yes", "y", @@ -168,7 +178,7 @@ export function parseFantasyMapParams(source: string, element: HTMLElement, ctx: // help message for the map parameter if the value contains any help keywords if (checkForHelp(mapHelpMessageString)) break; - if (value.trim().length == 0) addHelpMessage("Fantasy Map ERROR: map option is required!") ; + if (value.trim().length == 0) addHelpMessage("Fantasy Map ERROR: map option is required!"); // set the name of the map to load; supports relative paths, absolute paths, and bare filenames (e.g. "World Map.svg") - the file must be located somewhere in your vault - supported file types are SVG, PNG, JPG, JPEG, WEBP, and GIF params.map = value; @@ -183,6 +193,15 @@ export function parseFantasyMapParams(source: string, element: HTMLElement, ctx: params.mapIDs = value.split(",").map(id => id.trim()); break; + case "pinsize": + if (checkForHelp(pinSizeHelpMessageString)) break; + + const trimmed = value.trim(); + if (!isValidPinSize(trimmed)) addHelpMessage("Fantasy Map ERROR: Pin Size Value is invalid! Some examples of acceptable values are '24px', '5%'. This option utalizes css styles, specificaly the width property. ") + + params.pinSize = value; + break; + // set the default zoom increment, defaulting to 1 if the value is not a valid number case "defaultzoomincrement": @@ -297,6 +316,7 @@ export function parseFantasyMapParams(source: string, element: HTMLElement, ctx: } // Defaults + if (params.pinSize == null) params.pinSize = settings.defaultPinSize; if (params.defaultZoomIncrement == null) params.defaultZoomIncrement = settings.defaultZoomIncrement; if (params.defaultZoomLevel == null) params.defaultZoomLevel = 1; if (params.defaultLocation == null) params.defaultLocation = [0, 0]; @@ -312,6 +332,19 @@ export function parseFantasyMapParams(source: string, element: HTMLElement, ctx: return params as FantasyMapParams; } +function isValidPinSize(value: string): boolean { + const trimmed = value.trim(); + if (!trimmed) return false; + + const el = document.createElement("div"); + el.style.width = ""; + el.style.width = trimmed; + + const result = el.style.width !== ""; + el.remove(); + return result; +} + // Render a help message to markdown export async function fantasyMapHelpMessage(element: HTMLElement, ctx: MarkdownPostProcessorContext, component: Component, message: string | null, includeCopyLink: boolean = false) { @@ -330,7 +363,7 @@ export function appendCopyLink(container: HTMLElement) { link.addEventListener("click", async (event) => { event.preventDefault(); try { - await navigator.clipboard.writeText(fantasyMapCopyToClipboardString); + await navigator.clipboard.writeText(fantasyMapCodeBlockCopyToClipboardString); link.setText("Copied!"); setTimeout(() => link.setText("Copy Fantasy-Map template"), 1500); } catch (err) { diff --git a/src/pinInteractions.ts b/src/pinInteractions.ts index 26742b5..df4e3e7 100644 --- a/src/pinInteractions.ts +++ b/src/pinInteractions.ts @@ -1,10 +1,10 @@ -import {App, CachedMetadata, MarkdownPostProcessorContext, MarkdownRenderer, Plugin, TFile, Component } from "obsidian"; +import {App, CachedMetadata, MarkdownPostProcessorContext, MarkdownRenderer, Plugin, normalizePath, TFile, Component } from "obsidian"; import { FantasyMapSettings } from "./settings"; import { FantasyMapParams } from "./paramaters"; import { cancelMapPanning } from "./mapInteractions"; import { showCustomPreview, hideCustomPreview, destroyCustomPreview } from "./previewInteractions" -import pinIcon from "./assets/mapPinIcon_customizable.svg"; +import defaultPinIcon from "./assets/Pin.svg"; export interface Pin { note: TFile; @@ -50,7 +50,7 @@ export async function initPinInteractions( createPin(note); } - function createPin(note: TFile){ + async function createPin(note: TFile){ //#region parse and filter note front matter // get the note front matter; if no front matter detected, skip the note @@ -58,14 +58,14 @@ export async function initPinInteractions( const frontMatter = cache?.frontmatter; if (!frontMatter) return; - // if mapIDs parameter is set, only include notes with a matching fantasy-map-id in their front matter + // if mapIDs parameter is set, only include notes with a matching fm-id in their front matter if (paramaters.mapIDs.length > 0 && paramaters.mapIDs[0] !== "") { - const mapId = frontMatter["fantasy-map-id"]; + const mapId = frontMatter["fm-id"]; if (mapId !== undefined && !paramaters.mapIDs.includes(mapId)) return; } // get location from front matter and parse it; if no location or invalid format, skip the note - const frontMatterLc = frontMatter["fantasy-map-location"]; + const frontMatterLc = frontMatter["fm-location"]; if (frontMatterLc === undefined) return; const location = parseFormattedLocation(frontMatterLc); @@ -88,8 +88,26 @@ export async function initPinInteractions( // get the pin icon and add it to the pin element; set the width of the icon to 12px const pinIconEl = element.createEl("div", { cls: "map-pin-icon" }); - pinIconEl.innerHTML = pinIcon; - pinIconEl.style.width = `12px`; + + const pinIconValue = frontMatter["fm-pin-icon"]; + const pinFile = resolvePinIconFile(app, pinIconValue, ctx.sourcePath); + + const color = "--link-color"; + + if (pinFile) { + if (pinFile.extension === "svg") { + pinIconEl.innerHTML = addSvgViewBoxPadding(await app.vault.cachedRead(pinFile), 2); + } else { + const url = app.vault.getResourcePath(pinFile); + pinIconEl.innerHTML = ``; + } + } else { + pinIconEl.innerHTML = addSvgViewBoxPadding(defaultPinIcon, 2); + } + + const match = paramaters.pinSize.trim().match(/^([+-]?(?:\d+(?:\.\d+)?|\.\d+))%$/); + if (match) pinIconEl.style.width = `${Number(match[1]) * 0.01 * currentMap.width}px`; + else pinIconEl.style.width = paramaters.pinSize; //#endregion const newPin = { note, element, location } @@ -125,7 +143,7 @@ export async function initPinInteractions( selectedPin.location = pxToLocation(px); await app.fileManager.processFrontMatter(selectedPin.note, (frontmatter) => { if (!selectedPin) return; - frontmatter["fantasy-map-location"] = formatLocation(selectedPin?.location); + frontmatter["fm-location"] = formatLocation(selectedPin?.location); }); } else { @@ -148,6 +166,54 @@ export async function initPinInteractions( }) } +function resolvePinIconFile( + app: App, + value: unknown, + sourcePath: string +): TFile | null { + if (typeof value !== "string") return null; + + const raw = value.trim(); + if (!raw) return null; + + const normalized = raw.replace(/^\[\[|\]\]$/g, "").trim(); + if (!normalized) return null; + + const linked = app.metadataCache.getFirstLinkpathDest(normalized, sourcePath); + if (linked instanceof TFile) return linked; + + const byPath = app.vault.getAbstractFileByPath(normalizePath(normalized)); + if (byPath instanceof TFile) return byPath; + + const files = app.vault.getFiles(); + const lower = normalized.toLowerCase(); + + const supportedExts = new Set(["svg", "png", "jpg", "jpeg", "webp", "gif"]); + + const exactName = files.find(f => f.name.toLowerCase() === lower); + if (exactName) return exactName; + + const basenameMatches = files.filter( + f => f.basename.toLowerCase() === lower && supportedExts.has(f.extension.toLowerCase()) + ); + + if (basenameMatches.length === 1) return basenameMatches[0] as TFile; + + // Optional: prefer SVG if multiple matches exist + const svgMatch = basenameMatches.find(f => f.extension.toLowerCase() === "svg"); + if (svgMatch) return svgMatch; + + return basenameMatches[0] ?? null; +} + +function addSvgViewBoxPadding(svg: string, pad: number): string { + return svg.replace(/viewBox="([^"]+)"/, (_, vb) => { + const [x, y, w, h] = vb.trim().split(/\s+/).map(Number); + if ([x, y, w, h].some(Number.isNaN)) return `viewBox="${vb}"`; + return `viewBox="${x - pad} ${y - pad} ${w + pad * 2} ${h + pad * 2}"`; + }); +} + //#region Helper functions for translating between location and px coordinates, and handling mouse position and hover timeouts interface Location { lat: number; lng: number } diff --git a/src/settings.ts b/src/settings.ts index f4ff986..02b49ea 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,15 +1,17 @@ -import {App, PluginSettingTab, Setting} from "obsidian"; +import { App, PluginSettingTab, Setting } from "obsidian"; import FantasyMap from "./main"; -import {fantasyMapCopyToClipboardString} from "./paramaters"; +import { fantasyMapCodeBlockCopyToClipboardString, fantasyMapFrontMatterCopyToClipboardString } from "./paramaters"; export interface FantasyMapSettings { + defaultPinSize: string; // e.g. "24px", "1.5rem", "5%" defaultUnitOfMeasurement: string; // Metric or Imperial defaultZoomIncrement: number; } export const DEFAULT_SETTINGS: FantasyMapSettings = { - defaultUnitOfMeasurement: "Metric", - defaultZoomIncrement: 1 + defaultPinSize: "24px", + defaultZoomIncrement: 1, + defaultUnitOfMeasurement: "Metric" } // noinspection JSUnusedGlobalSymbols @@ -38,6 +40,46 @@ export class FantasyMapSettingTab extends PluginSettingTab { this.plugin.settings.defaultUnitOfMeasurement = value; await this.plugin.saveSettings(); }));*/ + + new Setting(containerEl) + .setName("Default Pin Size") + .setDesc("Set the default size for pins on the map (e.g. 24px, 1.5rem, 5%).") + .addText(text => { + text + .setPlaceholder("Enter a size (e.g. 24px)") + .setValue(this.plugin.settings.defaultPinSize) + .onChange(async (value) => { + const trimmed = value.trim(); + + if (!isValidPinSize(trimmed)) { + text.inputEl.addClass("is-invalid"); + text.inputEl.setAttribute( + "title", + "Enter a valid CSS size like 24px, 1.5rem, 5%, 10vw, or 0." + ); + return; + } + + text.inputEl.removeClass("is-invalid"); + text.inputEl.removeAttribute("title"); + + this.plugin.settings.defaultPinSize = trimmed; + await this.plugin.saveSettings(); + }); + }); + + function isValidPinSize(value: string): boolean { + const trimmed = value.trim(); + if (!trimmed) return false; + + const el = document.createElement("div"); + el.style.width = ""; + el.style.width = trimmed; + + const result = el.style.width !== ""; + el.remove(); + return result; + } new Setting(containerEl) .setName('Default Zoom Increment') @@ -61,7 +103,7 @@ export class FantasyMapSettingTab extends PluginSettingTab { .setName("Template code block") .setDesc("Copy this into a note to start a Fantasy-Map block.") .addTextArea(text => { - text.setValue(fantasyMapCopyToClipboardString); + text.setValue(fantasyMapCodeBlockCopyToClipboardString); text.setDisabled(true); text.inputEl.rows = 11; // adjust to fit your block text.inputEl.style.width = "100%"; @@ -73,7 +115,34 @@ export class FantasyMapSettingTab extends PluginSettingTab { .setCta() .onClick(async () => { try { - await navigator.clipboard.writeText(fantasyMapCopyToClipboardString); + await navigator.clipboard.writeText(fantasyMapCodeBlockCopyToClipboardString); + // @ts-ignore + new Notice("Fantasy-Map block copied"); + } catch (e) { + console.error(e); + // @ts-ignore + new Notice("Failed to copy block"); + } + }); + }); + + new Setting(containerEl) + .setName("Template Note Front Matter") + .setDesc("Copy this into a note to pin a note to your map.") + .addTextArea(text => { + text.setValue(fantasyMapFrontMatterCopyToClipboardString); + text.setDisabled(true); + text.inputEl.rows = 11; // adjust to fit your block + text.inputEl.style.width = "100%"; + text.inputEl.style.fontFamily = "var(--font-monospace)"; + }) + .addButton(button => { + button + .setButtonText("Copy") + .setCta() + .onClick(async () => { + try { + await navigator.clipboard.writeText(fantasyMapFrontMatterCopyToClipboardString); // @ts-ignore new Notice("Fantasy-Map block copied"); } catch (e) { diff --git a/styles.css b/styles.css index 8358b7b..2c57a2e 100644 --- a/styles.css +++ b/styles.css @@ -136,14 +136,14 @@ transform-origin: center bottom; transform: translateX(-100%); transition: transform 120ms ease, filter 120ms ease; - color: var(--interactive-normal); + fill: var(--text-accent); } .map-pin-icon svg:hover { cursor: pointer; transform: translate(-100%, -10%) scale(1.1); filter: drop-shadow(0 0 2px rgba(0,0,0,0.5)); - color: var(--interactive-hover); + fill: var(--text-accent-hover); } /* endregion */