From d642af85230a911f723ecfe199f39eeacca49471 Mon Sep 17 00:00:00 2001 From: barrulus Date: Sat, 5 Sep 2026 08:28:24 +0100 Subject: [PATCH] fix: strip zoom auto-visibility display from migrated label group styles Pre-1.140 zoom auto-visibility hid burg label tiers with an inline display: none, and a map saved while zoomed out carried it in the group's style attribute. The 1.140 migration harvested it verbatim into the persisted label group style, the 1.150 store migration kept it, and since the store is applied over the saved svg on load, those tiers never rendered again at any zoom. Strip display in labelStyleFromLegacy, the funnel for both harvests, and add a 1.151.2 pass over the stored label group styles for maps saved by 1.150-1.151.1, which already carry the value in their styles record. Closes #1773 --- docs/wiki/Changelog.md | 4 +++ package-lock.json | 4 +-- package.json | 2 +- src/generators/styles-legacy.test.ts | 13 ++++++++ src/generators/styles-legacy.ts | 13 +++++--- src/services/io/auto-update.test.ts | 45 ++++++++++++++++++++++++++++ src/services/io/auto-update.ts | 16 +++++++++- src/services/versioning.ts | 2 +- 8 files changed, 90 insertions(+), 9 deletions(-) diff --git a/docs/wiki/Changelog.md b/docs/wiki/Changelog.md index 1d32b95d23..15ea3ae6b0 100644 --- a/docs/wiki/Changelog.md +++ b/docs/wiki/Changelog.md @@ -14,6 +14,10 @@ Current version of the Fantasy Map Generator is the latest `master` branch. You # Releases +**1.151.2 - 2026-09-05**: + +- Fix burg label tiers hidden at every zoom on maps migrated from pre-1.140 versions by _[barrulus](https://github.com/barrulus)_ [1.151.2] + **1.151.1 - 2026-09-03**: - Brushes stamp by distance travelled: smooth, gap-free painting at any refresh rate by _[barrulus](https://github.com/barrulus)_ [1.151.1] diff --git a/package-lock.json b/package-lock.json index 197453408b..00db3fa511 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fantasy-map-generator", - "version": "1.151.1-fork.1", + "version": "1.151.2-fork.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fantasy-map-generator", - "version": "1.151.1-fork.1", + "version": "1.151.2-fork.1", "license": "MIT", "dependencies": { "alea": "^1.0.1", diff --git a/package.json b/package.json index 6aea77ff24..312983e79e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "fantasy-map-generator", "productName": "Fantasy Map Generator", - "version": "1.151.1-fork.1", + "version": "1.151.2-fork.1", "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", "bugs": { diff --git a/src/generators/styles-legacy.test.ts b/src/generators/styles-legacy.test.ts index 940107ef42..eb056a9a0c 100644 --- a/src/generators/styles-legacy.test.ts +++ b/src/generators/styles-legacy.test.ts @@ -164,6 +164,19 @@ test("labelGroupFromLegacy prefers a numeric data-size over font-size, stringifi expect(group.attrs["font-size"]).toBe("10"); }); +// pre-1.140 zoom auto-visibility hid a burg tier with an inline display: none, and a map saved while +// zoomed out carries it in the group's style attribute; harvested verbatim it hides the tier forever +test("labelGroupFromLegacy drops the zoom auto-visibility display from the style", () => { + const legacy = { style: "text-shadow: white 0px 0px 4px; display: none;", "data-dx": 0, "data-dy": -0.4 }; + expect(labelGroupFromLegacy(legacy).attrs.style).toBe( + "text-shadow: white 0px 0px 4px; transform: translate(0em, -0.4em)" + ); + expect(labelGroupFromLegacy({ style: "display: none;" }).attrs.style).toBeNull(); + expect(labelGroupFromLegacy({ style: "text-shadow: white 0px 0px 4px" }).attrs.style).toBe( + "text-shadow: white 0px 0px 4px" + ); +}); + const presetDir = path.join(__dirname, "../../public/styles"); test("all 12 shipped presets parse as the new format with zero warnings", () => { diff --git a/src/generators/styles-legacy.ts b/src/generators/styles-legacy.ts index b66e59e4e0..e28b7e759c 100644 --- a/src/generators/styles-legacy.ts +++ b/src/generators/styles-legacy.ts @@ -627,13 +627,17 @@ export function labelGroupFromLegacy(legacy: unknown): Styles["labels"]["groups" } function labelStyleFromLegacy(bag: Record): string | null { - const style = strOr(bag.style, null); const dx = toNumber(bag["data-dx"], 0); const dy = toNumber(bag["data-dy"], 0); - const declarations = style?.trim().replace(/;+$/, "") || ""; const transform = dx || dy ? `transform: translate(${dx}em, ${dy}em)` : ""; - const cssText = [declarations, transform].filter(Boolean).join("; "); - return cssText ?? null; + return stripDisplay([strOr(bag.style, null), transform].filter(Boolean).join("; ")); +} + +// pre-1.140 zoom auto-visibility hid a group with an inline display: none, which a map saved while +// zoomed out carries in the style attribute; it is layer state, not style, and must not be persisted +export function stripDisplay(style: string | null): string | null { + const declarations = (style || "").split(";").map(declaration => declaration.trim()); + return declarations.filter(declaration => declaration && !/^display\s*:/.test(declaration)).join("; ") || null; } // legacy wrote stored burg-group bags to the DOM verbatim with no per-key defaults; only @@ -721,6 +725,7 @@ globalThis.stylesLegacy = { harvestAttributes, stylesFromMap, harvestStylesFromSvg, + stripDisplay, migrateStyles, restoreStrippedLayerStyles, stripMigratedAttributes diff --git a/src/services/io/auto-update.test.ts b/src/services/io/auto-update.test.ts index 0ccb0adff4..15e9ba328b 100644 --- a/src/services/io/auto-update.test.ts +++ b/src/services/io/auto-update.test.ts @@ -371,6 +371,51 @@ describe("v1.140 label group migration", () => { }); // the .map file carries the whole #map svg, so its defs are only what the file was saved with +describe("v1.151.2 label group display cleanup", () => { + // v1.140-1.151 harvested the zoom auto-visibility display: none into the persisted label group + // style, and since v1.150 the store is re-applied over the saved svg, so the record has to be cleaned + function stylesRecord(style: string | null) { + const record = structuredClone(Styles.defaults) as { + labels: { groups: Record }; + }; + record.labels.groups.hamlet = { + ...record.labels.groups.city, + attrs: { ...record.labels.groups.city.attrs, style } + }; + return record; + } + + it("strips display from stored label group styles, keeping the rest of the record", () => { + const data: string[] = []; + data[48] = JSON.stringify( + stylesRecord("text-shadow: white 0px 0px 4px; display: none; transform: translate(0em, -0.4em)") + ); + + resolveVersionConflicts("1.151.1", data); + + const expected = stylesRecord("text-shadow: white 0px 0px 4px; transform: translate(0em, -0.4em)"); + expect(JSON.parse(data[48])).toEqual(expected); + }); + + it("stores null when display was the only declaration", () => { + const data: string[] = []; + data[48] = JSON.stringify(stylesRecord("display: none")); + + resolveVersionConflicts("1.151.1", data); + + expect(JSON.parse(data[48]).labels.groups.hamlet.attrs.style).toBeNull(); + }); + + it("leaves current maps alone", () => { + const data: string[] = []; + data[48] = JSON.stringify(stylesRecord("display: none")); + + resolveVersionConflicts(VERSION, data); + + expect(JSON.parse(data[48]).labels.groups.hamlet.attrs.style).toBe("display: none"); + }); +}); + describe("missing svg defs", () => { const getDeftempIds = () => Array.from(document.querySelectorAll("#deftemp > *"), node => node.id); diff --git a/src/services/io/auto-update.ts b/src/services/io/auto-update.ts index 14771c8582..3872e1671a 100644 --- a/src/services/io/auto-update.ts +++ b/src/services/io/auto-update.ts @@ -8,7 +8,12 @@ import type { GraphOverrides } from "@/generators/graph-override"; import { type Label, type LabelNameMode, Labels as LabelsGenerator } from "@/generators/labels-generator"; import type { Measurer, MeasurerType } from "@/generators/measurers-generator"; -import { labelGroupFromLegacy, migrateStyles, restoreStrippedLayerStyles } from "@/generators/styles-legacy"; +import { + labelGroupFromLegacy, + migrateStyles, + restoreStrippedLayerStyles, + stripDisplay +} from "@/generators/styles-legacy"; import type { Point } from "@/generators/voronoi"; import { getGroupStyle } from "@/renderers/labels/label-groups"; import { unfog } from "@/renderers/overlays/fogging"; @@ -1889,4 +1894,13 @@ export async function resolveVersionConflicts(mapVersion: string, data: string[] // v1.150.0 made the styles store the source of truth data[48] = await migrateStyles(data[48]); } + + if (isOlderThan("1.151.2")) { + // v1.140-1.151 harvested the zoom auto-visibility display: none into the persisted label group + // styles, and the store is applied over the saved svg, so the hidden tiers never came back + const record = data[48] ? safeParseJSON(data[48]) : undefined; + const groups: { attrs?: { style?: string | null } }[] = Object.values(record?.labels?.groups || {}); + for (const group of groups) if (group?.attrs) group.attrs.style = stripDisplay(group.attrs.style ?? null); + if (record) data[48] = JSON.stringify(record); + } } diff --git a/src/services/versioning.ts b/src/services/versioning.ts index fba183c162..df4a3e9554 100644 --- a/src/services/versioning.ts +++ b/src/services/versioning.ts @@ -19,7 +19,7 @@ import { dialogState } from "@/components/dialog/state"; import { tip } from "@/components/tooltips"; import { isElectron } from "./platform"; -export const VERSION = "1.151.1"; +export const VERSION = "1.151.2"; // new changes on top const latestPublicChanges = [