Final Touches to 1.0.0

This commit is contained in:
Daniel Fiuk 2026-04-14 11:36:59 -06:00
parent 0d9df2716e
commit fdf0b36eb4
7 changed files with 191 additions and 23 deletions

1
src/assets/Pin.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 325 416" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M162.007,415.437c0,0 -162.007,-87.629 -162.007,-253.429c0,-89.414 72.593,-162.007 162.007,-162.007c89.414,-0 162.007,72.593 162.007,162.007c0,165.8 -162.007,253.429 -162.007,253.429Zm0,-326.316c-40.228,-0 -72.887,32.66 -72.887,72.887c0,40.228 32.66,72.887 72.887,72.887c40.228,0 72.887,-32.66 72.887,-72.887c0,-40.228 -32.66,-72.887 -72.887,-72.887Z"/></svg>

After

Width:  |  Height:  |  Size: 809 B

View file

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 936 1260" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path transform="translate(0, 40)" d="M467.872,1199.768c0,0 -467.872,-253.071 -467.872,-731.896c0,-258.226 209.646,-467.872 467.872,-467.872c258.226,0 467.872,209.646 467.872,467.872c0,478.824 -467.872,731.896 -467.872,731.896Zm0,-942.391c-116.176,0 -210.496,94.32 -210.496,210.496c0,116.176 94.32,210.496 210.496,210.496c116.176,0 210.496,-94.32 210.496,-210.496c0,-116.176 -94.32,-210.496 -210.496,-210.496Z" style="fill:currentColor;"/></svg>

View file

@ -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
}

View file

@ -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("<span class=\"fantasy-map-error\">Fantasy Map ERROR: map option is required!</span>") ;
if (value.trim().length == 0) addHelpMessage("<span class=\"fantasy-map-error\">Fantasy Map ERROR: map option is required!</span>");
// 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("<span class=\"fantasy-map-error\">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. </span>")
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) {

View file

@ -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 = `<img src="${url}" alt="" />`;
}
} 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 }

View file

@ -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) {

View file

@ -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 */