From 7bf33b6e654205ab9f275aa3bf3faf16c0b678aa Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 10 Feb 2026 22:38:38 +0100 Subject: [PATCH 01/66] feat: add labels module and integrate into PackedGraph --- src/modules/index.ts | 1 + src/modules/labels.ts | 152 +++++++++++++++++++++++++++++++++++++++ src/types/PackedGraph.ts | 2 + 3 files changed, 155 insertions(+) create mode 100644 src/modules/labels.ts diff --git a/src/modules/index.ts b/src/modules/index.ts index a9ebf2b81..46ee67595 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -12,4 +12,5 @@ import "./routes-generator"; import "./states-generator"; import "./zones-generator"; import "./religions-generator"; +import "./labels"; import "./provinces-generator"; diff --git a/src/modules/labels.ts b/src/modules/labels.ts new file mode 100644 index 000000000..a68622631 --- /dev/null +++ b/src/modules/labels.ts @@ -0,0 +1,152 @@ +declare global { + var Labels: LabelsModule; +} + +// --- Types --- + +export interface StateLabelData { + i: number; + type: "state"; + stateId: number; + text: string; + pathPoints: [number, number][]; + startOffset: number; + fontSize: number; + letterSpacing: number; + transform: string; +} + +export interface BurgLabelData { + i: number; + type: "burg"; + burgId: number; + group: string; + text: string; + x: number; + y: number; + dx: number; + dy: number; +} + +export interface CustomLabelData { + i: number; + type: "custom"; + group: string; + text: string; + pathPoints: [number, number][]; + startOffset: number; + fontSize: number; + letterSpacing: number; + transform: string; +} + +export type LabelData = StateLabelData | BurgLabelData | CustomLabelData; + +// --- Implementation --- + +class LabelsModule { + private getNextId(): number { + const labels = pack.labels; + if (labels.length === 0) return 0; + + const existingIds = labels.map((l) => l.i).sort((a, b) => a - b); + for (let id = 0; id < existingIds[existingIds.length - 1]; id++) { + if (!existingIds.includes(id)) return id; + } + return existingIds[existingIds.length - 1] + 1; + } + + getAll(): LabelData[] { + return pack.labels; + } + + get(id: number): LabelData | undefined { + return pack.labels.find((l) => l.i === id); + } + + getByType(type: LabelData["type"]): LabelData[] { + return pack.labels.filter((l) => l.type === type); + } + + getByGroup(group: string): LabelData[] { + return pack.labels.filter( + (l) => (l.type === "burg" || l.type === "custom") && l.group === group, + ); + } + + getStateLabel(stateId: number): StateLabelData | undefined { + return pack.labels.find( + (l) => l.type === "state" && l.stateId === stateId, + ) as StateLabelData | undefined; + } + + getBurgLabel(burgId: number): BurgLabelData | undefined { + return pack.labels.find( + (l) => l.type === "burg" && l.burgId === burgId, + ) as BurgLabelData | undefined; + } + + addStateLabel( + data: Omit, + ): StateLabelData { + const label: StateLabelData = { i: this.getNextId(), type: "state", ...data }; + pack.labels.push(label); + return label; + } + + addBurgLabel(data: Omit): BurgLabelData { + const label: BurgLabelData = { i: this.getNextId(), type: "burg", ...data }; + pack.labels.push(label); + return label; + } + + addCustomLabel( + data: Omit, + ): CustomLabelData { + const label: CustomLabelData = { i: this.getNextId(), type: "custom", ...data }; + pack.labels.push(label); + return label; + } + + updateLabel(id: number, updates: Partial): void { + const label = pack.labels.find((l) => l.i === id); + if (!label) return; + Object.assign(label, updates, { i: label.i, type: label.type }); + } + + removeLabel(id: number): void { + const index = pack.labels.findIndex((l) => l.i === id); + if (index !== -1) pack.labels.splice(index, 1); + } + + removeByType(type: LabelData["type"]): void { + pack.labels = pack.labels.filter((l) => l.type !== type); + } + + removeByGroup(group: string): void { + pack.labels = pack.labels.filter( + (l) => + !((l.type === "burg" || l.type === "custom") && l.group === group), + ); + } + + removeStateLabel(stateId: number): void { + const index = pack.labels.findIndex( + (l) => l.type === "state" && l.stateId === stateId, + ); + if (index !== -1) pack.labels.splice(index, 1); + } + + removeBurgLabel(burgId: number): void { + const index = pack.labels.findIndex( + (l) => l.type === "burg" && l.burgId === burgId, + ); + if (index !== -1) pack.labels.splice(index, 1); + } + + clear(): void { + pack.labels = []; + } +} + +window.Labels = new LabelsModule(); diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts index b8749f0a5..f54d81576 100644 --- a/src/types/PackedGraph.ts +++ b/src/types/PackedGraph.ts @@ -5,6 +5,7 @@ import type { Province } from "../modules/provinces-generator"; import type { River } from "../modules/river-generator"; import type { Route } from "../modules/routes-generator"; import type { State } from "../modules/states-generator"; +import type { LabelData } from "../modules/labels"; import type { Zone } from "../modules/zones-generator"; type TypedArray = @@ -62,5 +63,6 @@ export interface PackedGraph { zones: Zone[]; markers: any[]; ice: any[]; + labels: LabelData[]; provinces: Province[]; } From dac231f91416db413355267768648e292a465c00 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 10 Feb 2026 22:53:32 +0100 Subject: [PATCH 02/66] refactor: clean up label-related code and introduce raycasting utilities --- src/modules/labels.ts | 4 - src/renderers/draw-state-labels.ts | 179 ++-------------------------- src/utils/label-raycast.ts | 185 +++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 175 deletions(-) create mode 100644 src/utils/label-raycast.ts diff --git a/src/modules/labels.ts b/src/modules/labels.ts index a68622631..f42f018b4 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -2,8 +2,6 @@ declare global { var Labels: LabelsModule; } -// --- Types --- - export interface StateLabelData { i: number; type: "state"; @@ -42,8 +40,6 @@ export interface CustomLabelData { export type LabelData = StateLabelData | BurgLabelData | CustomLabelData; -// --- Implementation --- - class LabelsModule { private getNextId(): number { const labels = pack.labels; diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index 24528d450..acf66c207 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -8,24 +8,17 @@ import { round, splitInTwo, } from "../utils"; +import { + Ray, + raycast, + findBestRayPair, + ANGLES +} from "../utils/label-raycast"; declare global { var drawStateLabels: (list?: number[]) => void; } -interface Ray { - angle: number; - length: number; - x: number; - y: number; -} - -interface AngleData { - angle: number; - dx: number; - dy: number; -} - type PathPoints = [number, number][]; // list - an optional array of stateIds to regenerate @@ -36,18 +29,9 @@ const stateLabelsRenderer = (list?: number[]): void => { const layerDisplay = labels.style("display"); labels.style("display", null); - const { cells, states, features } = pack; + const { cells, states } = pack; const stateIds = cells.state; - // increase step to 15 or 30 to make it faster and more horyzontal - // decrease step to 5 to improve accuracy - const ANGLE_STEP = 9; - const angles = precalculateAngles(ANGLE_STEP); - - const LENGTH_START = 5; - const LENGTH_STEP = 5; - const LENGTH_MAX = 300; - const labelPaths = getLabelPaths(); const letterLength = checkExampleLetterLength(); drawLabelPath(letterLength); @@ -66,7 +50,7 @@ const stateLabelsRenderer = (list?: number[]): void => { const maxLakeSize = state.cells! / 20; const [x0, y0] = state.pole!; - const rays: Ray[] = angles.map(({ angle, dx, dy }) => { + const rays: Ray[] = ANGLES.map(({ angle, dx, dy }) => { const { length, x, y } = raycast({ stateId: state.i, x0, @@ -219,153 +203,6 @@ const stateLabelsRenderer = (list?: number[]): void => { return 10; } - function precalculateAngles(step: number): AngleData[] { - const angles: AngleData[] = []; - const RAD = Math.PI / 180; - - for (let angle = 0; angle < 360; angle += step) { - const dx = Math.cos(angle * RAD); - const dy = Math.sin(angle * RAD); - angles.push({ angle, dx, dy }); - } - - return angles; - } - - function raycast({ - stateId, - x0, - y0, - dx, - dy, - maxLakeSize, - offset, - }: { - stateId: number; - x0: number; - y0: number; - dx: number; - dy: number; - maxLakeSize: number; - offset: number; - }): { length: number; x: number; y: number } { - let ray = { length: 0, x: x0, y: y0 }; - - for ( - let length = LENGTH_START; - length < LENGTH_MAX; - length += LENGTH_STEP - ) { - const [x, y] = [x0 + length * dx, y0 + length * dy]; - // offset points are perpendicular to the ray - const offset1: [number, number] = [x + -dy * offset, y + dx * offset]; - const offset2: [number, number] = [x + dy * offset, y + -dx * offset]; - - if (DEBUG.stateLabels) { - drawPoint([x, y], { - color: isInsideState(x, y) ? "blue" : "red", - radius: 0.8, - }); - drawPoint(offset1, { - color: isInsideState(...offset1) ? "blue" : "red", - radius: 0.4, - }); - drawPoint(offset2, { - color: isInsideState(...offset2) ? "blue" : "red", - radius: 0.4, - }); - } - - const inState = - isInsideState(x, y) && - isInsideState(...offset1) && - isInsideState(...offset2); - if (!inState) break; - ray = { length, x, y }; - } - - return ray; - - function isInsideState(x: number, y: number): boolean { - if (x < 0 || x > graphWidth || y < 0 || y > graphHeight) return false; - const cellId = findClosestCell(x, y, undefined, pack) as number; - - const feature = features[cells.f[cellId]]; - if (feature.type === "lake") - return isInnerLake(feature) || isSmallLake(feature); - - return stateIds[cellId] === stateId; - } - - function isInnerLake(feature: { shoreline: number[] }): boolean { - return feature.shoreline.every((cellId) => stateIds[cellId] === stateId); - } - - function isSmallLake(feature: { cells: number }): boolean { - return feature.cells <= maxLakeSize; - } - } - - function findBestRayPair(rays: Ray[]): [Ray, Ray] { - let bestPair: [Ray, Ray] | null = null; - let bestScore = -Infinity; - - for (let i = 0; i < rays.length; i++) { - const score1 = rays[i].length * scoreRayAngle(rays[i].angle); - - for (let j = i + 1; j < rays.length; j++) { - const score2 = rays[j].length * scoreRayAngle(rays[j].angle); - const pairScore = - (score1 + score2) * scoreCurvature(rays[i].angle, rays[j].angle); - - if (pairScore > bestScore) { - bestScore = pairScore; - bestPair = [rays[i], rays[j]]; - } - } - } - - return bestPair!; - } - - function scoreRayAngle(angle: number): number { - const normalizedAngle = Math.abs(angle % 180); // [0, 180] - const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1] - - if (horizontality === 1) return 1; // Best: horizontal - if (horizontality >= 0.75) return 0.9; // Very good: slightly slanted - if (horizontality >= 0.5) return 0.6; // Good: moderate slant - if (horizontality >= 0.25) return 0.5; // Acceptable: more slanted - if (horizontality >= 0.15) return 0.2; // Poor: almost vertical - return 0.1; // Very poor: almost vertical - } - - function scoreCurvature(angle1: number, angle2: number): number { - const delta = getAngleDelta(angle1, angle2); - const similarity = evaluateArc(angle1, angle2); - - if (delta === 180) return 1; // straight line: best - if (delta < 90) return 0; // acute: not allowed - if (delta < 120) return 0.6 * similarity; - if (delta < 140) return 0.7 * similarity; - if (delta < 160) return 0.8 * similarity; - - return similarity; - } - - function getAngleDelta(angle1: number, angle2: number): number { - let delta = Math.abs(angle1 - angle2) % 360; - if (delta > 180) delta = 360 - delta; // [0, 180] - return delta; - } - - // compute arc similarity towards x-axis - function evaluateArc(angle1: number, angle2: number): number { - const proximity1 = Math.abs((angle1 % 180) - 90); - const proximity2 = Math.abs((angle2 % 180) - 90); - return 1 - Math.abs(proximity1 - proximity2) / 90; - } - function getLinesAndRatio( mode: string, name: string, diff --git a/src/utils/label-raycast.ts b/src/utils/label-raycast.ts new file mode 100644 index 000000000..a5d96870b --- /dev/null +++ b/src/utils/label-raycast.ts @@ -0,0 +1,185 @@ +import { findClosestCell } from "./index"; + +export interface Ray { + angle: number; + length: number; + x: number; + y: number; +} + +export interface AngleData { + angle: number; + dx: number; + dy: number; +} + +interface RaycastParams { + stateId: number; + x0: number; + y0: number; + dx: number; + dy: number; + maxLakeSize: number; + offset: number; +} + +// increase step to 15 or 30 to make it faster and more horyzontal +// decrease step to 5 to improve accuracy +const ANGLE_STEP = 9; +export const ANGLES = precalculateAngles(ANGLE_STEP); + +const LENGTH_START = 5; +const LENGTH_STEP = 5; +const LENGTH_MAX = 300; + +/** + * Cast a ray from a point in a given direction until it exits a state. + * Checks both the ray point and offset points perpendicular to it. + */ +export function raycast({ + stateId, + x0, + y0, + dx, + dy, + maxLakeSize, + offset, +}: RaycastParams): { length: number; x: number; y: number } { + const { cells, features } = pack; + const stateIds = cells.state; + let ray = { length: 0, x: x0, y: y0 }; + + for ( + let length = LENGTH_START; + length < LENGTH_MAX; + length += LENGTH_STEP + ) { + const [x, y] = [x0 + length * dx, y0 + length * dy]; + // offset points are perpendicular to the ray + const offset1: [number, number] = [x + -dy * offset, y + dx * offset]; + const offset2: [number, number] = [x + dy * offset, y + -dx * offset]; + + const inState = + isInsideState(x, y, stateId) && + isInsideState(...offset1, stateId) && + isInsideState(...offset2, stateId); + if (!inState) break; + ray = { length, x, y }; + } + + return ray; + + function isInsideState(x: number, y: number, stateId: number): boolean { + if (x < 0 || x > graphWidth || y < 0 || y > graphHeight) return false; + const cellId = findClosestCell(x, y, undefined, pack) as number; + + const feature = features[cells.f[cellId]]; + if (feature.type === "lake") + return isInnerLake(feature) || isSmallLake(feature); + + return stateIds[cellId] === stateId; + } + + function isInnerLake(feature: { shoreline: number[] }): boolean { + return feature.shoreline.every((cellId) => stateIds[cellId] === stateId); + } + + function isSmallLake(feature: { cells: number }): boolean { + return feature.cells <= maxLakeSize; + } +} + +/** + * Score a ray angle based on how horizontal it is. + * Horizontal rays (0° or 180°) are preferred for label placement. + */ +export function scoreRayAngle(angle: number): number { + const normalizedAngle = Math.abs(angle % 180); // [0, 180] + const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1] + + if (horizontality === 1) return 1; // Best: horizontal + if (horizontality >= 0.75) return 0.9; // Very good: slightly slanted + if (horizontality >= 0.5) return 0.6; // Good: moderate slant + if (horizontality >= 0.25) return 0.5; // Acceptable: more slanted + if (horizontality >= 0.15) return 0.2; // Poor: almost vertical + return 0.1; // Very poor: almost vertical +} + +/** + * Calculate the angle delta between two angles (0-180 degrees). + */ +export function getAngleDelta(angle1: number, angle2: number): number { + let delta = Math.abs(angle1 - angle2) % 360; + if (delta > 180) delta = 360 - delta; // [0, 180] + return delta; +} + +/** + * Evaluate how similar the arc between two angles is. + * Computes proximity of both angles towards the x-axis. + */ +export function evaluateArc(angle1: number, angle2: number): number { + const proximity1 = Math.abs((angle1 % 180) - 90); + const proximity2 = Math.abs((angle2 % 180) - 90); + return 1 - Math.abs(proximity1 - proximity2) / 90; +} + +/** + * Score a ray pair based on the delta angle between them and their arc similarity. + * Penalizes acute angles (<90°), favors straight lines (180°). + */ +export function scoreCurvature(angle1: number, angle2: number): number { + const delta = getAngleDelta(angle1, angle2); + const similarity = evaluateArc(angle1, angle2); + + if (delta === 180) return 1; // straight line: best + if (delta < 90) return 0; // acute: not allowed + if (delta < 120) return 0.6 * similarity; + if (delta < 140) return 0.7 * similarity; + if (delta < 160) return 0.8 * similarity; + + return similarity; +} + +/** + * Precompute angles and their vector components for raycast directions. + * Used to sample rays around a point at regular angular intervals. + */ +function precalculateAngles(step: number): AngleData[] { + const angles: AngleData[] = []; + const RAD = Math.PI / 180; + + for (let angle = 0; angle < 360; angle += step) { + const dx = Math.cos(angle * RAD); + const dy = Math.sin(angle * RAD); + angles.push({ angle, dx, dy }); + } + + return angles; +} + +/** + * Find the best pair of rays for label placement along a curved path. + * Prefers horizontal rays and well-separated angles. + */ +export function findBestRayPair(rays: Ray[]): [Ray, Ray] { + let bestPair: [Ray, Ray] | null = null; + let bestScore = -Infinity; + + for (let i = 0; i < rays.length; i++) { + const score1 = rays[i].length * scoreRayAngle(rays[i].angle); + + for (let j = i + 1; j < rays.length; j++) { + const score2 = rays[j].length * scoreRayAngle(rays[j].angle); + const pairScore = + (score1 + score2) * scoreCurvature(rays[i].angle, rays[j].angle); + + if (pairScore > bestScore) { + bestScore = pairScore; + bestPair = [rays[i], rays[j]]; + } + } + } + + return bestPair!; +} From c467f87df552c93e9fd5ec6c26823d7b2bc2c1b7 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 10 Feb 2026 22:56:48 +0100 Subject: [PATCH 03/66] refactor: change exported functions to internal functions in label-raycast utility --- src/utils/label-raycast.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/label-raycast.ts b/src/utils/label-raycast.ts index a5d96870b..05e760430 100644 --- a/src/utils/label-raycast.ts +++ b/src/utils/label-raycast.ts @@ -7,7 +7,7 @@ export interface Ray { y: number; } -export interface AngleData { +interface AngleData { angle: number; dx: number; dy: number; @@ -93,7 +93,7 @@ export function raycast({ * Score a ray angle based on how horizontal it is. * Horizontal rays (0° or 180°) are preferred for label placement. */ -export function scoreRayAngle(angle: number): number { +function scoreRayAngle(angle: number): number { const normalizedAngle = Math.abs(angle % 180); // [0, 180] const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1] @@ -108,7 +108,7 @@ export function scoreRayAngle(angle: number): number { /** * Calculate the angle delta between two angles (0-180 degrees). */ -export function getAngleDelta(angle1: number, angle2: number): number { +function getAngleDelta(angle1: number, angle2: number): number { let delta = Math.abs(angle1 - angle2) % 360; if (delta > 180) delta = 360 - delta; // [0, 180] return delta; @@ -118,7 +118,7 @@ export function getAngleDelta(angle1: number, angle2: number): number { * Evaluate how similar the arc between two angles is. * Computes proximity of both angles towards the x-axis. */ -export function evaluateArc(angle1: number, angle2: number): number { +function evaluateArc(angle1: number, angle2: number): number { const proximity1 = Math.abs((angle1 % 180) - 90); const proximity2 = Math.abs((angle2 % 180) - 90); return 1 - Math.abs(proximity1 - proximity2) / 90; @@ -128,7 +128,7 @@ export function evaluateArc(angle1: number, angle2: number): number { * Score a ray pair based on the delta angle between them and their arc similarity. * Penalizes acute angles (<90°), favors straight lines (180°). */ -export function scoreCurvature(angle1: number, angle2: number): number { +function scoreCurvature(angle1: number, angle2: number): number { const delta = getAngleDelta(angle1, angle2); const similarity = evaluateArc(angle1, angle2); From 94b638f3cb571d1b85dbdd44633e80bd5fd0d0e7 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Wed, 11 Feb 2026 22:01:57 +0100 Subject: [PATCH 04/66] feat: integrate label generation into main flow and enhance label data handling --- public/main.js | 2 + src/modules/labels.ts | 94 +++++++++++++++++-- src/renderers/draw-burg-labels.ts | 89 ++++++++++++------ src/renderers/draw-state-labels.ts | 140 ++++++++++++++--------------- 4 files changed, 220 insertions(+), 105 deletions(-) diff --git a/public/main.js b/public/main.js index c0ac9d110..2f055942e 100644 --- a/public/main.js +++ b/public/main.js @@ -650,6 +650,8 @@ async function generate(options) { Provinces.generate(); Provinces.getPoles(); + Labels.generate(); + Rivers.specify(); Lakes.defineNames(); diff --git a/src/modules/labels.ts b/src/modules/labels.ts index f42f018b4..548645c5b 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -7,11 +7,7 @@ export interface StateLabelData { type: "state"; stateId: number; text: string; - pathPoints: [number, number][]; - startOffset: number; - fontSize: number; - letterSpacing: number; - transform: string; + fontSize?: number; } export interface BurgLabelData { @@ -32,10 +28,10 @@ export interface CustomLabelData { group: string; text: string; pathPoints: [number, number][]; - startOffset: number; - fontSize: number; - letterSpacing: number; - transform: string; + startOffset?: number; + fontSize?: number; + letterSpacing?: number; + transform?: string; } export type LabelData = StateLabelData | BurgLabelData | CustomLabelData; @@ -52,6 +48,12 @@ class LabelsModule { return existingIds[existingIds.length - 1] + 1; } + generate() : void { + this.clear(); + generateStateLabels(); + generateBurgLabels(); + } + getAll(): LabelData[] { return pack.labels; } @@ -145,4 +147,78 @@ class LabelsModule { } } +/** + * Generate state labels data entries for each state. + * Only stores essential label data; raycast path calculation happens during rendering. + * @param list - Optional array of stateIds to regenerate only those + */ +export function generateStateLabels(list?: number[]): void { + if (!TIME) console.time("generateStateLabels"); + else TIME && console.time("generateStateLabels"); + + const { states } = pack; + const labelsModule = window.Labels; + + // Remove existing state labels that need regeneration + if (list) { + list.forEach((stateId) => labelsModule.removeStateLabel(stateId)); + } else { + labelsModule.removeByType("state"); + } + + // Generate new label entries + for (const state of states) { + if (!state.i || state.removed || state.lock) continue; + if (list && !list.includes(state.i)) continue; + + labelsModule.addStateLabel({ + stateId: state.i, + text: state.name!, + fontSize: 100, + }); + } + + if (!TIME) console.timeEnd("generateStateLabels"); + else TIME && console.timeEnd("generateStateLabels"); +} + +/** + * Generate burg labels data from burgs. + * Populates pack.labels with BurgLabelData for each burg. + */ +export function generateBurgLabels(): void { + if (!TIME) console.time("generateBurgLabels"); + else TIME && console.time("generateBurgLabels"); + + const labelsModule = window.Labels; + + // Remove existing burg labels + labelsModule.removeByType("burg"); + + // Generate new labels for all active burgs + for (const burg of pack.burgs) { + if (!burg.i || burg.removed) continue; + + const group = burg.group || "unmarked"; + + // Get label group offset attributes if they exist (will be set during rendering) + // For now, use defaults - these will be updated during rendering phase + const dx = 0; + const dy = 0; + + labelsModule.addBurgLabel({ + burgId: burg.i, + group, + text: burg.name!, + x: burg.x, + y: burg.y, + dx, + dy, + }); + } + + if (!TIME) console.timeEnd("generateBurgLabels"); + else TIME && console.timeEnd("generateBurgLabels"); +} + window.Labels = new LabelsModule(); diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index 5dc6cc713..864395599 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -1,4 +1,5 @@ import type { Burg } from "../modules/burgs-generator"; +import type { BurgLabelData } from "../modules/labels"; declare global { var drawBurgLabels: () => void; @@ -15,31 +16,42 @@ const burgLabelsRenderer = (): void => { TIME && console.time("drawBurgLabels"); createLabelGroups(); - for (const { name } of options.burgs.groups as BurgGroup[]) { - const burgsInGroup = pack.burgs.filter( - (b) => b.group === name && !b.removed, - ); - if (!burgsInGroup.length) continue; + // Get all burg labels grouped by group name + const burgLabelsByGroup = new Map(); + for (const label of Labels.getByType("burg").map((l) => l as BurgLabelData)) { + if (!burgLabelsByGroup.has(label.group)) { + burgLabelsByGroup.set(label.group, []); + } + burgLabelsByGroup.get(label.group)!.push(label); + } - const labelGroup = burgLabels.select(`#${name}`); + // Render each group and update label offsets from SVG attributes + for (const [groupName, labels] of burgLabelsByGroup) { + const labelGroup = burgLabels.select(`#${groupName}`); if (labelGroup.empty()) continue; - const dx = labelGroup.attr("data-dx") || 0; - const dy = labelGroup.attr("data-dy") || 0; - - labelGroup - .selectAll("text") - .data(burgsInGroup) - .enter() - .append("text") - .attr("text-rendering", "optimizeSpeed") - .attr("id", (d) => `burgLabel${d.i}`) - .attr("data-id", (d) => d.i!) - .attr("x", (d) => d.x) - .attr("y", (d) => d.y) - .attr("dx", `${dx}em`) - .attr("dy", `${dy}em`) - .text((d) => d.name!); + const dxAttr = labelGroup.attr("data-dx"); + const dyAttr = labelGroup.attr("data-dy"); + const dx = dxAttr ? parseFloat(dxAttr) : 0; + const dy = dyAttr ? parseFloat(dyAttr) : 0; + + for (const labelData of labels) { + // Update label data with SVG group offsets + if (labelData.dx !== dx || labelData.dy !== dy) { + Labels.updateLabel(labelData.i, { dx, dy }); + } + + labelGroup + .append("text") + .attr("text-rendering", "optimizeSpeed") + .attr("id", `burgLabel${labelData.burgId}`) + .attr("data-id", labelData.burgId) + .attr("x", labelData.x) + .attr("y", labelData.y) + .attr("dx", `${dx}em`) + .attr("dy", `${dy}em`) + .text(labelData.text); + } } TIME && console.timeEnd("drawBurgLabels"); @@ -48,14 +60,40 @@ const burgLabelsRenderer = (): void => { const drawBurgLabelRenderer = (burg: Burg): void => { const labelGroup = burgLabels.select(`#${burg.group}`); if (labelGroup.empty()) { - drawBurgLabels(); + burgLabelsRenderer(); return; // redraw all labels if group is missing } - const dx = labelGroup.attr("data-dx") || 0; - const dy = labelGroup.attr("data-dy") || 0; + const dxAttr = labelGroup.attr("data-dx"); + const dyAttr = labelGroup.attr("data-dy"); + const dx = dxAttr ? parseFloat(dxAttr) : 0; + const dy = dyAttr ? parseFloat(dyAttr) : 0; removeBurgLabelRenderer(burg.i!); + + // Add/update label in data layer + const existingLabel = Labels.getBurgLabel(burg.i!); + if (existingLabel) { + Labels.updateLabel(existingLabel.i, { + text: burg.name!, + x: burg.x, + y: burg.y, + dx, + dy, + }); + } else { + Labels.addBurgLabel({ + burgId: burg.i!, + group: burg.group || "unmarked", + text: burg.name!, + x: burg.x, + y: burg.y, + dx, + dy, + }); + } + + // Render to SVG labelGroup .append("text") .attr("text-rendering", "optimizeSpeed") @@ -71,6 +109,7 @@ const drawBurgLabelRenderer = (burg: Burg): void => { const removeBurgLabelRenderer = (burgId: number): void => { const existingLabel = document.getElementById(`burgLabel${burgId}`); if (existingLabel) existingLabel.remove(); + Labels.removeBurgLabel(burgId); }; function createLabelGroups(): void { diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index acf66c207..a09de10d7 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -1,27 +1,50 @@ import { curveNatural, line, max, select } from "d3"; import { - drawPath, - drawPoint, findClosestCell, minmax, rn, round, splitInTwo, } from "../utils"; +import type { StateLabelData } from "../modules/labels"; import { - Ray, raycast, findBestRayPair, - ANGLES + ANGLES, } from "../utils/label-raycast"; declare global { var drawStateLabels: (list?: number[]) => void; } -type PathPoints = [number, number][]; +/** + * Helper function to calculate offset width for raycast based on state size + */ +function getOffsetWidth(cellsNumber: number): number { + if (cellsNumber < 40) return 0; + if (cellsNumber < 200) return 5; + return 10; +} + +function checkExampleLetterLength(): number { + const textGroup = select("g#labels > g#states"); + const testLabel = textGroup + .append("text") + .attr("x", 0) + .attr("y", 0) + .text("Example"); + const letterLength = + (testLabel.node() as SVGTextElement).getComputedTextLength() / 7; // approximate length of 1 letter + testLabel.remove(); + + return letterLength; +} -// list - an optional array of stateIds to regenerate +/** + * Render state labels from pack.labels data to SVG. + * Adjusts and fits labels based on layout constraints. + * list - optional array of stateIds to re-render + */ const stateLabelsRenderer = (list?: number[]): void => { TIME && console.time("drawStateLabels"); @@ -29,28 +52,41 @@ const stateLabelsRenderer = (list?: number[]): void => { const layerDisplay = labels.style("display"); labels.style("display", null); - const { cells, states } = pack; - const stateIds = cells.state; + const { states } = pack; + + // Get labels to render + const labelsToRender = list + ? Labels.getAll() + .filter((l) => l.type === "state" && list.includes((l as StateLabelData).stateId)) + .map((l) => l as StateLabelData) + : Labels.getByType("state").map((l) => l as StateLabelData); - const labelPaths = getLabelPaths(); const letterLength = checkExampleLetterLength(); - drawLabelPath(letterLength); + drawLabelPath(letterLength, labelsToRender); // restore labels visibility labels.style("display", layerDisplay); - function getLabelPaths(): [number, PathPoints][] { - const labelPaths: [number, PathPoints][] = []; + function drawLabelPath(letterLength: number, labelDataList: StateLabelData[]): void { + const mode = options.stateLabelsMode || "auto"; + const lineGen = line<[number, number]>().curve(curveNatural); - for (const state of states) { - if (!state.i || state.removed || state.lock) continue; - if (list && !list.includes(state.i)) continue; + const textGroup = select("g#labels > g#states"); + const pathGroup = select( + "defs > g#deftemp > g#textPaths", + ); + for (const labelData of labelDataList) { + const state = states[labelData.stateId]; + if (!state.i || state.removed) + throw new Error("State must not be neutral or removed"); + + // Calculate pathPoints using raycast algorithm (recalculated on each draw) const offset = getOffsetWidth(state.cells!); const maxLakeSize = state.cells! / 20; const [x0, y0] = state.pole!; - const rays: Ray[] = ANGLES.map(({ angle, dx, dy }) => { + const rays = ANGLES.map(({ angle, dx, dy }) => { const { length, x, y } = raycast({ stateId: state.i, x0, @@ -64,61 +100,20 @@ const stateLabelsRenderer = (list?: number[]): void => { }); const [ray1, ray2] = findBestRayPair(rays); - const pathPoints: PathPoints = [ + const pathPoints: [number, number][] = [ [ray1.x, ray1.y], state.pole!, [ray2.x, ray2.y], ]; if (ray1.x > ray2.x) pathPoints.reverse(); - if (DEBUG.stateLabels) { - drawPoint(state.pole!, { color: "black", radius: 1 }); - drawPath(pathPoints, { color: "black", width: 0.2 }); - } - - labelPaths.push([state.i, pathPoints]); - } - - return labelPaths; - } - - function checkExampleLetterLength(): number { - const textGroup = select("g#labels > g#states"); - const testLabel = textGroup - .append("text") - .attr("x", 0) - .attr("y", 0) - .text("Example"); - const letterLength = - (testLabel.node() as SVGTextElement).getComputedTextLength() / 7; // approximate length of 1 letter - testLabel.remove(); - - return letterLength; - } - - function drawLabelPath(letterLength: number): void { - const mode = options.stateLabelsMode || "auto"; - const lineGen = line<[number, number]>().curve(curveNatural); - - const textGroup = select("g#labels > g#states"); - const pathGroup = select( - "defs > g#deftemp > g#textPaths", - ); - - for (const [stateId, pathPoints] of labelPaths) { - const state = states[stateId]; - if (!state.i || state.removed) - throw new Error("State must not be neutral or removed"); - if (pathPoints.length < 2) - throw new Error("Label path must have at least 2 points"); - - textGroup.select(`#stateLabel${stateId}`).remove(); - pathGroup.select(`#textPath_stateLabel${stateId}`).remove(); + textGroup.select(`#stateLabel${labelData.stateId}`).remove(); + pathGroup.select(`#textPath_stateLabel${labelData.stateId}`).remove(); const textPath = pathGroup .append("path") .attr("d", round(lineGen(pathPoints) || "")) - .attr("id", `textPath_stateLabel${stateId}`); + .attr("id", `textPath_stateLabel${labelData.stateId}`); const pathLength = (textPath.node() as SVGPathElement).getTotalLength() / letterLength; // path length in letters @@ -129,6 +124,9 @@ const stateLabelsRenderer = (list?: number[]): void => { pathLength, ); + // Update label data with font size + Labels.updateLabel(labelData.i, { fontSize: ratio }); + // prolongate path if it's too short const longestLineLength = max(lines.map((line) => line.length)) || 0; if (pathLength && pathLength < longestLineLength) { @@ -149,7 +147,7 @@ const stateLabelsRenderer = (list?: number[]): void => { const textElement = textGroup .append("text") .attr("text-rendering", "optimizeSpeed") - .attr("id", `stateLabel${stateId}`) + .attr("id", `stateLabel${labelData.stateId}`) .append("textPath") .attr("startOffset", "50%") .attr("font-size", `${ratio}%`) @@ -163,12 +161,16 @@ const stateLabelsRenderer = (list?: number[]): void => { textElement.insertAdjacentHTML("afterbegin", spans.join("")); const { width, height } = textElement.getBBox(); - textElement.setAttribute("href", `#textPath_stateLabel${stateId}`); + textElement.setAttribute("href", `#textPath_stateLabel${labelData.stateId}`); + const stateIds = pack.cells.state; if (mode === "full" || lines.length === 1) continue; // check if label fits state boundaries. If no, replace it with short name - const [[x1, y1], [x2, y2]] = [pathPoints.at(0)!, pathPoints.at(-1)!]; + const [[x1, y1], [x2, y2]] = [ + pathPoints.at(0)!, + pathPoints.at(-1)!, + ]; const angleRad = Math.atan2(y2 - y1, x2 - x1); const isInsideState = checkIfInsideState( @@ -177,7 +179,7 @@ const stateLabelsRenderer = (list?: number[]): void => { width / 2, height / 2, stateIds, - stateId, + labelData.stateId, ); if (isInsideState) continue; @@ -187,6 +189,7 @@ const stateLabelsRenderer = (list?: number[]): void => { ? state.fullName! : state.name!; textElement.innerHTML = `${text}`; + Labels.updateLabel(labelData.i, { text }); const correctedRatio = minmax( rn((pathLength / text.length) * 50), @@ -194,15 +197,10 @@ const stateLabelsRenderer = (list?: number[]): void => { 130, ); textElement.setAttribute("font-size", `${correctedRatio}%`); + Labels.updateLabel(labelData.i, { fontSize: correctedRatio }); } } - function getOffsetWidth(cellsNumber: number): number { - if (cellsNumber < 40) return 0; - if (cellsNumber < 200) return 5; - return 10; - } - function getLinesAndRatio( mode: string, name: string, From 689fef0858e32c5025548588ed310e25cba51e9a Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 13 Feb 2026 18:07:12 +0100 Subject: [PATCH 05/66] feat: enhance label editing functionality and improve data model synchronization --- public/modules/ui/labels-editor.js | 118 +++++++++++++++++++++++++---- 1 file changed, 105 insertions(+), 13 deletions(-) diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index 8c47ec99e..32d1c2c74 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -1,4 +1,55 @@ "use strict"; +let currentLabelData = null; + +// Helper: extract control points from an SVG path element +function extractPathPoints(pathElement) { + if (!pathElement) return []; + const l = pathElement.getTotalLength(); + if (!l) return []; + const points = []; + const increment = l / Math.max(Math.ceil(l / 200), 2); + for (let i = 0; i <= l; i += increment) { + const point = pathElement.getPointAtLength(i); + points.push([point.x, point.y]); + } + return points; +} + +// Helper: find label data from the Labels data model for an SVG text element +function getLabelData(textElement) { + const id = textElement.id || ""; + if (id.startsWith("stateLabel")) { + return Labels.getStateLabel(+id.slice(10)); + } + // Custom labels: check for existing data-label-id attribute + const dataLabelId = textElement.getAttribute("data-label-id"); + if (dataLabelId != null) { + const existing = Labels.get(+dataLabelId); + if (existing) return existing; + // Data was cleared (e.g., map regenerated) — recreate + textElement.removeAttribute("data-label-id"); + } + // No data entry found — create one from SVG state (migration path) + return createCustomLabelDataFromSvg(textElement); +} + +// Helper: create a CustomLabelData entry from existing SVG elements +function createCustomLabelDataFromSvg(textElement) { + const textPathEl = textElement.querySelector("textPath"); + if (!textPathEl) return null; + const group = textElement.parentNode.id; + const text = [...textPathEl.querySelectorAll("tspan")].map(t => t.textContent).join("|"); + const pathEl = byId("textPath_" + textElement.id); + const pathPoints = extractPathPoints(pathEl); + const startOffset = parseFloat(textPathEl.getAttribute("startOffset")) || 50; + const fontSize = parseFloat(textPathEl.getAttribute("font-size")) || 100; + const letterSpacing = parseFloat(textPathEl.getAttribute("letter-spacing") || "0"); + const transform = textElement.getAttribute("transform") || undefined; + const label = Labels.addCustomLabel({ group, text, pathPoints, startOffset, fontSize, letterSpacing, transform }); + textElement.setAttribute("data-label-id", String(label.i)); + return label; +} + function editLabel() { if (customization) return; closeDialogs(); @@ -10,11 +61,14 @@ function editLabel() { elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true); viewbox.on("touchmove mousemove", showEditorTips); + // Resolve label data from the data model + currentLabelData = getLabelData(text); + $("#labelEditor").dialog({ title: "Edit Label", resizable: false, width: fitContent(), - position: {my: "center top+10", at: "bottom", of: text, collision: "fit"}, + position: { my: "center top+10", at: "bottom", of: text, collision: "fit" }, close: closeLabelEditor }); @@ -82,11 +136,20 @@ function editLabel() { } function updateValues(textPath) { - byId("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|"); - byId("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset")); - byId("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size")); - let letterSpacingSize = textPath.getAttribute("letter-spacing") ? textPath.getAttribute("letter-spacing") : 0; - byId("labelLetterSpacingSize").value = parseFloat(letterSpacingSize); + if (currentLabelData && currentLabelData.type === "custom") { + // Custom labels: read all values from data model + byId("labelText").value = currentLabelData.text || ""; + byId("labelStartOffset").value = currentLabelData.startOffset || 50; + byId("labelRelativeSize").value = currentLabelData.fontSize || 100; + byId("labelLetterSpacingSize").value = currentLabelData.letterSpacing || 0; + } else { + // State labels and fallback: read from SVG, use data model fontSize if available + byId("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|"); + byId("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset")) || 50; + byId("labelRelativeSize").value = (currentLabelData && currentLabelData.fontSize) || parseFloat(textPath.getAttribute("font-size")) || 100; + let letterSpacingSize = textPath.getAttribute("letter-spacing") ? textPath.getAttribute("letter-spacing") : 0; + byId("labelLetterSpacingSize").value = parseFloat(letterSpacingSize); + } } function drawControlPointsAndLine() { @@ -128,11 +191,13 @@ function editLabel() { .select("#controlPoints") .selectAll("circle") .each(function () { - points.push([this.getAttribute("cx"), this.getAttribute("cy")]); + points.push([+this.getAttribute("cx"), +this.getAttribute("cy")]); }); const d = round(lineGen(points)); path.setAttribute("d", d); debug.select("#controlPoints > path").attr("d", d); + // Sync path control points back to data model + if (currentLabelData) Labels.updateLabel(currentLabelData.i, { pathPoints: points }); } function clickControlPoint() { @@ -187,6 +252,7 @@ function editLabel() { const transform = `translate(${dx + x},${dy + y})`; elSelected.attr("transform", transform); debug.select("#controlPoints").attr("transform", transform); + if (currentLabelData) Labels.updateLabel(currentLabelData.i, { transform }); }); } @@ -205,6 +271,9 @@ function editLabel() { function changeGroup() { byId(this.value).appendChild(elSelected.node()); + if (currentLabelData && currentLabelData.type === "custom") { + Labels.updateLabel(currentLabelData.i, { group: this.value }); + } } function toggleNewGroupInput() { @@ -243,6 +312,9 @@ function editLabel() { if (oldGroup !== "states" && oldGroup !== "addedLabels" && oldGroup.childElementCount === 1) { byId("labelGroupSelect").selectedOptions[0].remove(); byId("labelGroupSelect").options.add(new Option(group, group, false, true)); + // Update data model for labels in the old group + const oldGroupName = oldGroup.id; + Labels.getByGroup(oldGroupName).forEach(l => Labels.updateLabel(l.i, { group })); oldGroup.id = group; toggleNewGroupInput(); byId("labelGroupInput").value = ""; @@ -254,6 +326,10 @@ function editLabel() { newGroup.id = group; byId("labelGroupSelect").options.add(new Option(group, group, false, true)); byId(group).appendChild(elSelected.node()); + // Update data model group for the moved label + if (currentLabelData && currentLabelData.type === "custom") { + Labels.updateLabel(currentLabelData.i, { group }); + } toggleNewGroupInput(); byId("labelGroupInput").value = ""; @@ -263,9 +339,8 @@ function editLabel() { const group = elSelected.node().parentNode.id; const basic = group === "states" || group === "addedLabels"; const count = elSelected.node().parentNode.childElementCount; - alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${ - basic ? "all elements in the group" : "the entire label group" - }?

Labels to be + alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${basic ? "all elements in the group" : "the entire label group" + }?

Labels to be removed: ${count}`; $("#alert").dialog({ resizable: false, @@ -275,6 +350,12 @@ function editLabel() { $(this).dialog("close"); $("#labelEditor").dialog("close"); hideGroupSection(); + // Remove from data model + if (basic && group === "states") { + Labels.removeByType("state"); + } else { + Labels.removeByGroup(group); + } labels .select("#" + group) .selectAll("text") @@ -311,15 +392,17 @@ function editLabel() { el.innerHTML = lines.map((line, index) => `${line}`).join(""); } else el.innerHTML = `${lines}`; + // Update data model + if (currentLabelData) Labels.updateLabel(currentLabelData.i, { text: input }); + if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning"); } function generateRandomName() { let name = ""; - if (elSelected.attr("id").slice(0, 10) === "stateLabel") { - const id = +elSelected.attr("id").slice(10); - const culture = pack.states[id].culture; + if (currentLabelData && currentLabelData.type === "state") { + const culture = pack.states[currentLabelData.stateId].culture; name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture); } else { const box = elSelected.node().getBBox(); @@ -358,17 +441,20 @@ function editLabel() { function changeStartOffset() { elSelected.select("textPath").attr("startOffset", this.value + "%"); + if (currentLabelData) Labels.updateLabel(currentLabelData.i, { startOffset: +this.value }); tip("Label offset: " + this.value + "%"); } function changeRelativeSize() { elSelected.select("textPath").attr("font-size", this.value + "%"); + if (currentLabelData) Labels.updateLabel(currentLabelData.i, { fontSize: +this.value }); tip("Label relative size: " + this.value + "%"); changeText(); } function changeLetterSpacingSize() { elSelected.select("textPath").attr("letter-spacing", this.value + "px"); + if (currentLabelData) Labels.updateLabel(currentLabelData.i, { letterSpacing: +this.value }); tip("Label letter-spacing size: " + this.value + "px"); changeText(); } @@ -379,6 +465,11 @@ function editLabel() { const path = defs.select("#textPath_" + elSelected.attr("id")); path.attr("d", `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`); drawControlPointsAndLine(); + // Sync aligned path to data model + if (currentLabelData) { + const pathEl = byId("textPath_" + elSelected.attr("id")); + Labels.updateLabel(currentLabelData.i, { pathPoints: extractPathPoints(pathEl) }); + } } function editLabelLegend() { @@ -395,6 +486,7 @@ function editLabel() { buttons: { Remove: function () { $(this).dialog("close"); + if (currentLabelData) Labels.removeLabel(currentLabelData.i); defs.select("#textPath_" + elSelected.attr("id")).remove(); elSelected.remove(); $("#labelEditor").dialog("close"); From 2379770bfa49cf78ae4472189905e94ca0a319fc Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 13 Feb 2026 18:12:11 +0100 Subject: [PATCH 06/66] refactor: clean up code formatting and improve timing logic in label generation functions --- src/modules/labels.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 548645c5b..8f24b8225 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -48,7 +48,7 @@ class LabelsModule { return existingIds[existingIds.length - 1] + 1; } - generate() : void { + generate(): void { this.clear(); generateStateLabels(); generateBurgLabels(); @@ -153,8 +153,7 @@ class LabelsModule { * @param list - Optional array of stateIds to regenerate only those */ export function generateStateLabels(list?: number[]): void { - if (!TIME) console.time("generateStateLabels"); - else TIME && console.time("generateStateLabels"); + if (TIME) console.time("generateStateLabels"); const { states } = pack; const labelsModule = window.Labels; @@ -178,8 +177,7 @@ export function generateStateLabels(list?: number[]): void { }); } - if (!TIME) console.timeEnd("generateStateLabels"); - else TIME && console.timeEnd("generateStateLabels"); + if (TIME) console.timeEnd("generateStateLabels"); } /** @@ -200,7 +198,7 @@ export function generateBurgLabels(): void { if (!burg.i || burg.removed) continue; const group = burg.group || "unmarked"; - + // Get label group offset attributes if they exist (will be set during rendering) // For now, use defaults - these will be updated during rendering phase const dx = 0; From 471e865c5e6ce07a1ac6eb0caaee29c1625f04a3 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 13 Feb 2026 18:12:16 +0100 Subject: [PATCH 07/66] refactor: update import path for findClosestCell utility --- src/utils/label-raycast.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/label-raycast.ts b/src/utils/label-raycast.ts index 05e760430..f4f3520c6 100644 --- a/src/utils/label-raycast.ts +++ b/src/utils/label-raycast.ts @@ -1,4 +1,4 @@ -import { findClosestCell } from "./index"; +import { findClosestCell } from "./graphUtils"; export interface Ray { angle: number; From ca6d01f4beff9ae46ef5bf4a2a0b9b8ce4316ab1 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Mon, 16 Feb 2026 19:59:29 +0100 Subject: [PATCH 08/66] feat: synchronize label data model with burg name and position updates --- public/modules/ui/burg-editor.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index 4232d2391..3cdfa3fd1 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -118,6 +118,9 @@ function editBurg(id) { const id = +elSelected.attr("data-id"); pack.burgs[id].name = burgName.value; elSelected.text(burgName.value); + // Sync to Labels data model + const labelData = Labels.getBurgLabel(id); + if (labelData) Labels.updateLabel(labelData.i, {text: burgName.value}); } function generateNameRandom() { @@ -382,6 +385,10 @@ function editBurg(id) { burg.y = y; if (burg.capital) pack.states[newState].center = burg.cell; + // Sync position to Labels data model + const labelData = Labels.getBurgLabel(id); + if (labelData) Labels.updateLabel(labelData.i, {x, y}); + if (d3.event.shiftKey === false) toggleRelocateBurg(); } From 3ab40ada5f0abb40d6343706770e41dad434a0f1 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 17 Feb 2026 01:45:48 +0100 Subject: [PATCH 09/66] feat: migrate label data structure from SVG to data model and update version to 1.113.0 --- public/modules/dynamic/auto-update.js | 152 ++++++++++++++++++++++++++ public/modules/io/load.js | 3 +- public/modules/io/save.js | 4 +- public/versioning.js | 2 +- 4 files changed, 158 insertions(+), 3 deletions(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index 0b1cd227c..e255930d4 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1106,4 +1106,156 @@ export function resolveVersionConflicts(mapVersion) { } } + + if (isOlderThan("1.113.0")) { + // v1.113.0 moved labels data from SVG to data model + // Migrate old SVG labels to pack.labels structure + if (!pack.labels || !pack.labels.length) { + pack.labels = []; + let labelId = 0; + + // Migrate state labels + const stateLabelsGroup = document.querySelector("#labels > #states"); + if (stateLabelsGroup) { + stateLabelsGroup.querySelectorAll("text").forEach(textElement => { + const id = textElement.getAttribute("id"); + if (!id || !id.startsWith("stateLabel")) return; + + const stateIdMatch = id.match(/stateLabel(\d+)/); + if (!stateIdMatch) return; + + const stateId = +stateIdMatch[1]; + const state = pack.states[stateId]; + if (!state || state.removed) return; + + const textPath = textElement.querySelector("textPath"); + if (!textPath) return; + + const text = textPath.textContent.trim(); + const fontSizeAttr = textPath.getAttribute("font-size"); + const fontSize = fontSizeAttr ? parseFloat(fontSizeAttr) : 100; + + pack.labels.push({ + i: labelId++, + type: "state", + stateId: stateId, + text: text, + fontSize: fontSize + }); + }); + } + + // Migrate burg labels + const burgLabelsGroup = document.querySelector("#burgLabels"); + if (burgLabelsGroup) { + burgLabelsGroup.querySelectorAll("g").forEach(groupElement => { + const group = groupElement.getAttribute("id"); + if (!group) return; + + const dxAttr = groupElement.getAttribute("data-dx"); + const dyAttr = groupElement.getAttribute("data-dy"); + const dx = dxAttr ? parseFloat(dxAttr) : 0; + const dy = dyAttr ? parseFloat(dyAttr) : 0; + + groupElement.querySelectorAll("text").forEach(textElement => { + const burgId = +textElement.getAttribute("data-id"); + if (!burgId) return; + + const burg = pack.burgs[burgId]; + if (!burg || burg.removed) return; + + const text = textElement.textContent.trim(); + // Use burg coordinates, not SVG text coordinates + // SVG coordinates may be affected by viewbox transforms + const x = burg.x; + const y = burg.y; + + pack.labels.push({ + i: labelId++, + type: "burg", + burgId: burgId, + group: group, + text: text, + x: x, + y: y, + dx: dx, + dy: dy + }); + }); + }); + } + + // Migrate custom labels + const customLabelsGroup = document.querySelector("#labels > #addedLabels"); + if (customLabelsGroup) { + customLabelsGroup.querySelectorAll("text").forEach(textElement => { + const id = textElement.getAttribute("id"); + if (!id) return; + + const group = "custom"; + const textPath = textElement.querySelector("textPath"); + if (!textPath) return; + + const text = textPath.textContent.trim(); + const fontSizeAttr = textPath.getAttribute("font-size"); + const fontSize = fontSizeAttr ? parseFloat(fontSizeAttr) : 100; + const letterSpacingAttr = textPath.getAttribute("letter-spacing"); + const letterSpacing = letterSpacingAttr ? parseFloat(letterSpacingAttr) : 0; + const startOffsetAttr = textPath.getAttribute("startOffset"); + const startOffset = startOffsetAttr ? parseFloat(startOffsetAttr) : 50; + const transform = textPath.getAttribute("transform"); + + // Get path points from the referenced path + const href = textPath.getAttribute("href"); + if (!href) return; + + const pathId = href.replace("#", ""); + const pathElement = document.getElementById(pathId); + if (!pathElement) return; + + const d = pathElement.getAttribute("d"); + if (!d) return; + + // Parse path data to extract points (simplified - assumes M and L commands) + const pathPoints = []; + const commands = d.match(/[MLZ][^MLZ]*/g); + if (commands) { + commands.forEach(cmd => { + const type = cmd[0]; + if (type === "M" || type === "L") { + const coords = cmd.slice(1).trim().split(/[\s,]+/).map(Number); + if (coords.length >= 2) { + pathPoints.push([coords[0], coords[1]]); + } + } + }); + } + + if (pathPoints.length > 0) { + pack.labels.push({ + i: labelId++, + type: "custom", + group: group, + text: text, + pathPoints: pathPoints, + startOffset: startOffset, + fontSize: fontSize, + letterSpacing: letterSpacing, + transform: transform || undefined + }); + } + }); + } + + // Clear old SVG labels and redraw from data + if (stateLabelsGroup) stateLabelsGroup.querySelectorAll("*").forEach(el => el.remove()); + if (burgLabelsGroup) burgLabelsGroup.querySelectorAll("text").forEach(el => el.remove()); + + // Regenerate labels from data + if (layerIsOn("toggleLabels")) { + drawStateLabels(); + drawBurgLabels(); + } + } + } } diff --git a/public/modules/io/load.js b/public/modules/io/load.js index 9b401733e..441e25fe0 100644 --- a/public/modules/io/load.js +++ b/public/modules/io/load.js @@ -407,6 +407,7 @@ async function parseLoadedData(data, mapVersion) { // data[28] had deprecated cells.crossroad pack.cells.routes = data[36] ? JSON.parse(data[36]) : {}; pack.ice = data[39] ? JSON.parse(data[39]) : []; + pack.labels = data[40] ? JSON.parse(data[40]) : []; if (data[31]) { const namesDL = data[31].split("/"); @@ -473,7 +474,7 @@ async function parseLoadedData(data, mapVersion) { { // dynamically import and run auto-update script - const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.109.4"); + const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.113.0"); resolveVersionConflicts(mapVersion); } diff --git a/public/modules/io/save.js b/public/modules/io/save.js index 25cd7493c..4d6c40ea1 100644 --- a/public/modules/io/save.js +++ b/public/modules/io/save.js @@ -104,6 +104,7 @@ function prepareMapData() { const routes = JSON.stringify(pack.routes); const zones = JSON.stringify(pack.zones); const ice = JSON.stringify(pack.ice); + const labels = JSON.stringify(pack.labels || []); // store name array only if not the same as default const defaultNB = Names.getNameBases(); @@ -158,7 +159,8 @@ function prepareMapData() { cellRoutes, routes, zones, - ice + ice, + labels ].join("\r\n"); return mapData; } diff --git a/public/versioning.js b/public/versioning.js index fd2a67a2d..07993ff8c 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.112.1"; +const VERSION = "1.113.0"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { From 6ab2c03860f268e66ca0436f51e966eb3deb396a Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 17 Feb 2026 01:45:52 +0100 Subject: [PATCH 10/66] fix: prevent error on rendering removed or neutral states in stateLabelsRenderer --- src/renderers/draw-state-labels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index a09de10d7..e42078c6c 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -79,7 +79,7 @@ const stateLabelsRenderer = (list?: number[]): void => { for (const labelData of labelDataList) { const state = states[labelData.stateId]; if (!state.i || state.removed) - throw new Error("State must not be neutral or removed"); + continue; // Calculate pathPoints using raycast algorithm (recalculated on each draw) const offset = getOffsetWidth(state.cells!); From 861db87bff7ec8d4dd1cd43421a5a8dc5b0b87a7 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 17 Feb 2026 01:50:26 +0100 Subject: [PATCH 11/66] refactor: encapsulate label generation functions within LabelsModule --- src/modules/labels.ts | 139 +++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 71 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 8f24b8225..4b89d6c97 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -50,8 +50,8 @@ class LabelsModule { generate(): void { this.clear(); - generateStateLabels(); - generateBurgLabels(); + this.generateStateLabels(); + this.generateBurgLabels(); } getAll(): LabelData[] { @@ -145,78 +145,75 @@ class LabelsModule { clear(): void { pack.labels = []; } -} - -/** - * Generate state labels data entries for each state. - * Only stores essential label data; raycast path calculation happens during rendering. - * @param list - Optional array of stateIds to regenerate only those - */ -export function generateStateLabels(list?: number[]): void { - if (TIME) console.time("generateStateLabels"); - - const { states } = pack; - const labelsModule = window.Labels; - - // Remove existing state labels that need regeneration - if (list) { - list.forEach((stateId) => labelsModule.removeStateLabel(stateId)); - } else { - labelsModule.removeByType("state"); - } - - // Generate new label entries - for (const state of states) { - if (!state.i || state.removed || state.lock) continue; - if (list && !list.includes(state.i)) continue; - - labelsModule.addStateLabel({ - stateId: state.i, - text: state.name!, - fontSize: 100, - }); + + /** + * Generate state labels data entries for each state. + * Only stores essential label data; raycast path calculation happens during rendering. + * @param list - Optional array of stateIds to regenerate only those + */ + generateStateLabels(list?: number[]): void { + if (TIME) console.time("generateStateLabels"); + + const { states } = pack; + + // Remove existing state labels that need regeneration + if (list) { + list.forEach((stateId) => this.removeStateLabel(stateId)); + } else { + this.removeByType("state"); + } + + // Generate new label entries + for (const state of states) { + if (!state.i || state.removed || state.lock) continue; + if (list && !list.includes(state.i)) continue; + + this.addStateLabel({ + stateId: state.i, + text: state.name!, + fontSize: 100, + }); + } + + if (TIME) console.timeEnd("generateStateLabels"); + } + + /** + * Generate burg labels data from burgs. + * Populates pack.labels with BurgLabelData for each burg. + */ + generateBurgLabels(): void { + if (TIME) console.time("generateBurgLabels"); + + // Remove existing burg labels + this.removeByType("burg"); + + // Generate new labels for all active burgs + for (const burg of pack.burgs) { + if (!burg.i || burg.removed) continue; + + const group = burg.group || "unmarked"; + + // Get label group offset attributes if they exist (will be set during rendering) + // For now, use defaults - these will be updated during rendering phase + const dx = 0; + const dy = 0; + + this.addBurgLabel({ + burgId: burg.i, + group, + text: burg.name!, + x: burg.x, + y: burg.y, + dx, + dy, + }); + } + + if (TIME) console.timeEnd("generateBurgLabels"); } - if (TIME) console.timeEnd("generateStateLabels"); } -/** - * Generate burg labels data from burgs. - * Populates pack.labels with BurgLabelData for each burg. - */ -export function generateBurgLabels(): void { - if (!TIME) console.time("generateBurgLabels"); - else TIME && console.time("generateBurgLabels"); - - const labelsModule = window.Labels; - - // Remove existing burg labels - labelsModule.removeByType("burg"); - - // Generate new labels for all active burgs - for (const burg of pack.burgs) { - if (!burg.i || burg.removed) continue; - - const group = burg.group || "unmarked"; - - // Get label group offset attributes if they exist (will be set during rendering) - // For now, use defaults - these will be updated during rendering phase - const dx = 0; - const dy = 0; - - labelsModule.addBurgLabel({ - burgId: burg.i, - group, - text: burg.name!, - x: burg.x, - y: burg.y, - dx, - dy, - }); - } - - if (!TIME) console.timeEnd("generateBurgLabels"); - else TIME && console.timeEnd("generateBurgLabels"); -} window.Labels = new LabelsModule(); From 0d56479e007aa69b983075bf85c2dde0724a2ef3 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 17 Feb 2026 01:51:08 +0100 Subject: [PATCH 12/66] refactor: update import path for label-raycast and improve state label rendering logic --- src/renderers/draw-state-labels.ts | 6 +++--- src/{utils => renderers}/label-raycast.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/{utils => renderers}/label-raycast.ts (98%) diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index e42078c6c..307423bde 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -11,7 +11,7 @@ import { raycast, findBestRayPair, ANGLES, -} from "../utils/label-raycast"; +} from "./label-raycast"; declare global { var drawStateLabels: (list?: number[]) => void; @@ -57,8 +57,8 @@ const stateLabelsRenderer = (list?: number[]): void => { // Get labels to render const labelsToRender = list ? Labels.getAll() - .filter((l) => l.type === "state" && list.includes((l as StateLabelData).stateId)) - .map((l) => l as StateLabelData) + .filter((l) => l.type === "state" && list.includes((l as StateLabelData).stateId)) + .map((l) => l as StateLabelData) : Labels.getByType("state").map((l) => l as StateLabelData); const letterLength = checkExampleLetterLength(); diff --git a/src/utils/label-raycast.ts b/src/renderers/label-raycast.ts similarity index 98% rename from src/utils/label-raycast.ts rename to src/renderers/label-raycast.ts index f4f3520c6..c02680875 100644 --- a/src/utils/label-raycast.ts +++ b/src/renderers/label-raycast.ts @@ -1,4 +1,4 @@ -import { findClosestCell } from "./graphUtils"; +import { findClosestCell } from "../utils/graphUtils"; export interface Ray { angle: number; From c22e6eb0c57dc2d21e7f93c63ec3185b0e532c45 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 17 Feb 2026 02:11:35 +0100 Subject: [PATCH 13/66] chore: update script version numbers to 1.113.0 in index.html --- src/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.html b/src/index.html index 7b3f031d4..9ebb271b2 100644 --- a/src/index.html +++ b/src/index.html @@ -8509,7 +8509,7 @@ - + @@ -8527,12 +8527,12 @@ - + - + @@ -8555,8 +8555,8 @@ - - + + From 32e70496da9477044aee50e3652e2b84cb73a8b4 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 19 Feb 2026 19:13:11 +0100 Subject: [PATCH 14/66] refactor: replace currentLabelData with direct calls to getLabelData in editLabel function --- public/modules/ui/labels-editor.js | 58 +++++++++++++++++------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index 32d1c2c74..647eb7bff 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -1,5 +1,4 @@ "use strict"; -let currentLabelData = null; // Helper: extract control points from an SVG path element function extractPathPoints(pathElement) { @@ -61,9 +60,6 @@ function editLabel() { elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true); viewbox.on("touchmove mousemove", showEditorTips); - // Resolve label data from the data model - currentLabelData = getLabelData(text); - $("#labelEditor").dialog({ title: "Edit Label", resizable: false, @@ -136,17 +132,18 @@ function editLabel() { } function updateValues(textPath) { - if (currentLabelData && currentLabelData.type === "custom") { + const labelData = getLabelData(elSelected.node()); + if (labelData && labelData.type === "custom") { // Custom labels: read all values from data model - byId("labelText").value = currentLabelData.text || ""; - byId("labelStartOffset").value = currentLabelData.startOffset || 50; - byId("labelRelativeSize").value = currentLabelData.fontSize || 100; - byId("labelLetterSpacingSize").value = currentLabelData.letterSpacing || 0; + byId("labelText").value = labelData.text || ""; + byId("labelStartOffset").value = labelData.startOffset || 50; + byId("labelRelativeSize").value = labelData.fontSize || 100; + byId("labelLetterSpacingSize").value = labelData.letterSpacing || 0; } else { // State labels and fallback: read from SVG, use data model fontSize if available byId("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|"); byId("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset")) || 50; - byId("labelRelativeSize").value = (currentLabelData && currentLabelData.fontSize) || parseFloat(textPath.getAttribute("font-size")) || 100; + byId("labelRelativeSize").value = (labelData && labelData.fontSize) || parseFloat(textPath.getAttribute("font-size")) || 100; let letterSpacingSize = textPath.getAttribute("letter-spacing") ? textPath.getAttribute("letter-spacing") : 0; byId("labelLetterSpacingSize").value = parseFloat(letterSpacingSize); } @@ -197,7 +194,8 @@ function editLabel() { path.setAttribute("d", d); debug.select("#controlPoints > path").attr("d", d); // Sync path control points back to data model - if (currentLabelData) Labels.updateLabel(currentLabelData.i, { pathPoints: points }); + const labelData = getLabelData(elSelected.node()); + if (labelData) Labels.updateLabel(labelData.i, { pathPoints: points }); } function clickControlPoint() { @@ -252,7 +250,8 @@ function editLabel() { const transform = `translate(${dx + x},${dy + y})`; elSelected.attr("transform", transform); debug.select("#controlPoints").attr("transform", transform); - if (currentLabelData) Labels.updateLabel(currentLabelData.i, { transform }); + const labelData = getLabelData(elSelected.node()); + if (labelData) Labels.updateLabel(labelData.i, { transform }); }); } @@ -271,8 +270,9 @@ function editLabel() { function changeGroup() { byId(this.value).appendChild(elSelected.node()); - if (currentLabelData && currentLabelData.type === "custom") { - Labels.updateLabel(currentLabelData.i, { group: this.value }); + const labelData = getLabelData(elSelected.node()); + if (labelData && labelData.type === "custom") { + Labels.updateLabel(labelData.i, { group: this.value }); } } @@ -327,8 +327,9 @@ function editLabel() { byId("labelGroupSelect").options.add(new Option(group, group, false, true)); byId(group).appendChild(elSelected.node()); // Update data model group for the moved label - if (currentLabelData && currentLabelData.type === "custom") { - Labels.updateLabel(currentLabelData.i, { group }); + const labelData = getLabelData(elSelected.node()); + if (labelData && labelData.type === "custom") { + Labels.updateLabel(labelData.i, { group }); } toggleNewGroupInput(); @@ -393,7 +394,8 @@ function editLabel() { } else el.innerHTML = `${lines}`; // Update data model - if (currentLabelData) Labels.updateLabel(currentLabelData.i, { text: input }); + const labelData = getLabelData(elSelected.node()); + if (labelData) Labels.updateLabel(labelData.i, { text: input }); if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning"); @@ -401,8 +403,9 @@ function editLabel() { function generateRandomName() { let name = ""; - if (currentLabelData && currentLabelData.type === "state") { - const culture = pack.states[currentLabelData.stateId].culture; + const labelData = getLabelData(elSelected.node()); + if (labelData && labelData.type === "state") { + const culture = pack.states[labelData.stateId].culture; name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture); } else { const box = elSelected.node().getBBox(); @@ -441,20 +444,23 @@ function editLabel() { function changeStartOffset() { elSelected.select("textPath").attr("startOffset", this.value + "%"); - if (currentLabelData) Labels.updateLabel(currentLabelData.i, { startOffset: +this.value }); + const labelData = getLabelData(elSelected.node()); + if (labelData) Labels.updateLabel(labelData.i, { startOffset: +this.value }); tip("Label offset: " + this.value + "%"); } function changeRelativeSize() { elSelected.select("textPath").attr("font-size", this.value + "%"); - if (currentLabelData) Labels.updateLabel(currentLabelData.i, { fontSize: +this.value }); + const labelData = getLabelData(elSelected.node()); + if (labelData) Labels.updateLabel(labelData.i, { fontSize: +this.value }); tip("Label relative size: " + this.value + "%"); changeText(); } function changeLetterSpacingSize() { elSelected.select("textPath").attr("letter-spacing", this.value + "px"); - if (currentLabelData) Labels.updateLabel(currentLabelData.i, { letterSpacing: +this.value }); + const labelData = getLabelData(elSelected.node()); + if (labelData) Labels.updateLabel(labelData.i, { letterSpacing: +this.value }); tip("Label letter-spacing size: " + this.value + "px"); changeText(); } @@ -466,9 +472,10 @@ function editLabel() { path.attr("d", `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`); drawControlPointsAndLine(); // Sync aligned path to data model - if (currentLabelData) { + const labelData = getLabelData(elSelected.node()); + if (labelData) { const pathEl = byId("textPath_" + elSelected.attr("id")); - Labels.updateLabel(currentLabelData.i, { pathPoints: extractPathPoints(pathEl) }); + Labels.updateLabel(labelData.i, { pathPoints: extractPathPoints(pathEl) }); } } @@ -486,7 +493,8 @@ function editLabel() { buttons: { Remove: function () { $(this).dialog("close"); - if (currentLabelData) Labels.removeLabel(currentLabelData.i); + const labelData = getLabelData(elSelected.node()); + if (labelData) Labels.removeLabel(labelData.i); defs.select("#textPath_" + elSelected.attr("id")).remove(); elSelected.remove(); $("#labelEditor").dialog("close"); From 6d99d8260d71bd62311fc5097690474bd24ec795 Mon Sep 17 00:00:00 2001 From: kruschen Date: Thu, 19 Feb 2026 19:42:18 +0100 Subject: [PATCH 15/66] Fix spelling in label-raycast.ts [Copilot] Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/renderers/label-raycast.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/label-raycast.ts b/src/renderers/label-raycast.ts index c02680875..342135cf5 100644 --- a/src/renderers/label-raycast.ts +++ b/src/renderers/label-raycast.ts @@ -23,7 +23,7 @@ interface RaycastParams { offset: number; } -// increase step to 15 or 30 to make it faster and more horyzontal +// increase step to 15 or 30 to make it faster and more horizontal // decrease step to 5 to improve accuracy const ANGLE_STEP = 9; export const ANGLES = precalculateAngles(ANGLE_STEP); From 6fa3f786dc384301a3acfefea629a9d0cc1ea7e2 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 19 Feb 2026 20:07:48 +0100 Subject: [PATCH 16/66] refactor: improve code formatting and organization in labels and renderers --- src/modules/labels.ts | 57 +++++++++++++++--------------- src/renderers/draw-state-labels.ts | 39 +++++++++----------- src/renderers/label-raycast.ts | 6 +--- src/types/PackedGraph.ts | 2 +- 4 files changed, 48 insertions(+), 56 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 4b89d6c97..a607ee72f 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -79,15 +79,17 @@ class LabelsModule { } getBurgLabel(burgId: number): BurgLabelData | undefined { - return pack.labels.find( - (l) => l.type === "burg" && l.burgId === burgId, - ) as BurgLabelData | undefined; + return pack.labels.find((l) => l.type === "burg" && l.burgId === burgId) as + | BurgLabelData + | undefined; } - addStateLabel( - data: Omit, - ): StateLabelData { - const label: StateLabelData = { i: this.getNextId(), type: "state", ...data }; + addStateLabel(data: Omit): StateLabelData { + const label: StateLabelData = { + i: this.getNextId(), + type: "state", + ...data, + }; pack.labels.push(label); return label; } @@ -98,10 +100,12 @@ class LabelsModule { return label; } - addCustomLabel( - data: Omit, - ): CustomLabelData { - const label: CustomLabelData = { i: this.getNextId(), type: "custom", ...data }; + addCustomLabel(data: Omit): CustomLabelData { + const label: CustomLabelData = { + i: this.getNextId(), + type: "custom", + ...data, + }; pack.labels.push(label); return label; } @@ -123,8 +127,7 @@ class LabelsModule { removeByGroup(group: string): void { pack.labels = pack.labels.filter( - (l) => - !((l.type === "burg" || l.type === "custom") && l.group === group), + (l) => !((l.type === "burg" || l.type === "custom") && l.group === group), ); } @@ -145,7 +148,7 @@ class LabelsModule { clear(): void { pack.labels = []; } - + /** * Generate state labels data entries for each state. * Only stores essential label data; raycast path calculation happens during rendering. @@ -153,52 +156,52 @@ class LabelsModule { */ generateStateLabels(list?: number[]): void { if (TIME) console.time("generateStateLabels"); - + const { states } = pack; - + // Remove existing state labels that need regeneration if (list) { list.forEach((stateId) => this.removeStateLabel(stateId)); } else { this.removeByType("state"); } - + // Generate new label entries for (const state of states) { if (!state.i || state.removed || state.lock) continue; if (list && !list.includes(state.i)) continue; - + this.addStateLabel({ stateId: state.i, text: state.name!, fontSize: 100, }); } - + if (TIME) console.timeEnd("generateStateLabels"); } - + /** * Generate burg labels data from burgs. * Populates pack.labels with BurgLabelData for each burg. */ generateBurgLabels(): void { if (TIME) console.time("generateBurgLabels"); - + // Remove existing burg labels this.removeByType("burg"); - + // Generate new labels for all active burgs for (const burg of pack.burgs) { if (!burg.i || burg.removed) continue; - + const group = burg.group || "unmarked"; - + // Get label group offset attributes if they exist (will be set during rendering) // For now, use defaults - these will be updated during rendering phase const dx = 0; const dy = 0; - + this.addBurgLabel({ burgId: burg.i, group, @@ -209,11 +212,9 @@ class LabelsModule { dy, }); } - + if (TIME) console.timeEnd("generateBurgLabels"); } - } - window.Labels = new LabelsModule(); diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index 307423bde..7015abaeb 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -1,17 +1,7 @@ import { curveNatural, line, max, select } from "d3"; -import { - findClosestCell, - minmax, - rn, - round, - splitInTwo, -} from "../utils"; import type { StateLabelData } from "../modules/labels"; -import { - raycast, - findBestRayPair, - ANGLES, -} from "./label-raycast"; +import { findClosestCell, minmax, rn, round, splitInTwo } from "../utils"; +import { ANGLES, findBestRayPair, raycast } from "./label-raycast"; declare global { var drawStateLabels: (list?: number[]) => void; @@ -57,8 +47,11 @@ const stateLabelsRenderer = (list?: number[]): void => { // Get labels to render const labelsToRender = list ? Labels.getAll() - .filter((l) => l.type === "state" && list.includes((l as StateLabelData).stateId)) - .map((l) => l as StateLabelData) + .filter( + (l) => + l.type === "state" && list.includes((l as StateLabelData).stateId), + ) + .map((l) => l as StateLabelData) : Labels.getByType("state").map((l) => l as StateLabelData); const letterLength = checkExampleLetterLength(); @@ -67,7 +60,10 @@ const stateLabelsRenderer = (list?: number[]): void => { // restore labels visibility labels.style("display", layerDisplay); - function drawLabelPath(letterLength: number, labelDataList: StateLabelData[]): void { + function drawLabelPath( + letterLength: number, + labelDataList: StateLabelData[], + ): void { const mode = options.stateLabelsMode || "auto"; const lineGen = line<[number, number]>().curve(curveNatural); @@ -78,8 +74,7 @@ const stateLabelsRenderer = (list?: number[]): void => { for (const labelData of labelDataList) { const state = states[labelData.stateId]; - if (!state.i || state.removed) - continue; + if (!state.i || state.removed) continue; // Calculate pathPoints using raycast algorithm (recalculated on each draw) const offset = getOffsetWidth(state.cells!); @@ -161,16 +156,16 @@ const stateLabelsRenderer = (list?: number[]): void => { textElement.insertAdjacentHTML("afterbegin", spans.join("")); const { width, height } = textElement.getBBox(); - textElement.setAttribute("href", `#textPath_stateLabel${labelData.stateId}`); + textElement.setAttribute( + "href", + `#textPath_stateLabel${labelData.stateId}`, + ); const stateIds = pack.cells.state; if (mode === "full" || lines.length === 1) continue; // check if label fits state boundaries. If no, replace it with short name - const [[x1, y1], [x2, y2]] = [ - pathPoints.at(0)!, - pathPoints.at(-1)!, - ]; + const [[x1, y1], [x2, y2]] = [pathPoints.at(0)!, pathPoints.at(-1)!]; const angleRad = Math.atan2(y2 - y1, x2 - x1); const isInsideState = checkIfInsideState( diff --git a/src/renderers/label-raycast.ts b/src/renderers/label-raycast.ts index 342135cf5..41b394142 100644 --- a/src/renderers/label-raycast.ts +++ b/src/renderers/label-raycast.ts @@ -49,11 +49,7 @@ export function raycast({ const stateIds = cells.state; let ray = { length: 0, x: x0, y: y0 }; - for ( - let length = LENGTH_START; - length < LENGTH_MAX; - length += LENGTH_STEP - ) { + for (let length = LENGTH_START; length < LENGTH_MAX; length += LENGTH_STEP) { const [x, y] = [x0 + length * dx, y0 + length * dy]; // offset points are perpendicular to the ray const offset1: [number, number] = [x + -dy * offset, y + dx * offset]; diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts index f54d81576..8c1fed100 100644 --- a/src/types/PackedGraph.ts +++ b/src/types/PackedGraph.ts @@ -1,11 +1,11 @@ import type { Burg } from "../modules/burgs-generator"; import type { Culture } from "../modules/cultures-generator"; import type { PackedGraphFeature } from "../modules/features"; +import type { LabelData } from "../modules/labels"; import type { Province } from "../modules/provinces-generator"; import type { River } from "../modules/river-generator"; import type { Route } from "../modules/routes-generator"; import type { State } from "../modules/states-generator"; -import type { LabelData } from "../modules/labels"; import type { Zone } from "../modules/zones-generator"; type TypedArray = From e1740567c6745e21cc0cb35369000e3d53534316 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 21 Feb 2026 20:37:58 +0100 Subject: [PATCH 17/66] refactor: optimize label rendering by building HTML string for batch updates --- src/renderers/draw-burg-labels.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index 864395599..1a78cfe4b 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -35,22 +35,23 @@ const burgLabelsRenderer = (): void => { const dx = dxAttr ? parseFloat(dxAttr) : 0; const dy = dyAttr ? parseFloat(dyAttr) : 0; + // Build HTML string for all labels in this group + const labelsHTML: string[] = []; for (const labelData of labels) { // Update label data with SVG group offsets if (labelData.dx !== dx || labelData.dy !== dy) { Labels.updateLabel(labelData.i, { dx, dy }); } - labelGroup - .append("text") - .attr("text-rendering", "optimizeSpeed") - .attr("id", `burgLabel${labelData.burgId}`) - .attr("data-id", labelData.burgId) - .attr("x", labelData.x) - .attr("y", labelData.y) - .attr("dx", `${dx}em`) - .attr("dy", `${dy}em`) - .text(labelData.text); + labelsHTML.push( + `${labelData.text}` + ); + } + + // Set all labels at once + const groupNode = labelGroup.node(); + if (groupNode) { + groupNode.innerHTML = labelsHTML.join(""); } } From fd3200739f954bce83fe6d4768c97c3f82c1b84e Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 24 Feb 2026 19:06:22 +0100 Subject: [PATCH 18/66] apply format --- src/renderers/draw-burg-labels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index 1a78cfe4b..e59f14f64 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -44,7 +44,7 @@ const burgLabelsRenderer = (): void => { } labelsHTML.push( - `${labelData.text}` + `${labelData.text}`, ); } From 3927a762fccf12458ee3b96c9f7e1425a59ce104 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 27 Feb 2026 01:23:22 +0100 Subject: [PATCH 19/66] chore: update version to 1.114.0 and adjust related script references + fix migration --- public/modules/dynamic/auto-update.js | 15 ++++++++++----- public/versioning.js | 2 +- src/index.html | 10 +++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index e255930d4..aaf635396 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1107,8 +1107,8 @@ export function resolveVersionConflicts(mapVersion) { } - if (isOlderThan("1.113.0")) { - // v1.113.0 moved labels data from SVG to data model + if (isOlderThan("1.114.0")) { + // v1.114.0 moved labels data from SVG to data model // Migrate old SVG labels to pack.labels structure if (!pack.labels || !pack.labels.length) { pack.labels = []; @@ -1206,7 +1206,7 @@ export function resolveVersionConflicts(mapVersion) { const transform = textPath.getAttribute("transform"); // Get path points from the referenced path - const href = textPath.getAttribute("href"); + const href = textPath.getAttribute("xlink:href") || textPath.getAttribute("href"); if (!href) return; const pathId = href.replace("#", ""); @@ -1216,9 +1216,9 @@ export function resolveVersionConflicts(mapVersion) { const d = pathElement.getAttribute("d"); if (!d) return; - // Parse path data to extract points (simplified - assumes M and L commands) + // Parse path data to extract points(M, L and C commands) const pathPoints = []; - const commands = d.match(/[MLZ][^MLZ]*/g); + const commands = d.match(/[MLC][^MLC]*/g); if (commands) { commands.forEach(cmd => { const type = cmd[0]; @@ -1227,6 +1227,11 @@ export function resolveVersionConflicts(mapVersion) { if (coords.length >= 2) { pathPoints.push([coords[0], coords[1]]); } + } else if (type === "C") { + const coords = cmd.slice(1).trim().split(/[\s,]+/).map(Number); + if (coords.length >= 6) { + pathPoints.push([coords[4], coords[5]]); + } } }); } diff --git a/public/versioning.js b/public/versioning.js index d7620449e..ba2b4e45c 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.113.0"; +const VERSION = "1.114.0"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { diff --git a/src/index.html b/src/index.html index e5d2c5f69..878f3947b 100644 --- a/src/index.html +++ b/src/index.html @@ -8506,7 +8506,7 @@ - + @@ -8524,12 +8524,12 @@ - + - + @@ -8551,8 +8551,8 @@ - - + + From fab495fb8bdc272fd846a65a258191c2164cbb07 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Wed, 4 Mar 2026 23:11:52 +0100 Subject: [PATCH 20/66] refactor: streamline label management by integrating label removal and rendering functions --- src/modules/burgs-generator.ts | 3 +- src/modules/labels.ts | 12 +----- src/renderers/draw-burg-labels.ts | 65 +++++++++---------------------- 3 files changed, 22 insertions(+), 58 deletions(-) diff --git a/src/modules/burgs-generator.ts b/src/modules/burgs-generator.ts index 6fccd517f..29794222b 100644 --- a/src/modules/burgs-generator.ts +++ b/src/modules/burgs-generator.ts @@ -1,5 +1,6 @@ import { quadtree } from "d3-quadtree"; import { byId, each, gauss, minmax, normalize, P, rn } from "../utils"; +import { drawBurgLabel } from "../renderers/draw-burg-labels"; declare global { var Burgs: BurgModule; @@ -728,7 +729,7 @@ class BurgModule { } removeBurgIcon(burg.i!); - removeBurgLabel(burg.i!); + Labels.removeBurgLabel(burg.i!); } } window.Burgs = new BurgModule(); diff --git a/src/modules/labels.ts b/src/modules/labels.ts index a607ee72f..94ef689f9 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -1,3 +1,4 @@ + declare global { var Labels: LabelsModule; } @@ -18,8 +19,6 @@ export interface BurgLabelData { text: string; x: number; y: number; - dx: number; - dy: number; } export interface CustomLabelData { @@ -197,19 +196,12 @@ class LabelsModule { const group = burg.group || "unmarked"; - // Get label group offset attributes if they exist (will be set during rendering) - // For now, use defaults - these will be updated during rendering phase - const dx = 0; - const dy = 0; - this.addBurgLabel({ burgId: burg.i, group, text: burg.name!, x: burg.x, - y: burg.y, - dx, - dy, + y: burg.y }); } diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index e59f14f64..6fcd0e4bf 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -1,18 +1,20 @@ import type { Burg } from "../modules/burgs-generator"; import type { BurgLabelData } from "../modules/labels"; -declare global { - var drawBurgLabels: () => void; - var drawBurgLabel: (burg: Burg) => void; - var removeBurgLabel: (burgId: number) => void; -} - interface BurgGroup { name: string; order: number; } -const burgLabelsRenderer = (): void => { +// remove this section once layer.js is refactored-------------------------------- +declare global { + var drawBurgLabels: () => void; +} + +window.drawBurgLabels = drawBurgLabels; +// section end ------------------------------------------------------------------- + +export function drawBurgLabels(): void { TIME && console.time("drawBurgLabels"); createLabelGroups(); @@ -30,19 +32,13 @@ const burgLabelsRenderer = (): void => { const labelGroup = burgLabels.select(`#${groupName}`); if (labelGroup.empty()) continue; - const dxAttr = labelGroup.attr("data-dx"); - const dyAttr = labelGroup.attr("data-dy"); + const dxAttr = style.burgLabels?.[groupName]?.["data-dx"]; + const dyAttr = style.burgLabels?.[groupName]?.["data-dy"]; const dx = dxAttr ? parseFloat(dxAttr) : 0; const dy = dyAttr ? parseFloat(dyAttr) : 0; - // Build HTML string for all labels in this group const labelsHTML: string[] = []; for (const labelData of labels) { - // Update label data with SVG group offsets - if (labelData.dx !== dx || labelData.dy !== dy) { - Labels.updateLabel(labelData.i, { dx, dy }); - } - labelsHTML.push( `${labelData.text}`, ); @@ -58,10 +54,11 @@ const burgLabelsRenderer = (): void => { TIME && console.timeEnd("drawBurgLabels"); }; -const drawBurgLabelRenderer = (burg: Burg): void => { +export function drawBurgLabel(burg: Burg): void { + // TODO: remove label group dependency - for now, if group is missing, redraw all labels to recreate the group const labelGroup = burgLabels.select(`#${burg.group}`); if (labelGroup.empty()) { - burgLabelsRenderer(); + drawBurgLabels(); return; // redraw all labels if group is missing } @@ -70,29 +67,8 @@ const drawBurgLabelRenderer = (burg: Burg): void => { const dx = dxAttr ? parseFloat(dxAttr) : 0; const dy = dyAttr ? parseFloat(dyAttr) : 0; - removeBurgLabelRenderer(burg.i!); - - // Add/update label in data layer - const existingLabel = Labels.getBurgLabel(burg.i!); - if (existingLabel) { - Labels.updateLabel(existingLabel.i, { - text: burg.name!, - x: burg.x, - y: burg.y, - dx, - dy, - }); - } else { - Labels.addBurgLabel({ - burgId: burg.i!, - group: burg.group || "unmarked", - text: burg.name!, - x: burg.x, - y: burg.y, - dx, - dy, - }); - } + const existingLabel = document.getElementById(`burgLabel${burg.i}`); + if (existingLabel) existingLabel.remove(); // Render to SVG labelGroup @@ -107,10 +83,9 @@ const drawBurgLabelRenderer = (burg: Burg): void => { .text(burg.name!); }; -const removeBurgLabelRenderer = (burgId: number): void => { +export function removeBurgLabel(burgId: number): void { const existingLabel = document.getElementById(`burgLabel${burgId}`); if (existingLabel) existingLabel.remove(); - Labels.removeBurgLabel(burgId); }; function createLabelGroups(): void { @@ -140,8 +115,4 @@ function createLabelGroups(): void { }); group.attr("id", name); } -} - -window.drawBurgLabels = burgLabelsRenderer; -window.drawBurgLabel = drawBurgLabelRenderer; -window.removeBurgLabel = removeBurgLabelRenderer; +} \ No newline at end of file From 25824fe39ce5e7e3c7ac426b6faaa7eed1d1f6f7 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Wed, 4 Mar 2026 23:21:58 +0100 Subject: [PATCH 21/66] fix: regenerateStateLabels --- public/modules/ui/tools.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/modules/ui/tools.js b/public/modules/ui/tools.js index eade993f4..b4734c4bf 100644 --- a/public/modules/ui/tools.js +++ b/public/modules/ui/tools.js @@ -77,6 +77,7 @@ toolsContent.addEventListener("click", function (event) { function processFeatureRegeneration(event, button) { if (button === "regenerateStateLabels") { $("#labels").fadeIn(); + Labels.generateStateLabels(); drawStateLabels(); } else if (button === "regenerateReliefIcons") { drawReliefIcons(); From 5f3592412bc14196562ab75783bcdc33fd9cc779 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sun, 8 Mar 2026 01:31:11 +0100 Subject: [PATCH 22/66] refactor: update label indexing to use current length for state and custom labels --- public/modules/dynamic/auto-update.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index aaf635396..be2e535ac 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1136,7 +1136,7 @@ export function resolveVersionConflicts(mapVersion) { const fontSize = fontSizeAttr ? parseFloat(fontSizeAttr) : 100; pack.labels.push({ - i: labelId++, + i: pack.labels.length, type: "state", stateId: stateId, text: text, @@ -1238,7 +1238,7 @@ export function resolveVersionConflicts(mapVersion) { if (pathPoints.length > 0) { pack.labels.push({ - i: labelId++, + i: pack.labels.length, type: "custom", group: group, text: text, From 967e8897ed9747af66307c799a61b2d681c4f167 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 12 Mar 2026 00:13:00 +0100 Subject: [PATCH 23/66] simplify generateStateLabels method and removed removeStateLabel method --- src/modules/labels.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 94ef689f9..35088d713 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -130,13 +130,6 @@ class LabelsModule { ); } - removeStateLabel(stateId: number): void { - const index = pack.labels.findIndex( - (l) => l.type === "state" && l.stateId === stateId, - ); - if (index !== -1) pack.labels.splice(index, 1); - } - removeBurgLabel(burgId: number): void { const index = pack.labels.findIndex( (l) => l.type === "burg" && l.burgId === burgId, @@ -153,22 +146,17 @@ class LabelsModule { * Only stores essential label data; raycast path calculation happens during rendering. * @param list - Optional array of stateIds to regenerate only those */ - generateStateLabels(list?: number[]): void { + generateStateLabels(): void { if (TIME) console.time("generateStateLabels"); const { states } = pack; // Remove existing state labels that need regeneration - if (list) { - list.forEach((stateId) => this.removeStateLabel(stateId)); - } else { - this.removeByType("state"); - } + this.removeByType("state"); // Generate new label entries for (const state of states) { if (!state.i || state.removed || state.lock) continue; - if (list && !list.includes(state.i)) continue; this.addStateLabel({ stateId: state.i, From 9b4add5071d7b831ebbdd72648b26ec34160458a Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 27 Mar 2026 20:18:50 +0100 Subject: [PATCH 24/66] remove getByType out of lables.ts --- src/modules/labels.ts | 7 +------ src/renderers/draw-burg-labels.ts | 11 ++++++----- src/renderers/draw-state-labels.ts | 14 ++++++-------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 35088d713..8dce3d7f1 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -1,4 +1,3 @@ - declare global { var Labels: LabelsModule; } @@ -61,10 +60,6 @@ class LabelsModule { return pack.labels.find((l) => l.i === id); } - getByType(type: LabelData["type"]): LabelData[] { - return pack.labels.filter((l) => l.type === type); - } - getByGroup(group: string): LabelData[] { return pack.labels.filter( (l) => (l.type === "burg" || l.type === "custom") && l.group === group, @@ -189,7 +184,7 @@ class LabelsModule { group, text: burg.name!, x: burg.x, - y: burg.y + y: burg.y, }); } diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index 6fcd0e4bf..b13261298 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -20,7 +20,8 @@ export function drawBurgLabels(): void { // Get all burg labels grouped by group name const burgLabelsByGroup = new Map(); - for (const label of Labels.getByType("burg").map((l) => l as BurgLabelData)) { + for (const label of Labels.getAll()) { + if (label.type !== "burg") continue; if (!burgLabelsByGroup.has(label.group)) { burgLabelsByGroup.set(label.group, []); } @@ -52,7 +53,7 @@ export function drawBurgLabels(): void { } TIME && console.timeEnd("drawBurgLabels"); -}; +} export function drawBurgLabel(burg: Burg): void { // TODO: remove label group dependency - for now, if group is missing, redraw all labels to recreate the group @@ -81,12 +82,12 @@ export function drawBurgLabel(burg: Burg): void { .attr("dx", `${dx}em`) .attr("dy", `${dy}em`) .text(burg.name!); -}; +} export function removeBurgLabel(burgId: number): void { const existingLabel = document.getElementById(`burgLabel${burgId}`); if (existingLabel) existingLabel.remove(); -}; +} function createLabelGroups(): void { // save existing styles and remove all groups @@ -115,4 +116,4 @@ function createLabelGroups(): void { }); group.attr("id", name); } -} \ No newline at end of file +} diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index 7015abaeb..c0f15bcea 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -45,14 +45,12 @@ const stateLabelsRenderer = (list?: number[]): void => { const { states } = pack; // Get labels to render - const labelsToRender = list - ? Labels.getAll() - .filter( - (l) => - l.type === "state" && list.includes((l as StateLabelData).stateId), - ) - .map((l) => l as StateLabelData) - : Labels.getByType("state").map((l) => l as StateLabelData); + const labelsToRender: StateLabelData[] = + list && list.length > 0 + ? list + .map((idx) => Labels.get(idx)) + .filter((label) => label?.type === "state") + : Labels.getAll().filter((label) => label.type === "state"); const letterLength = checkExampleLetterLength(); drawLabelPath(letterLength, labelsToRender); From baeab354b646bd7afc535d1f1bdc1ced3f446109 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 27 Mar 2026 20:40:20 +0100 Subject: [PATCH 25/66] remove getStateLable function from labels.ts --- public/modules/ui/labels-editor.js | 2 +- src/modules/labels.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index 647eb7bff..57054e42a 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -18,7 +18,7 @@ function extractPathPoints(pathElement) { function getLabelData(textElement) { const id = textElement.id || ""; if (id.startsWith("stateLabel")) { - return Labels.getStateLabel(+id.slice(10)); + return Labels.get(+id.slice(10)); } // Custom labels: check for existing data-label-id attribute const dataLabelId = textElement.getAttribute("data-label-id"); diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 8dce3d7f1..20268ea3c 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -66,12 +66,6 @@ class LabelsModule { ); } - getStateLabel(stateId: number): StateLabelData | undefined { - return pack.labels.find( - (l) => l.type === "state" && l.stateId === stateId, - ) as StateLabelData | undefined; - } - getBurgLabel(burgId: number): BurgLabelData | undefined { return pack.labels.find((l) => l.type === "burg" && l.burgId === burgId) as | BurgLabelData From 5d90b663c4f27ee6f10f2dd6716da09e39fe4a29 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 27 Mar 2026 20:59:10 +0100 Subject: [PATCH 26/66] remove getBurgLabel from labels.ts --- public/modules/ui/burg-editor.js | 16 ++++++++-------- src/modules/labels.ts | 6 ------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index 3cdfa3fd1..d75ca9ad3 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -15,7 +15,7 @@ function editBurg(id) { title: "Edit Burg", resizable: false, close: closeBurgEditor, - position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"} + position: { my: "left top", at: "left+10 top+10", of: "svg", collision: "fit" } }); if (modules.editBurg) return; @@ -51,7 +51,7 @@ function editBurg(id) { function updateGroupsList() { byId("burgGroup").options.length = 0; // remove all options - for (const {name} of options.burgs.groups) { + for (const { name } of options.burgs.groups) { byId("burgGroup").options.add(new Option(name, name)); } } @@ -119,8 +119,8 @@ function editBurg(id) { pack.burgs[id].name = burgName.value; elSelected.text(burgName.value); // Sync to Labels data model - const labelData = Labels.getBurgLabel(id); - if (labelData) Labels.updateLabel(labelData.i, {text: burgName.value}); + const labelData = Labels.get(id); + if (labelData) Labels.updateLabel(labelData.i, { text: burgName.value }); } function generateNameRandom() { @@ -202,7 +202,7 @@ function editBurg(id) { } function toggleCapital(burgId) { - const {burgs, states} = pack; + const { burgs, states } = pack; if (burgs[burgId].capital) return tip("To change capital please assign a capital status to another burg of this state", false, "error"); @@ -302,7 +302,7 @@ function editBurg(id) { prompt( "Provide custom URL to the burg map. It can be a link to a generator or just an image. Leave empty to use the default map preview", - {default: Burgs.getPreview(burg).link, required: false}, + { default: Burgs.getPreview(burg).link, required: false }, link => { if (link) burg.link = link; else delete burg.link; @@ -386,8 +386,8 @@ function editBurg(id) { if (burg.capital) pack.states[newState].center = burg.cell; // Sync position to Labels data model - const labelData = Labels.getBurgLabel(id); - if (labelData) Labels.updateLabel(labelData.i, {x, y}); + const labelData = Labels.get(id); + if (labelData) Labels.updateLabel(labelData.i, { x, y }); if (d3.event.shiftKey === false) toggleRelocateBurg(); } diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 20268ea3c..e8abd2625 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -66,12 +66,6 @@ class LabelsModule { ); } - getBurgLabel(burgId: number): BurgLabelData | undefined { - return pack.labels.find((l) => l.type === "burg" && l.burgId === burgId) as - | BurgLabelData - | undefined; - } - addStateLabel(data: Omit): StateLabelData { const label: StateLabelData = { i: this.getNextId(), From 470c42a11f6064717303dec18e7b3f175323072c Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 23 Apr 2026 20:59:43 +0200 Subject: [PATCH 27/66] refactor: make label addition methods private and streamline data assignment --- src/modules/labels.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index e8abd2625..b14b4a8f9 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -66,27 +66,27 @@ class LabelsModule { ); } - addStateLabel(data: Omit): StateLabelData { + private addStateLabel(data: Omit): StateLabelData { const label: StateLabelData = { + ...data, i: this.getNextId(), type: "state", - ...data, }; pack.labels.push(label); return label; } - addBurgLabel(data: Omit): BurgLabelData { - const label: BurgLabelData = { i: this.getNextId(), type: "burg", ...data }; + private addBurgLabel(data: Omit): BurgLabelData { + const label: BurgLabelData = { ...data, i: this.getNextId(), type: "burg", }; pack.labels.push(label); return label; } addCustomLabel(data: Omit): CustomLabelData { const label: CustomLabelData = { + ...data, i: this.getNextId(), type: "custom", - ...data, }; pack.labels.push(label); return label; From 5c5f6c0b2601fb30b877cb40866f4842bd3ead85 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 23 Apr 2026 22:49:37 +0200 Subject: [PATCH 28/66] rename Labels.updateLabel to just Labels.update Co-authored-by: Copilot --- public/modules/ui/burg-editor.js | 4 ++-- public/modules/ui/labels-editor.js | 20 ++++++++++---------- src/modules/labels.ts | 2 +- src/renderers/draw-state-labels.ts | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index d75ca9ad3..14a9ba4b6 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -120,7 +120,7 @@ function editBurg(id) { elSelected.text(burgName.value); // Sync to Labels data model const labelData = Labels.get(id); - if (labelData) Labels.updateLabel(labelData.i, { text: burgName.value }); + if (labelData) Labels.update(labelData.i, { text: burgName.value }); } function generateNameRandom() { @@ -387,7 +387,7 @@ function editBurg(id) { // Sync position to Labels data model const labelData = Labels.get(id); - if (labelData) Labels.updateLabel(labelData.i, { x, y }); + if (labelData) Labels.update(labelData.i, { x, y }); if (d3.event.shiftKey === false) toggleRelocateBurg(); } diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index 57054e42a..6d98342ea 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -195,7 +195,7 @@ function editLabel() { debug.select("#controlPoints > path").attr("d", d); // Sync path control points back to data model const labelData = getLabelData(elSelected.node()); - if (labelData) Labels.updateLabel(labelData.i, { pathPoints: points }); + if (labelData) Labels.update(labelData.i, { pathPoints: points }); } function clickControlPoint() { @@ -251,7 +251,7 @@ function editLabel() { elSelected.attr("transform", transform); debug.select("#controlPoints").attr("transform", transform); const labelData = getLabelData(elSelected.node()); - if (labelData) Labels.updateLabel(labelData.i, { transform }); + if (labelData) Labels.update(labelData.i, { transform }); }); } @@ -272,7 +272,7 @@ function editLabel() { byId(this.value).appendChild(elSelected.node()); const labelData = getLabelData(elSelected.node()); if (labelData && labelData.type === "custom") { - Labels.updateLabel(labelData.i, { group: this.value }); + Labels.update(labelData.i, { group: this.value }); } } @@ -314,7 +314,7 @@ function editLabel() { byId("labelGroupSelect").options.add(new Option(group, group, false, true)); // Update data model for labels in the old group const oldGroupName = oldGroup.id; - Labels.getByGroup(oldGroupName).forEach(l => Labels.updateLabel(l.i, { group })); + Labels.getByGroup(oldGroupName).forEach(l => Labels.update(l.i, { group })); oldGroup.id = group; toggleNewGroupInput(); byId("labelGroupInput").value = ""; @@ -329,7 +329,7 @@ function editLabel() { // Update data model group for the moved label const labelData = getLabelData(elSelected.node()); if (labelData && labelData.type === "custom") { - Labels.updateLabel(labelData.i, { group }); + Labels.update(labelData.i, { group }); } toggleNewGroupInput(); @@ -395,7 +395,7 @@ function editLabel() { // Update data model const labelData = getLabelData(elSelected.node()); - if (labelData) Labels.updateLabel(labelData.i, { text: input }); + if (labelData) Labels.update(labelData.i, { text: input }); if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning"); @@ -445,14 +445,14 @@ function editLabel() { function changeStartOffset() { elSelected.select("textPath").attr("startOffset", this.value + "%"); const labelData = getLabelData(elSelected.node()); - if (labelData) Labels.updateLabel(labelData.i, { startOffset: +this.value }); + if (labelData) Labels.update(labelData.i, { startOffset: +this.value }); tip("Label offset: " + this.value + "%"); } function changeRelativeSize() { elSelected.select("textPath").attr("font-size", this.value + "%"); const labelData = getLabelData(elSelected.node()); - if (labelData) Labels.updateLabel(labelData.i, { fontSize: +this.value }); + if (labelData) Labels.update(labelData.i, { fontSize: +this.value }); tip("Label relative size: " + this.value + "%"); changeText(); } @@ -460,7 +460,7 @@ function editLabel() { function changeLetterSpacingSize() { elSelected.select("textPath").attr("letter-spacing", this.value + "px"); const labelData = getLabelData(elSelected.node()); - if (labelData) Labels.updateLabel(labelData.i, { letterSpacing: +this.value }); + if (labelData) Labels.update(labelData.i, { letterSpacing: +this.value }); tip("Label letter-spacing size: " + this.value + "px"); changeText(); } @@ -475,7 +475,7 @@ function editLabel() { const labelData = getLabelData(elSelected.node()); if (labelData) { const pathEl = byId("textPath_" + elSelected.attr("id")); - Labels.updateLabel(labelData.i, { pathPoints: extractPathPoints(pathEl) }); + Labels.update(labelData.i, { pathPoints: extractPathPoints(pathEl) }); } } diff --git a/src/modules/labels.ts b/src/modules/labels.ts index b14b4a8f9..ca5212464 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -92,7 +92,7 @@ class LabelsModule { return label; } - updateLabel(id: number, updates: Partial): void { + update(id: number, updates: Partial): void { const label = pack.labels.find((l) => l.i === id); if (!label) return; Object.assign(label, updates, { i: label.i, type: label.type }); diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index c0f15bcea..77f46c485 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -48,8 +48,8 @@ const stateLabelsRenderer = (list?: number[]): void => { const labelsToRender: StateLabelData[] = list && list.length > 0 ? list - .map((idx) => Labels.get(idx)) - .filter((label) => label?.type === "state") + .map((idx) => Labels.get(idx)) + .filter((label) => label?.type === "state") : Labels.getAll().filter((label) => label.type === "state"); const letterLength = checkExampleLetterLength(); @@ -118,7 +118,7 @@ const stateLabelsRenderer = (list?: number[]): void => { ); // Update label data with font size - Labels.updateLabel(labelData.i, { fontSize: ratio }); + Labels.update(labelData.i, { fontSize: ratio }); // prolongate path if it's too short const longestLineLength = max(lines.map((line) => line.length)) || 0; @@ -182,7 +182,7 @@ const stateLabelsRenderer = (list?: number[]): void => { ? state.fullName! : state.name!; textElement.innerHTML = `${text}`; - Labels.updateLabel(labelData.i, { text }); + Labels.update(labelData.i, { text }); const correctedRatio = minmax( rn((pathLength / text.length) * 50), @@ -190,7 +190,7 @@ const stateLabelsRenderer = (list?: number[]): void => { 130, ); textElement.setAttribute("font-size", `${correctedRatio}%`); - Labels.updateLabel(labelData.i, { fontSize: correctedRatio }); + Labels.update(labelData.i, { fontSize: correctedRatio }); } } From 924b537d2f8502eca91869fd1f43bd68cc002e4c Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 23 Apr 2026 22:52:39 +0200 Subject: [PATCH 29/66] rename Labels.removeLabel to Labels.remove --- public/modules/ui/labels-editor.js | 2 +- src/modules/labels.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index 6d98342ea..cbbe1d7e2 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -494,7 +494,7 @@ function editLabel() { Remove: function () { $(this).dialog("close"); const labelData = getLabelData(elSelected.node()); - if (labelData) Labels.removeLabel(labelData.i); + if (labelData) Labels.remove(labelData.i); defs.select("#textPath_" + elSelected.attr("id")).remove(); elSelected.remove(); $("#labelEditor").dialog("close"); diff --git a/src/modules/labels.ts b/src/modules/labels.ts index ca5212464..bab198648 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -98,7 +98,7 @@ class LabelsModule { Object.assign(label, updates, { i: label.i, type: label.type }); } - removeLabel(id: number): void { + remove(id: number): void { const index = pack.labels.findIndex((l) => l.i === id); if (index !== -1) pack.labels.splice(index, 1); } From 563e1c9572241589f52b85d54eeb3c8e29f69477 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 23 Apr 2026 23:20:51 +0200 Subject: [PATCH 30/66] remove burg specific removal method --- src/modules/burgs-generator.ts | 3 ++- src/modules/labels.ts | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/modules/burgs-generator.ts b/src/modules/burgs-generator.ts index 29794222b..5633be91d 100644 --- a/src/modules/burgs-generator.ts +++ b/src/modules/burgs-generator.ts @@ -729,7 +729,8 @@ class BurgModule { } removeBurgIcon(burg.i!); - Labels.removeBurgLabel(burg.i!); + const labelId = pack.labels.findIndex((l) => l.type === "burg" && l.burgId === burgId); + Labels.remove(labelId); } } window.Burgs = new BurgModule(); diff --git a/src/modules/labels.ts b/src/modules/labels.ts index bab198648..28bc1cc34 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -113,13 +113,6 @@ class LabelsModule { ); } - removeBurgLabel(burgId: number): void { - const index = pack.labels.findIndex( - (l) => l.type === "burg" && l.burgId === burgId, - ); - if (index !== -1) pack.labels.splice(index, 1); - } - clear(): void { pack.labels = []; } From aa2d4d9921ed4080b0b7b91a5287b322f3d7ea48 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 00:18:46 +0200 Subject: [PATCH 31/66] refactor: enhance label data structure and extraction methods for SVG paths --- public/modules/dynamic/auto-update.js | 97 +++++++++++++-------------- public/modules/ui/labels-editor.js | 18 +---- src/modules/labels.ts | 4 ++ src/utils/index.ts | 2 + src/utils/pathUtils.ts | 15 +++++ 5 files changed, 69 insertions(+), 67 deletions(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index be2e535ac..c543ff3e0 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1120,27 +1120,45 @@ export function resolveVersionConflicts(mapVersion) { stateLabelsGroup.querySelectorAll("text").forEach(textElement => { const id = textElement.getAttribute("id"); if (!id || !id.startsWith("stateLabel")) return; - + const stateIdMatch = id.match(/stateLabel(\d+)/); if (!stateIdMatch) return; - + const stateId = +stateIdMatch[1]; const state = pack.states[stateId]; if (!state || state.removed) return; - + const textPath = textElement.querySelector("textPath"); if (!textPath) return; - + const text = textPath.textContent.trim(); const fontSizeAttr = textPath.getAttribute("font-size"); const fontSize = fontSizeAttr ? parseFloat(fontSizeAttr) : 100; - + const letterSpacingAttr = textPath.getAttribute("letter-spacing"); + const letterSpacing = letterSpacingAttr ? parseFloat(letterSpacingAttr) : 0; + const startOffsetAttr = textPath.getAttribute("startOffset"); + const startOffset = startOffsetAttr ? parseFloat(startOffsetAttr) : 50; + const transform = textPath.getAttribute("transform"); + + // Get path points from the referenced path + const href = textPath.getAttribute("xlink:href") || textPath.getAttribute("href"); + if (!href) return; + + const pathId = href.replace("#", ""); + const pathElement = document.getElementById(pathId); + + pack.labels.push({ i: pack.labels.length, type: "state", stateId: stateId, text: text, - fontSize: fontSize + pathPoints: extractPathPoints(pathElement), + startOffset: startOffset, + fontSize: fontSize, + letterSpacing: letterSpacing, + transform: transform || undefined + }); }); } @@ -1151,25 +1169,25 @@ export function resolveVersionConflicts(mapVersion) { burgLabelsGroup.querySelectorAll("g").forEach(groupElement => { const group = groupElement.getAttribute("id"); if (!group) return; - + const dxAttr = groupElement.getAttribute("data-dx"); const dyAttr = groupElement.getAttribute("data-dy"); const dx = dxAttr ? parseFloat(dxAttr) : 0; const dy = dyAttr ? parseFloat(dyAttr) : 0; - + groupElement.querySelectorAll("text").forEach(textElement => { const burgId = +textElement.getAttribute("data-id"); if (!burgId) return; - + const burg = pack.burgs[burgId]; if (!burg || burg.removed) return; - + const text = textElement.textContent.trim(); // Use burg coordinates, not SVG text coordinates // SVG coordinates may be affected by viewbox transforms const x = burg.x; const y = burg.y; - + pack.labels.push({ i: labelId++, type: "burg", @@ -1191,11 +1209,11 @@ export function resolveVersionConflicts(mapVersion) { customLabelsGroup.querySelectorAll("text").forEach(textElement => { const id = textElement.getAttribute("id"); if (!id) return; - + const group = "custom"; const textPath = textElement.querySelector("textPath"); if (!textPath) return; - + const text = textPath.textContent.trim(); const fontSizeAttr = textPath.getAttribute("font-size"); const fontSize = fontSizeAttr ? parseFloat(fontSizeAttr) : 100; @@ -1204,51 +1222,26 @@ export function resolveVersionConflicts(mapVersion) { const startOffsetAttr = textPath.getAttribute("startOffset"); const startOffset = startOffsetAttr ? parseFloat(startOffsetAttr) : 50; const transform = textPath.getAttribute("transform"); - + // Get path points from the referenced path const href = textPath.getAttribute("xlink:href") || textPath.getAttribute("href"); if (!href) return; - + const pathId = href.replace("#", ""); const pathElement = document.getElementById(pathId); if (!pathElement) return; - - const d = pathElement.getAttribute("d"); - if (!d) return; - - // Parse path data to extract points(M, L and C commands) - const pathPoints = []; - const commands = d.match(/[MLC][^MLC]*/g); - if (commands) { - commands.forEach(cmd => { - const type = cmd[0]; - if (type === "M" || type === "L") { - const coords = cmd.slice(1).trim().split(/[\s,]+/).map(Number); - if (coords.length >= 2) { - pathPoints.push([coords[0], coords[1]]); - } - } else if (type === "C") { - const coords = cmd.slice(1).trim().split(/[\s,]+/).map(Number); - if (coords.length >= 6) { - pathPoints.push([coords[4], coords[5]]); - } - } - }); - } - - if (pathPoints.length > 0) { - pack.labels.push({ - i: pack.labels.length, - type: "custom", - group: group, - text: text, - pathPoints: pathPoints, - startOffset: startOffset, - fontSize: fontSize, - letterSpacing: letterSpacing, - transform: transform || undefined - }); - } + + pack.labels.push({ + i: pack.labels.length, + type: "custom", + group: group, + text: text, + pathPoints: extractPathPoints(pathElement), + startOffset: startOffset, + fontSize: fontSize, + letterSpacing: letterSpacing, + transform: transform || undefined + }); }); } diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index cbbe1d7e2..59787864b 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -1,18 +1,6 @@ "use strict"; -// Helper: extract control points from an SVG path element -function extractPathPoints(pathElement) { - if (!pathElement) return []; - const l = pathElement.getTotalLength(); - if (!l) return []; - const points = []; - const increment = l / Math.max(Math.ceil(l / 200), 2); - for (let i = 0; i <= l; i += increment) { - const point = pathElement.getPointAtLength(i); - points.push([point.x, point.y]); - } - return points; -} + // Helper: find label data from the Labels data model for an SVG text element function getLabelData(textElement) { @@ -26,10 +14,10 @@ function getLabelData(textElement) { const existing = Labels.get(+dataLabelId); if (existing) return existing; // Data was cleared (e.g., map regenerated) — recreate - textElement.removeAttribute("data-label-id"); + // textElement.removeAttribute("data-label-id"); } // No data entry found — create one from SVG state (migration path) - return createCustomLabelDataFromSvg(textElement); + return undefined; } // Helper: create a CustomLabelData entry from existing SVG elements diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 28bc1cc34..2cac98620 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -7,7 +7,11 @@ export interface StateLabelData { type: "state"; stateId: number; text: string; + pathPoints?: [number, number][]; + startOffset?: number; fontSize?: number; + letterSpacing?: number; + transform?: string; } export interface BurgLabelData { diff --git a/src/utils/index.ts b/src/utils/index.ts index 59b4b5286..bbc770969 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -106,6 +106,7 @@ import { getIsolines, getPolesOfInaccessibility, getVertexPath, + extractPathPoints, } from "./pathUtils"; window.getIsolines = getIsolines; @@ -115,6 +116,7 @@ window.findPath = (start, end, getCost) => findPath(start, end, getCost, (window as any).pack); window.getVertexPath = (cellsArray) => getVertexPath(cellsArray, (window as any).pack); +window.extractPathPoints = extractPathPoints; import { capitalize, diff --git a/src/utils/pathUtils.ts b/src/utils/pathUtils.ts index 36baec86e..4a00124fd 100644 --- a/src/utils/pathUtils.ts +++ b/src/utils/pathUtils.ts @@ -370,6 +370,20 @@ export const findPath = ( return null; }; +// Helper: extract control points from an SVG path element +export const extractPathPoints = (pathElement: SVGPathElement) => { + if (!pathElement) return []; + const l = pathElement.getTotalLength(); + if (!l) return []; + const points = []; + const increment = l / Math.max(Math.ceil(l / 200), 2); + for (let i = 0; i <= l; i += increment) { + const point = pathElement.getPointAtLength(i); + points.push([point.x, point.y]); + } + return points; +} + declare global { interface Window { ERROR: boolean; @@ -380,5 +394,6 @@ declare global { connectVertices: typeof connectVertices; findPath: typeof findPath; getVertexPath: typeof getVertexPath; + extractPathPoints: typeof extractPathPoints; } } From e4a4c922f7986b67bfad3d0b61a9be5c8155a23a Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 00:26:40 +0200 Subject: [PATCH 32/66] refactor: rename label interfaces for consistency and clarity --- src/modules/labels.ts | 20 ++++++++++---------- src/renderers/draw-burg-labels.ts | 4 ++-- src/renderers/draw-state-labels.ts | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 2cac98620..c1c352991 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -2,7 +2,7 @@ declare global { var Labels: LabelsModule; } -export interface StateLabelData { +export interface StateLabel { i: number; type: "state"; stateId: number; @@ -14,7 +14,7 @@ export interface StateLabelData { transform?: string; } -export interface BurgLabelData { +export interface BurgLabel { i: number; type: "burg"; burgId: number; @@ -24,7 +24,7 @@ export interface BurgLabelData { y: number; } -export interface CustomLabelData { +export interface CustomLabel { i: number; type: "custom"; group: string; @@ -36,7 +36,7 @@ export interface CustomLabelData { transform?: string; } -export type LabelData = StateLabelData | BurgLabelData | CustomLabelData; +export type LabelData = StateLabel | BurgLabel | CustomLabel; class LabelsModule { private getNextId(): number { @@ -70,8 +70,8 @@ class LabelsModule { ); } - private addStateLabel(data: Omit): StateLabelData { - const label: StateLabelData = { + private addStateLabel(data: Omit): StateLabel { + const label: StateLabel = { ...data, i: this.getNextId(), type: "state", @@ -80,14 +80,14 @@ class LabelsModule { return label; } - private addBurgLabel(data: Omit): BurgLabelData { - const label: BurgLabelData = { ...data, i: this.getNextId(), type: "burg", }; + private addBurgLabel(data: Omit): BurgLabel { + const label: BurgLabel = { ...data, i: this.getNextId(), type: "burg", }; pack.labels.push(label); return label; } - addCustomLabel(data: Omit): CustomLabelData { - const label: CustomLabelData = { + addCustomLabel(data: Omit): CustomLabel { + const label: CustomLabel = { ...data, i: this.getNextId(), type: "custom", diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index b13261298..511ca89fc 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -1,5 +1,5 @@ import type { Burg } from "../modules/burgs-generator"; -import type { BurgLabelData } from "../modules/labels"; +import type { BurgLabel } from "../modules/labels"; interface BurgGroup { name: string; @@ -19,7 +19,7 @@ export function drawBurgLabels(): void { createLabelGroups(); // Get all burg labels grouped by group name - const burgLabelsByGroup = new Map(); + const burgLabelsByGroup = new Map(); for (const label of Labels.getAll()) { if (label.type !== "burg") continue; if (!burgLabelsByGroup.has(label.group)) { diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index 77f46c485..c778f8871 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -1,5 +1,5 @@ import { curveNatural, line, max, select } from "d3"; -import type { StateLabelData } from "../modules/labels"; +import type { StateLabel } from "../modules/labels"; import { findClosestCell, minmax, rn, round, splitInTwo } from "../utils"; import { ANGLES, findBestRayPair, raycast } from "./label-raycast"; @@ -45,7 +45,7 @@ const stateLabelsRenderer = (list?: number[]): void => { const { states } = pack; // Get labels to render - const labelsToRender: StateLabelData[] = + const labelsToRender: StateLabel[] = list && list.length > 0 ? list .map((idx) => Labels.get(idx)) @@ -60,7 +60,7 @@ const stateLabelsRenderer = (list?: number[]): void => { function drawLabelPath( letterLength: number, - labelDataList: StateLabelData[], + labelDataList: StateLabel[], ): void { const mode = options.stateLabelsMode || "auto"; const lineGen = line<[number, number]>().curve(curveNatural); From 2bed4d5e02c3f9a7244a9378983f4ff95337f0db Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 00:29:49 +0200 Subject: [PATCH 33/66] remove redundant check in state label migration --- public/modules/dynamic/auto-update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index c543ff3e0..c521624bc 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1119,7 +1119,7 @@ export function resolveVersionConflicts(mapVersion) { if (stateLabelsGroup) { stateLabelsGroup.querySelectorAll("text").forEach(textElement => { const id = textElement.getAttribute("id"); - if (!id || !id.startsWith("stateLabel")) return; + if (!id) return; const stateIdMatch = id.match(/stateLabel(\d+)/); if (!stateIdMatch) return; From c0dcad1da31dc94ff869eead73dffd5a1576227e Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 00:41:07 +0200 Subject: [PATCH 34/66] removed svg fallback from label editor --- public/modules/ui/labels-editor.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index 59787864b..9f02324e8 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -121,19 +121,12 @@ function editLabel() { function updateValues(textPath) { const labelData = getLabelData(elSelected.node()); - if (labelData && labelData.type === "custom") { + if (labelData) { // Custom labels: read all values from data model byId("labelText").value = labelData.text || ""; byId("labelStartOffset").value = labelData.startOffset || 50; byId("labelRelativeSize").value = labelData.fontSize || 100; byId("labelLetterSpacingSize").value = labelData.letterSpacing || 0; - } else { - // State labels and fallback: read from SVG, use data model fontSize if available - byId("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|"); - byId("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset")) || 50; - byId("labelRelativeSize").value = (labelData && labelData.fontSize) || parseFloat(textPath.getAttribute("font-size")) || 100; - let letterSpacingSize = textPath.getAttribute("letter-spacing") ? textPath.getAttribute("letter-spacing") : 0; - byId("labelLetterSpacingSize").value = parseFloat(letterSpacingSize); } } From 854cefec21e6fb8e00a87c7b51fa5eda1569975e Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 00:47:48 +0200 Subject: [PATCH 35/66] remove unhelpful comment --- public/modules/ui/labels-editor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index 9f02324e8..b6955835c 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -122,7 +122,6 @@ function editLabel() { function updateValues(textPath) { const labelData = getLabelData(elSelected.node()); if (labelData) { - // Custom labels: read all values from data model byId("labelText").value = labelData.text || ""; byId("labelStartOffset").value = labelData.startOffset || 50; byId("labelRelativeSize").value = labelData.fontSize || 100; From 6324eb60c46437407814eb7bbdb732b0d9cffd67 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 00:57:53 +0200 Subject: [PATCH 36/66] quick win simplification --- public/modules/ui/burg-editor.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index 14a9ba4b6..635328956 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -119,8 +119,7 @@ function editBurg(id) { pack.burgs[id].name = burgName.value; elSelected.text(burgName.value); // Sync to Labels data model - const labelData = Labels.get(id); - if (labelData) Labels.update(labelData.i, { text: burgName.value }); + Labels.update(id, { text: burgName.value }); } function generateNameRandom() { @@ -386,8 +385,7 @@ function editBurg(id) { if (burg.capital) pack.states[newState].center = burg.cell; // Sync position to Labels data model - const labelData = Labels.get(id); - if (labelData) Labels.update(labelData.i, { x, y }); + Labels.update(id, { x, y }); if (d3.event.shiftKey === false) toggleRelocateBurg(); } From 3e29f3e3a992181e74426a674fc78c19ec3b5ad2 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 01:06:04 +0200 Subject: [PATCH 37/66] cleanup comments --- public/modules/ui/burg-editor.js | 1 - src/modules/labels.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index 635328956..f932a0774 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -384,7 +384,6 @@ function editBurg(id) { burg.y = y; if (burg.capital) pack.states[newState].center = burg.cell; - // Sync position to Labels data model Labels.update(id, { x, y }); if (d3.event.shiftKey === false) toggleRelocateBurg(); diff --git a/src/modules/labels.ts b/src/modules/labels.ts index c1c352991..151dacf81 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -134,7 +134,6 @@ class LabelsModule { // Remove existing state labels that need regeneration this.removeByType("state"); - // Generate new label entries for (const state of states) { if (!state.i || state.removed || state.lock) continue; @@ -155,10 +154,8 @@ class LabelsModule { generateBurgLabels(): void { if (TIME) console.time("generateBurgLabels"); - // Remove existing burg labels this.removeByType("burg"); - // Generate new labels for all active burgs for (const burg of pack.burgs) { if (!burg.i || burg.removed) continue; From d84292e8be0ea414a08a68f52408ba4f7786f689 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 01:06:35 +0200 Subject: [PATCH 38/66] add helpful description to getNextId --- src/modules/labels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 151dacf81..8b5ea1af6 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -39,6 +39,7 @@ export interface CustomLabel { export type LabelData = StateLabel | BurgLabel | CustomLabel; class LabelsModule { + // Gets the next possible Label ID in O(n log n) time. Allows for non-sequential IDs private getNextId(): number { const labels = pack.labels; if (labels.length === 0) return 0; @@ -131,7 +132,6 @@ class LabelsModule { const { states } = pack; - // Remove existing state labels that need regeneration this.removeByType("state"); for (const state of states) { From 701448b2a55b07e63e725e56afc5a4af4a0eeed5 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 01:27:45 +0200 Subject: [PATCH 39/66] regenerate Labels on Erase Mode Exit --- public/modules/ui/heightmap-editor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/modules/ui/heightmap-editor.js b/public/modules/ui/heightmap-editor.js index e204055a3..cc0fd743d 100644 --- a/public/modules/ui/heightmap-editor.js +++ b/public/modules/ui/heightmap-editor.js @@ -256,6 +256,7 @@ function editHeightmap(options) { Provinces.generate(); Provinces.getPoles(); + Labels.generate(); Rivers.specify(); Lakes.defineNames(); From 0b772be548f55de6b01451b2c2a1e051b1e73452 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 01:29:28 +0200 Subject: [PATCH 40/66] remove useless comment --- public/modules/dynamic/auto-update.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index c521624bc..45a3f878b 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1223,7 +1223,6 @@ export function resolveVersionConflicts(mapVersion) { const startOffset = startOffsetAttr ? parseFloat(startOffsetAttr) : 50; const transform = textPath.getAttribute("transform"); - // Get path points from the referenced path const href = textPath.getAttribute("xlink:href") || textPath.getAttribute("href"); if (!href) return; From 368eec0cc2f5022aa96adfd4b626da601615b999 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 02:08:15 +0200 Subject: [PATCH 41/66] add error logging for label update when label not found --- src/modules/labels.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 8b5ea1af6..7e0052b57 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -99,7 +99,10 @@ class LabelsModule { update(id: number, updates: Partial): void { const label = pack.labels.find((l) => l.i === id); - if (!label) return; + if (!label) { + ERROR && console.error(`Label with id ${id} was not found for update.`); + return; + } Object.assign(label, updates, { i: label.i, type: label.type }); } From 0433669ee3955fe68b47e5c0efe658c26104a367 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 20:01:03 +0200 Subject: [PATCH 42/66] implement custom label rendering and update label attributes for improved positioning --- public/modules/dynamic/auto-update.js | 47 ++++++++++--------- public/modules/ui/labels-editor.js | 26 +++-------- public/modules/ui/layers.js | 1 + src/modules/labels.ts | 6 ++- src/renderers/draw-custom-labels.ts | 65 +++++++++++++++++++++++++++ src/renderers/draw-state-labels.ts | 5 ++- src/renderers/index.ts | 1 + 7 files changed, 107 insertions(+), 44 deletions(-) create mode 100644 src/renderers/draw-custom-labels.ts diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index 45a3f878b..81b92a697 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1139,6 +1139,7 @@ export function resolveVersionConflicts(mapVersion) { const startOffsetAttr = textPath.getAttribute("startOffset"); const startOffset = startOffsetAttr ? parseFloat(startOffsetAttr) : 50; const transform = textPath.getAttribute("transform"); + const [dx, dy] = transform ? parseTransform(transform) : [0, 0]; // Get path points from the referenced path const href = textPath.getAttribute("xlink:href") || textPath.getAttribute("href"); @@ -1151,14 +1152,14 @@ export function resolveVersionConflicts(mapVersion) { pack.labels.push({ i: pack.labels.length, type: "state", - stateId: stateId, - text: text, + stateId, + text, pathPoints: extractPathPoints(pathElement), - startOffset: startOffset, - fontSize: fontSize, - letterSpacing: letterSpacing, - transform: transform || undefined - + startOffset, + fontSize, + letterSpacing, + dx, + dy }); }); } @@ -1191,13 +1192,13 @@ export function resolveVersionConflicts(mapVersion) { pack.labels.push({ i: labelId++, type: "burg", - burgId: burgId, - group: group, - text: text, - x: x, - y: y, - dx: dx, - dy: dy + burgId, + group, + text, + x, + y, + dx, + dy }); }); }); @@ -1222,6 +1223,7 @@ export function resolveVersionConflicts(mapVersion) { const startOffsetAttr = textPath.getAttribute("startOffset"); const startOffset = startOffsetAttr ? parseFloat(startOffsetAttr) : 50; const transform = textPath.getAttribute("transform"); + const [dx, dy] = transform ? parseTransform(transform) : [0, 0]; const href = textPath.getAttribute("xlink:href") || textPath.getAttribute("href"); if (!href) return; @@ -1233,13 +1235,14 @@ export function resolveVersionConflicts(mapVersion) { pack.labels.push({ i: pack.labels.length, type: "custom", - group: group, - text: text, + group, + text, pathPoints: extractPathPoints(pathElement), - startOffset: startOffset, - fontSize: fontSize, - letterSpacing: letterSpacing, - transform: transform || undefined + startOffset, + fontSize, + letterSpacing, + dx, + dy }); }); } @@ -1247,11 +1250,13 @@ export function resolveVersionConflicts(mapVersion) { // Clear old SVG labels and redraw from data if (stateLabelsGroup) stateLabelsGroup.querySelectorAll("*").forEach(el => el.remove()); if (burgLabelsGroup) burgLabelsGroup.querySelectorAll("text").forEach(el => el.remove()); - + if (customLabelsGroup) customLabelsGroup.querySelectorAll("text").forEach(el => el.remove()); + // Regenerate labels from data if (layerIsOn("toggleLabels")) { drawStateLabels(); drawBurgLabels(); + drawCustomLabels(); } } } diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index b6955835c..5e86e8220 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -8,6 +8,9 @@ function getLabelData(textElement) { if (id.startsWith("stateLabel")) { return Labels.get(+id.slice(10)); } + if (id.startsWith("customLabel")) { + return Labels.get(+id.slice(11)); + } // Custom labels: check for existing data-label-id attribute const dataLabelId = textElement.getAttribute("data-label-id"); if (dataLabelId != null) { @@ -20,23 +23,6 @@ function getLabelData(textElement) { return undefined; } -// Helper: create a CustomLabelData entry from existing SVG elements -function createCustomLabelDataFromSvg(textElement) { - const textPathEl = textElement.querySelector("textPath"); - if (!textPathEl) return null; - const group = textElement.parentNode.id; - const text = [...textPathEl.querySelectorAll("tspan")].map(t => t.textContent).join("|"); - const pathEl = byId("textPath_" + textElement.id); - const pathPoints = extractPathPoints(pathEl); - const startOffset = parseFloat(textPathEl.getAttribute("startOffset")) || 50; - const fontSize = parseFloat(textPathEl.getAttribute("font-size")) || 100; - const letterSpacing = parseFloat(textPathEl.getAttribute("letter-spacing") || "0"); - const transform = textElement.getAttribute("transform") || undefined; - const label = Labels.addCustomLabel({ group, text, pathPoints, startOffset, fontSize, letterSpacing, transform }); - textElement.setAttribute("data-label-id", String(label.i)); - return label; -} - function editLabel() { if (customization) return; closeDialogs(); @@ -227,11 +213,13 @@ function editLabel() { d3.event.on("drag", function () { const x = d3.event.x, y = d3.event.y; - const transform = `translate(${dx + x},${dy + y})`; + const effectiveDx = dx + x; + const effectiveDy = dy + y; + const transform = `translate(${effectiveDx},${effectiveDy})`; elSelected.attr("transform", transform); debug.select("#controlPoints").attr("transform", transform); const labelData = getLabelData(elSelected.node()); - if (labelData) Labels.update(labelData.i, { transform }); + if (labelData) Labels.update(labelData.i, { dx: effectiveDx, dy: effectiveDy }); }); } diff --git a/public/modules/ui/layers.js b/public/modules/ui/layers.js index f2f04a4be..fd523b320 100644 --- a/public/modules/ui/layers.js +++ b/public/modules/ui/layers.js @@ -851,6 +851,7 @@ function toggleLabels(event) { function drawLabels() { drawStateLabels(); drawBurgLabels(); + drawCustomLabels(); invokeActiveZooming(); } diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 7e0052b57..aa8f45080 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -11,7 +11,8 @@ export interface StateLabel { startOffset?: number; fontSize?: number; letterSpacing?: number; - transform?: string; + dx?: number; + dy?: number; } export interface BurgLabel { @@ -33,7 +34,8 @@ export interface CustomLabel { startOffset?: number; fontSize?: number; letterSpacing?: number; - transform?: string; + dx?: number; + dy?: number; } export type LabelData = StateLabel | BurgLabel | CustomLabel; diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts new file mode 100644 index 000000000..0923a64d3 --- /dev/null +++ b/src/renderers/draw-custom-labels.ts @@ -0,0 +1,65 @@ +import { curveNatural, line } from "d3"; +import { CustomLabel } from "../modules/labels"; + +// remove this section once layer.js is refactored-------------------------------- +declare global { + var drawCustomLabels: () => void; +} + +window.drawCustomLabels = customLabelRenderer; +// ------------------------------------------------------------------------------- + +export function customLabelRenderer() { + const customLabels = Labels.getAll().filter(labels => labels.type === "custom") + const customLabelsHTML: string[] = []; + const pathGroup = defs.select( + "g#deftemp > g#textPaths", + ); + for (const labelData of customLabels) { + const pathId = addPathForLabel(labelData, pathGroup.node()!); + console.log("Constructing label HTML for label", labelData, "with pathId", pathId); + customLabelsHTML.push(constructLabelHTML(labelData, pathId).outerHTML); + } + + const customLabelsGroup = labels.select(`#addedLabels`); + const groupNode = customLabelsGroup.node(); + if (groupNode) { + groupNode.innerHTML = customLabelsHTML.join(""); + } +} + +function constructLabelHTML(label: CustomLabel, pathId: string): SVGTextElement { + const textParts = label.text.split("|"); + const tspans = textParts.map((part, index) => { + const tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + tspan.setAttribute("x", "0"); + tspan.setAttribute("dy", index ? "1em" : `${((textParts.length - 1) / -2)}em`); + tspan.textContent = part; + return tspan; + }); + + const textPath = document.createElementNS("http://www.w3.org/2000/svg", "textPath"); + textPath.setAttribute("href", `#${pathId}`); + textPath.setAttribute("startOffset", label.startOffset ? `${label.startOffset}%` : "50%"); + textPath.setAttribute("font-size", label.fontSize ? `${label.fontSize}%` : "100%"); + textPath.append(...tspans); + + const textDom = document.createElementNS("http://www.w3.org/2000/svg", "text"); + textDom.setAttribute("text-rendering", "optimizeSpeed"); + textDom.setAttribute("id", `customLabel${label.i}`); + textDom.setAttribute("transform", `translate(${label.dx || 0}, ${label.dy || 0})`); + textDom.appendChild(textPath); + + return textDom; +} + +function addPathForLabel(label: CustomLabel, pathGroup: SVGGElement): string { + const pathId = `textPath_customLabel${label.i}`; + const pathData = line<[number, number]>().curve(curveNatural)(label.pathPoints || []); + const domPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); + domPath.setAttribute("id", pathId); + domPath.setAttribute("d", pathData || ""); + pathGroup.appendChild(domPath); + + return pathId; +} \ No newline at end of file diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index c778f8871..46a3c8483 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -140,7 +140,8 @@ const stateLabelsRenderer = (list?: number[]): void => { const textElement = textGroup .append("text") .attr("text-rendering", "optimizeSpeed") - .attr("id", `stateLabel${labelData.stateId}`) + .attr("id", `stateLabel${labelData.i}`) + .attr("transform", `translate(${labelData.dx || 0}, ${labelData.dy || 0})`) .append("textPath") .attr("startOffset", "50%") .attr("font-size", `${ratio}%`) @@ -156,7 +157,7 @@ const stateLabelsRenderer = (list?: number[]): void => { const { width, height } = textElement.getBBox(); textElement.setAttribute( "href", - `#textPath_stateLabel${labelData.stateId}`, + `#textPath_stateLabel${labelData.i}`, ); const stateIds = pack.cells.state; diff --git a/src/renderers/index.ts b/src/renderers/index.ts index 5ea6e5028..3febf4ed3 100644 --- a/src/renderers/index.ts +++ b/src/renderers/index.ts @@ -1,6 +1,7 @@ import "./draw-borders"; import "./draw-burg-icons"; import "./draw-burg-labels"; +import "./draw-custom-labels"; import "./draw-emblems"; import "./draw-features"; import "./draw-heightmap"; From d4c7fb118e6259128bc863e52b2a9e7fd59782cb Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 20:58:11 +0200 Subject: [PATCH 43/66] bump version --- public/modules/dynamic/auto-update.js | 4 ++-- public/modules/io/load.js | 2 +- public/versioning.js | 2 +- src/index.html | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index 674689f37..f32217f6f 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1116,8 +1116,8 @@ export function resolveVersionConflicts(mapVersion) { }); } - if (isOlderThan("1.114.0")) { - // v1.114.0 moved labels data from SVG to data model + if (isOlderThan("1.122.0")) { + // v1.122.0 moved labels data from SVG to data model // Migrate old SVG labels to pack.labels structure if (!pack.labels || !pack.labels.length) { pack.labels = []; diff --git a/public/modules/io/load.js b/public/modules/io/load.js index cef4871e7..e30e0404c 100644 --- a/public/modules/io/load.js +++ b/public/modules/io/load.js @@ -481,7 +481,7 @@ async function parseLoadedData(data, mapVersion) { { // dynamically import and run auto-update script - const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.120.5"); + const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.122.0"); resolveVersionConflicts(mapVersion); } diff --git a/public/versioning.js b/public/versioning.js index 21bec91ac..ecfdb6764 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -16,7 +16,7 @@ * For the changes that may be interesting to end users, update the `latestPublicChanges` array below (new changes on top). */ -const VERSION = "1.121.0"; +const VERSION = "1.122.0"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { diff --git a/src/index.html b/src/index.html index 01d310e40..e679fdc9e 100644 --- a/src/index.html +++ b/src/index.html @@ -8569,18 +8569,18 @@ - + - + - + - + @@ -8592,12 +8592,12 @@ - + - + @@ -8619,8 +8619,8 @@ - - + + From 8b985787b4578e7d50e151f5a567799ce5c6d9c9 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 20:58:36 +0200 Subject: [PATCH 44/66] format files --- src/modules/burgs-generator.ts | 4 +- src/modules/labels.ts | 2 +- src/renderers/draw-custom-labels.ts | 66 ++++++++++++++++++++++------- src/renderers/draw-state-labels.ts | 22 ++++------ src/utils/pathUtils.ts | 2 +- 5 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/modules/burgs-generator.ts b/src/modules/burgs-generator.ts index 113e480eb..5084bed33 100644 --- a/src/modules/burgs-generator.ts +++ b/src/modules/burgs-generator.ts @@ -730,7 +730,9 @@ class BurgModule { } removeBurgIcon(burg.i!); - const labelId = pack.labels.findIndex((l) => l.type === "burg" && l.burgId === burgId); + const labelId = pack.labels.findIndex( + (l) => l.type === "burg" && l.burgId === burgId, + ); Labels.remove(labelId); } } diff --git a/src/modules/labels.ts b/src/modules/labels.ts index aa8f45080..adfe4b1a4 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -84,7 +84,7 @@ class LabelsModule { } private addBurgLabel(data: Omit): BurgLabel { - const label: BurgLabel = { ...data, i: this.getNextId(), type: "burg", }; + const label: BurgLabel = { ...data, i: this.getNextId(), type: "burg" }; pack.labels.push(label); return label; } diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts index 0923a64d3..586d1fcb8 100644 --- a/src/renderers/draw-custom-labels.ts +++ b/src/renderers/draw-custom-labels.ts @@ -10,14 +10,19 @@ window.drawCustomLabels = customLabelRenderer; // ------------------------------------------------------------------------------- export function customLabelRenderer() { - const customLabels = Labels.getAll().filter(labels => labels.type === "custom") - const customLabelsHTML: string[] = []; - const pathGroup = defs.select( - "g#deftemp > g#textPaths", + const customLabels = Labels.getAll().filter( + (labels) => labels.type === "custom", ); + const customLabelsHTML: string[] = []; + const pathGroup = defs.select("g#deftemp > g#textPaths"); for (const labelData of customLabels) { const pathId = addPathForLabel(labelData, pathGroup.node()!); - console.log("Constructing label HTML for label", labelData, "with pathId", pathId); + console.log( + "Constructing label HTML for label", + labelData, + "with pathId", + pathId, + ); customLabelsHTML.push(constructLabelHTML(labelData, pathId).outerHTML); } @@ -28,26 +33,50 @@ export function customLabelRenderer() { } } -function constructLabelHTML(label: CustomLabel, pathId: string): SVGTextElement { +function constructLabelHTML( + label: CustomLabel, + pathId: string, +): SVGTextElement { const textParts = label.text.split("|"); const tspans = textParts.map((part, index) => { - const tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + const tspan = document.createElementNS( + "http://www.w3.org/2000/svg", + "tspan", + ); tspan.setAttribute("x", "0"); - tspan.setAttribute("dy", index ? "1em" : `${((textParts.length - 1) / -2)}em`); + tspan.setAttribute( + "dy", + index ? "1em" : `${(textParts.length - 1) / -2}em`, + ); tspan.textContent = part; return tspan; }); - const textPath = document.createElementNS("http://www.w3.org/2000/svg", "textPath"); + const textPath = document.createElementNS( + "http://www.w3.org/2000/svg", + "textPath", + ); textPath.setAttribute("href", `#${pathId}`); - textPath.setAttribute("startOffset", label.startOffset ? `${label.startOffset}%` : "50%"); - textPath.setAttribute("font-size", label.fontSize ? `${label.fontSize}%` : "100%"); + textPath.setAttribute( + "startOffset", + label.startOffset ? `${label.startOffset}%` : "50%", + ); + textPath.setAttribute( + "font-size", + label.fontSize ? `${label.fontSize}%` : "100%", + ); textPath.append(...tspans); - const textDom = document.createElementNS("http://www.w3.org/2000/svg", "text"); + const textDom = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); textDom.setAttribute("text-rendering", "optimizeSpeed"); textDom.setAttribute("id", `customLabel${label.i}`); - textDom.setAttribute("transform", `translate(${label.dx || 0}, ${label.dy || 0})`); + textDom.setAttribute( + "transform", + `translate(${label.dx || 0}, ${label.dy || 0})`, + ); textDom.appendChild(textPath); return textDom; @@ -55,11 +84,16 @@ function constructLabelHTML(label: CustomLabel, pathId: string): SVGTextElement function addPathForLabel(label: CustomLabel, pathGroup: SVGGElement): string { const pathId = `textPath_customLabel${label.i}`; - const pathData = line<[number, number]>().curve(curveNatural)(label.pathPoints || []); - const domPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); + const pathData = line<[number, number]>().curve(curveNatural)( + label.pathPoints || [], + ); + const domPath = document.createElementNS( + "http://www.w3.org/2000/svg", + "path", + ); domPath.setAttribute("id", pathId); domPath.setAttribute("d", pathData || ""); pathGroup.appendChild(domPath); return pathId; -} \ No newline at end of file +} diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index e4851a75f..9098bf67f 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -2,13 +2,7 @@ import { curveNatural, line, max, select } from "d3"; import type { StateLabel } from "../modules/labels"; import { ANGLES, findBestRayPair, raycast } from "./label-raycast"; import type { TypedArray } from "../types/PackedGraph"; -import { - findClosestCell, - minmax, - rn, - round, - splitInTwo, -} from "../utils"; +import { findClosestCell, minmax, rn, round, splitInTwo } from "../utils"; declare global { var drawStateLabels: (list?: number[]) => void; @@ -55,8 +49,8 @@ const stateLabelsRenderer = (list?: number[]): void => { const labelsToRender: StateLabel[] = list && list.length > 0 ? list - .map((idx) => Labels.get(idx)) - .filter((label) => label?.type === "state") + .map((idx) => Labels.get(idx)) + .filter((label) => label?.type === "state") : Labels.getAll().filter((label) => label.type === "state"); const letterLength = checkExampleLetterLength(); @@ -148,7 +142,10 @@ const stateLabelsRenderer = (list?: number[]): void => { .append("text") .attr("text-rendering", "optimizeSpeed") .attr("id", `stateLabel${labelData.i}`) - .attr("transform", `translate(${labelData.dx || 0}, ${labelData.dy || 0})`) + .attr( + "transform", + `translate(${labelData.dx || 0}, ${labelData.dy || 0})`, + ) .append("textPath") .attr("startOffset", "50%") .attr("font-size", `${ratio}%`) @@ -162,10 +159,7 @@ const stateLabelsRenderer = (list?: number[]): void => { textElement.insertAdjacentHTML("afterbegin", spans.join("")); const { width, height } = textElement.getBBox(); - textElement.setAttribute( - "href", - `#textPath_stateLabel${labelData.i}`, - ); + textElement.setAttribute("href", `#textPath_stateLabel${labelData.i}`); const stateIds = pack.cells.state; if (mode === "full" || lines.length === 1) continue; diff --git a/src/utils/pathUtils.ts b/src/utils/pathUtils.ts index 4a00124fd..7cfa4fbf2 100644 --- a/src/utils/pathUtils.ts +++ b/src/utils/pathUtils.ts @@ -382,7 +382,7 @@ export const extractPathPoints = (pathElement: SVGPathElement) => { points.push([point.x, point.y]); } return points; -} +}; declare global { interface Window { From fd0bd4d7c32dd0313ce86798fd57e04495dfad04 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 2 May 2026 21:02:51 +0200 Subject: [PATCH 45/66] apply lint --- src/renderers/draw-custom-labels.ts | 2 +- src/renderers/draw-state-labels.ts | 2 +- src/utils/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts index 586d1fcb8..b1b542d6a 100644 --- a/src/renderers/draw-custom-labels.ts +++ b/src/renderers/draw-custom-labels.ts @@ -1,5 +1,5 @@ import { curveNatural, line } from "d3"; -import { CustomLabel } from "../modules/labels"; +import type { CustomLabel } from "../modules/labels"; // remove this section once layer.js is refactored-------------------------------- declare global { diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index 9098bf67f..2616efb13 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -1,8 +1,8 @@ import { curveNatural, line, max, select } from "d3"; import type { StateLabel } from "../modules/labels"; -import { ANGLES, findBestRayPair, raycast } from "./label-raycast"; import type { TypedArray } from "../types/PackedGraph"; import { findClosestCell, minmax, rn, round, splitInTwo } from "../utils"; +import { ANGLES, findBestRayPair, raycast } from "./label-raycast"; declare global { var drawStateLabels: (list?: number[]) => void; diff --git a/src/utils/index.ts b/src/utils/index.ts index 0ba587d5d..67c419955 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -66,11 +66,11 @@ import { import { ensureEl, getComposedPath, getNextId } from "./nodeUtils"; import { connectVertices, + extractPathPoints, findPath, getIsolines, getPolesOfInaccessibility, getVertexPath, - extractPathPoints, } from "./pathUtils"; import { biased, From 27e35d4819d145f2ab9666777a568b5b83717e34 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 5 May 2026 22:37:06 +0200 Subject: [PATCH 46/66] improve burgLabels --- public/modules/ui/burg-editor.js | 9 ++++++++- public/modules/ui/labels-editor.js | 9 --------- src/modules/burgs-generator.ts | 16 ++++++++++++++-- src/modules/labels.ts | 5 ++++- src/renderers/draw-burg-labels.ts | 19 +++++++++---------- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index 3464407a0..8f3d8a6c1 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -109,7 +109,14 @@ function editBurg(id) { d3.event.on("drag", function () { const x = d3.event.x, y = d3.event.y; - this.setAttribute("transform", `translate(${dx + x},${dy + y})`); + const effectiveDx = dx + x; + const effectiveDy = dy + y; + this.setAttribute("transform", `translate(${effectiveDx},${effectiveDy})`); + const id = +elSelected.attr("id").slice(9); // remove "burgLabel" from id to get burg id + Labels.update(id, { + dx: effectiveDx, + dy: effectiveDy + }) tip('Use dragging for fine-tuning only, to actually move burg use "Relocate" button', false, "warning"); }); } diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index 5f4fddf30..c6131096c 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -11,15 +11,6 @@ function getLabelData(textElement) { if (id.startsWith("customLabel")) { return Labels.get(+id.slice(11)); } - // Custom labels: check for existing data-label-id attribute - const dataLabelId = textElement.getAttribute("data-label-id"); - if (dataLabelId != null) { - const existing = Labels.get(+dataLabelId); - if (existing) return existing; - // Data was cleared (e.g., map regenerated) — recreate - // textElement.removeAttribute("data-label-id"); - } - // No data entry found — create one from SVG state (migration path) return undefined; } diff --git a/src/modules/burgs-generator.ts b/src/modules/burgs-generator.ts index 5084bed33..ea37165e4 100644 --- a/src/modules/burgs-generator.ts +++ b/src/modules/burgs-generator.ts @@ -1,6 +1,7 @@ import { quadtree } from "d3-quadtree"; import { drawBurgLabel } from "../renderers/draw-burg-labels"; import { each, ensureEl, gauss, minmax, normalize, P, rn } from "../utils"; +import type { BurgLabel } from "./labels"; declare global { var Burgs: BurgModule; @@ -693,7 +694,13 @@ class BurgModule { if (newRoute && layerIsOn("toggleRoutes")) drawRoute(newRoute); drawBurgIcon(burg); - drawBurgLabel(burg); + Labels.addBurgLabel({ + burgId, + group: burg.group!, + text: burg.name!, + x, + y, + }); return burgId; } @@ -710,7 +717,12 @@ class BurgModule { } drawBurgIcon(burg); - drawBurgLabel(burg); + const labelToChange = Labels.getAll() + .filter((l) => l.type === "burg" && l.burgId === burg.i) + .at(0); + // Typescript cannot infer that labelToChange is guranteed to be a BurgLabel through the type check above. + // It can be cast without any issue. + if (labelToChange) drawBurgLabel(labelToChange as unknown as BurgLabel); } remove(burgId: number) { diff --git a/src/modules/labels.ts b/src/modules/labels.ts index adfe4b1a4..a603f361d 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -1,3 +1,5 @@ +import { drawBurgLabel } from "../renderers/draw-burg-labels"; + declare global { var Labels: LabelsModule; } @@ -83,9 +85,10 @@ class LabelsModule { return label; } - private addBurgLabel(data: Omit): BurgLabel { + addBurgLabel(data: Omit): BurgLabel { const label: BurgLabel = { ...data, i: this.getNextId(), type: "burg" }; pack.labels.push(label); + drawBurgLabel(label); return label; } diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index 511ca89fc..da4d28be1 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -1,4 +1,3 @@ -import type { Burg } from "../modules/burgs-generator"; import type { BurgLabel } from "../modules/labels"; interface BurgGroup { @@ -41,7 +40,7 @@ export function drawBurgLabels(): void { const labelsHTML: string[] = []; for (const labelData of labels) { labelsHTML.push( - `${labelData.text}`, + `${labelData.text}`, ); } @@ -55,9 +54,9 @@ export function drawBurgLabels(): void { TIME && console.timeEnd("drawBurgLabels"); } -export function drawBurgLabel(burg: Burg): void { +export function drawBurgLabel(burgLabel: BurgLabel): void { // TODO: remove label group dependency - for now, if group is missing, redraw all labels to recreate the group - const labelGroup = burgLabels.select(`#${burg.group}`); + const labelGroup = burgLabels.select(`#${burgLabel.group}`); if (labelGroup.empty()) { drawBurgLabels(); return; // redraw all labels if group is missing @@ -68,20 +67,20 @@ export function drawBurgLabel(burg: Burg): void { const dx = dxAttr ? parseFloat(dxAttr) : 0; const dy = dyAttr ? parseFloat(dyAttr) : 0; - const existingLabel = document.getElementById(`burgLabel${burg.i}`); + const existingLabel = document.getElementById(`burgLabel${burgLabel.burgId}`); if (existingLabel) existingLabel.remove(); // Render to SVG labelGroup .append("text") .attr("text-rendering", "optimizeSpeed") - .attr("id", `burgLabel${burg.i}`) - .attr("data-id", burg.i!) - .attr("x", burg.x) - .attr("y", burg.y) + .attr("id", `burgLabel${burgLabel.i}`) + .attr("data-id", burgLabel.burgId) + .attr("x", burgLabel.x) + .attr("y", burgLabel.y) .attr("dx", `${dx}em`) .attr("dy", `${dy}em`) - .text(burg.name!); + .text(burgLabel.text); } export function removeBurgLabel(burgId: number): void { From 3664ac82708531f9d2e2429f02bd7cc346dcde2d Mon Sep 17 00:00:00 2001 From: kruschen Date: Tue, 5 May 2026 22:44:40 +0200 Subject: [PATCH 47/66] Fix leaking paths when redrawing custom labels Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/renderers/draw-custom-labels.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts index b1b542d6a..59497840a 100644 --- a/src/renderers/draw-custom-labels.ts +++ b/src/renderers/draw-custom-labels.ts @@ -87,13 +87,15 @@ function addPathForLabel(label: CustomLabel, pathGroup: SVGGElement): string { const pathData = line<[number, number]>().curve(curveNatural)( label.pathPoints || [], ); - const domPath = document.createElementNS( - "http://www.w3.org/2000/svg", - "path", - ); + let domPath = pathGroup.querySelector(`#${pathId}`); + + if (!domPath) { + domPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); + pathGroup.appendChild(domPath); + } + domPath.setAttribute("id", pathId); domPath.setAttribute("d", pathData || ""); - pathGroup.appendChild(domPath); return pathId; } From 3aa69bf9bd5bbe4041a6a0992585a2fd6d537190 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 5 May 2026 23:11:08 +0200 Subject: [PATCH 48/66] improvements to migrating, burg removal, stale paths and label objects are now cleaned up --- public/modules/dynamic/auto-update.js | 2 +- src/modules/burgs-generator.ts | 3 ++- src/renderers/draw-custom-labels.ts | 7 +++++-- src/renderers/draw-state-labels.ts | 8 ++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index f32217f6f..69b30fcc8 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1147,7 +1147,7 @@ export function resolveVersionConflicts(mapVersion) { const letterSpacing = letterSpacingAttr ? parseFloat(letterSpacingAttr) : 0; const startOffsetAttr = textPath.getAttribute("startOffset"); const startOffset = startOffsetAttr ? parseFloat(startOffsetAttr) : 50; - const transform = textPath.getAttribute("transform"); + const transform = textElement.getAttribute("transform"); const [dx, dy] = transform ? parseTransform(transform) : [0, 0]; // Get path points from the referenced path diff --git a/src/modules/burgs-generator.ts b/src/modules/burgs-generator.ts index ea37165e4..a8675839e 100644 --- a/src/modules/burgs-generator.ts +++ b/src/modules/burgs-generator.ts @@ -1,5 +1,5 @@ import { quadtree } from "d3-quadtree"; -import { drawBurgLabel } from "../renderers/draw-burg-labels"; +import { drawBurgLabel, removeBurgLabel } from "../renderers/draw-burg-labels"; import { each, ensureEl, gauss, minmax, normalize, P, rn } from "../utils"; import type { BurgLabel } from "./labels"; @@ -746,6 +746,7 @@ class BurgModule { (l) => l.type === "burg" && l.burgId === burgId, ); Labels.remove(labelId); + removeBurgLabel(burgId); } } window.Burgs = new BurgModule(); diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts index 59497840a..374b8cb16 100644 --- a/src/renderers/draw-custom-labels.ts +++ b/src/renderers/draw-custom-labels.ts @@ -59,12 +59,15 @@ function constructLabelHTML( textPath.setAttribute("href", `#${pathId}`); textPath.setAttribute( "startOffset", - label.startOffset ? `${label.startOffset}%` : "50%", + label.startOffset != null ? `${label.startOffset}%` : "50%", ); textPath.setAttribute( "font-size", - label.fontSize ? `${label.fontSize}%` : "100%", + label.fontSize != null ? `${label.fontSize}%` : "100%", ); + if (label.letterSpacing !== undefined) { + textPath.setAttribute("letter-spacing", `${label.letterSpacing}`); + } textPath.append(...tspans); const textDom = document.createElementNS( diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index 2616efb13..9b2e33ad0 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -101,13 +101,13 @@ const stateLabelsRenderer = (list?: number[]): void => { ]; if (ray1.x > ray2.x) pathPoints.reverse(); - textGroup.select(`#stateLabel${labelData.stateId}`).remove(); - pathGroup.select(`#textPath_stateLabel${labelData.stateId}`).remove(); + textGroup.select(`#stateLabel${labelData.i}`).remove(); + pathGroup.select(`#textPath_stateLabel${labelData.i}`).remove(); const textPath = pathGroup .append("path") .attr("d", round(lineGen(pathPoints) || "")) - .attr("id", `textPath_stateLabel${labelData.stateId}`); + .attr("id", `textPath_stateLabel${labelData.i}`); const pathLength = (textPath.node() as SVGPathElement).getTotalLength() / letterLength; // path length in letters @@ -158,8 +158,8 @@ const stateLabelsRenderer = (list?: number[]): void => { ); textElement.insertAdjacentHTML("afterbegin", spans.join("")); - const { width, height } = textElement.getBBox(); textElement.setAttribute("href", `#textPath_stateLabel${labelData.i}`); + const { width, height } = textElement.getBBox(); const stateIds = pack.cells.state; if (mode === "full" || lines.length === 1) continue; From de3c86116234c0afa28be089d0ad3f3b126ad39b Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 5 May 2026 23:13:18 +0200 Subject: [PATCH 49/66] remove left over console print --- src/renderers/draw-custom-labels.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts index 374b8cb16..0290d1a7d 100644 --- a/src/renderers/draw-custom-labels.ts +++ b/src/renderers/draw-custom-labels.ts @@ -17,12 +17,6 @@ export function customLabelRenderer() { const pathGroup = defs.select("g#deftemp > g#textPaths"); for (const labelData of customLabels) { const pathId = addPathForLabel(labelData, pathGroup.node()!); - console.log( - "Constructing label HTML for label", - labelData, - "with pathId", - pathId, - ); customLabelsHTML.push(constructLabelHTML(labelData, pathId).outerHTML); } From 7cc7a695c7af05961f5fdaa733ba1864f2a84143 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 5 May 2026 23:15:12 +0200 Subject: [PATCH 50/66] fix: id properly incements when updating older maps --- public/modules/dynamic/auto-update.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index 69b30fcc8..66c4e0d6f 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1121,7 +1121,6 @@ export function resolveVersionConflicts(mapVersion) { // Migrate old SVG labels to pack.labels structure if (!pack.labels || !pack.labels.length) { pack.labels = []; - let labelId = 0; // Migrate state labels const stateLabelsGroup = document.querySelector("#labels > #states"); @@ -1199,7 +1198,7 @@ export function resolveVersionConflicts(mapVersion) { const y = burg.y; pack.labels.push({ - i: labelId++, + i: pack.labels.length, type: "burg", burgId, group, From 5ce7498ad976fad5387018ffeabaecd487abf0e5 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Tue, 5 May 2026 23:19:34 +0200 Subject: [PATCH 51/66] fix: custom label transforms are now properly migrated --- public/modules/dynamic/auto-update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index 66c4e0d6f..ae4731610 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -1230,7 +1230,7 @@ export function resolveVersionConflicts(mapVersion) { const letterSpacing = letterSpacingAttr ? parseFloat(letterSpacingAttr) : 0; const startOffsetAttr = textPath.getAttribute("startOffset"); const startOffset = startOffsetAttr ? parseFloat(startOffsetAttr) : 50; - const transform = textPath.getAttribute("transform"); + const transform = textElement.getAttribute("transform"); const [dx, dy] = transform ? parseTransform(transform) : [0, 0]; const href = textPath.getAttribute("xlink:href") || textPath.getAttribute("href"); From e80c8595c4f16ba2dc70fb2198580f313e377e21 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Wed, 6 May 2026 00:13:11 +0200 Subject: [PATCH 52/66] fix: burg change name is now consistant with the index --- public/modules/ui/burg-editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index 8f3d8a6c1..af5d801b8 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -125,8 +125,8 @@ function editBurg(id) { const id = +elSelected.attr("data-id"); pack.burgs[id].name = burgName.value; elSelected.text(burgName.value); - // Sync to Labels data model - Labels.update(id, { text: burgName.value }); + const labelId = Labels.getAll().findIndex(label => label.type === "burg" && label.burgId === id); + Labels.update(labelId, { text: burgName.value }); } function generateNameRandom() { From 9eb45be26cbb36574965bd17468d8dfcdd2c2116 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Wed, 6 May 2026 00:19:16 +0200 Subject: [PATCH 53/66] unified id checking in burg-editor.js and apply transform reset on relocation of burg to label --- public/modules/ui/burg-editor.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index af5d801b8..cd4ca7e57 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -112,8 +112,8 @@ function editBurg(id) { const effectiveDx = dx + x; const effectiveDy = dy + y; this.setAttribute("transform", `translate(${effectiveDx},${effectiveDy})`); - const id = +elSelected.attr("id").slice(9); // remove "burgLabel" from id to get burg id - Labels.update(id, { + const labelId = Labels.getAll().findIndex(label => label.type === "burg" && label.burgId === id); + Labels.update(labelId, { dx: effectiveDx, dy: effectiveDy }) @@ -393,7 +393,8 @@ function editBurg(id) { burg.y = y; if (burg.capital) pack.states[newState].center = burg.cell; - Labels.update(id, { x, y }); + const labelId = Labels.getAll().findIndex(label => label.type === "burg" && label.burgId === id); + Labels.update(labelId, { x, y, dx: 0, dy: 0 }); if (d3.event.shiftKey === false) toggleRelocateBurg(); } From ec742de690e5b66d6c40d096958ec6b4f4e184a7 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Wed, 6 May 2026 00:25:36 +0200 Subject: [PATCH 54/66] fix: burg label id issue by false assumption --- public/modules/ui/burg-editor.js | 13 +++++++------ src/modules/burgs-generator.ts | 6 ++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index cd4ca7e57..957c02659 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -112,8 +112,8 @@ function editBurg(id) { const effectiveDx = dx + x; const effectiveDy = dy + y; this.setAttribute("transform", `translate(${effectiveDx},${effectiveDy})`); - const labelId = Labels.getAll().findIndex(label => label.type === "burg" && label.burgId === id); - Labels.update(labelId, { + const label = Labels.getAll().find(label => label.type === "burg" && label.burgId === id); + Labels.update(label.i, { dx: effectiveDx, dy: effectiveDy }) @@ -125,8 +125,9 @@ function editBurg(id) { const id = +elSelected.attr("data-id"); pack.burgs[id].name = burgName.value; elSelected.text(burgName.value); - const labelId = Labels.getAll().findIndex(label => label.type === "burg" && label.burgId === id); - Labels.update(labelId, { text: burgName.value }); + + const label = Labels.getAll().find(label => label.type === "burg" && label.burgId === id); + Labels.update(label.i, { text: burgName.value }); } function generateNameRandom() { @@ -393,8 +394,8 @@ function editBurg(id) { burg.y = y; if (burg.capital) pack.states[newState].center = burg.cell; - const labelId = Labels.getAll().findIndex(label => label.type === "burg" && label.burgId === id); - Labels.update(labelId, { x, y, dx: 0, dy: 0 }); + const label = Labels.getAll().find(label => label.type === "burg" && label.burgId === id); + Labels.update(label.i, { x, y, dx: 0, dy: 0 }); if (d3.event.shiftKey === false) toggleRelocateBurg(); } diff --git a/src/modules/burgs-generator.ts b/src/modules/burgs-generator.ts index a8675839e..aa1c6cd46 100644 --- a/src/modules/burgs-generator.ts +++ b/src/modules/burgs-generator.ts @@ -742,10 +742,8 @@ class BurgModule { } removeBurgIcon(burg.i!); - const labelId = pack.labels.findIndex( - (l) => l.type === "burg" && l.burgId === burgId, - ); - Labels.remove(labelId); + const label = Labels.getAll().find((l) => l.type === "burg" && l.burgId === burgId); + if (label) Labels.remove(label.i); removeBurgLabel(burgId); } } From 6808bb9d1611b37ce438694b3c8c126c33e2b905 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Wed, 6 May 2026 21:27:45 +0200 Subject: [PATCH 55/66] fix burg related label bugs --- public/modules/ui/burg-editor.js | 11 ++++++----- src/renderers/draw-burg-labels.ts | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/public/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js index 957c02659..4aa72d9a3 100644 --- a/public/modules/ui/burg-editor.js +++ b/public/modules/ui/burg-editor.js @@ -112,11 +112,11 @@ function editBurg(id) { const effectiveDx = dx + x; const effectiveDy = dy + y; this.setAttribute("transform", `translate(${effectiveDx},${effectiveDy})`); - const label = Labels.getAll().find(label => label.type === "burg" && label.burgId === id); + const label = Labels.getAll().find((l) => l.type === "burg" && l.burgId === +burg); Labels.update(label.i, { dx: effectiveDx, dy: effectiveDy - }) + }); tip('Use dragging for fine-tuning only, to actually move burg use "Relocate" button', false, "warning"); }); } @@ -126,7 +126,7 @@ function editBurg(id) { pack.burgs[id].name = burgName.value; elSelected.text(burgName.value); - const label = Labels.getAll().find(label => label.type === "burg" && label.burgId === id); + const label = Labels.getAll().find((l) => l.type === "burg" && l.burgId === +burg); Labels.update(label.i, { text: burgName.value }); } @@ -374,8 +374,10 @@ function editBurg(id) { const x = rn(point[0], 2); const y = rn(point[1], 2); + const label = Labels.getAll().find(l=> l.type === "burg" && l.burgId === id); + burgIcons.select(`#burg${id}`).attr("x", x).attr("y", y); - burgLabels.select(`#burgLabel${id}`).attr("transform", null).attr("x", x).attr("y", y); + burgLabels.select(`#burgLabel${label.i}`).attr("transform", null).attr("x", x).attr("y", y); const anchor = anchors.select("use[data-id='" + id + "']"); if (anchor.size()) { @@ -394,7 +396,6 @@ function editBurg(id) { burg.y = y; if (burg.capital) pack.states[newState].center = burg.cell; - const label = Labels.getAll().find(label => label.type === "burg" && label.burgId === id); Labels.update(label.i, { x, y, dx: 0, dy: 0 }); if (d3.event.shiftKey === false) toggleRelocateBurg(); diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index da4d28be1..494b2691b 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -37,17 +37,26 @@ export function drawBurgLabels(): void { const dx = dxAttr ? parseFloat(dxAttr) : 0; const dy = dyAttr ? parseFloat(dyAttr) : 0; - const labelsHTML: string[] = []; + const labelsHTML: SVGTextElement[] = []; for (const labelData of labels) { + const textElement = document.createElementNS("http://www.w3.org/2000/svg", "text"); + textElement.setAttribute("text-rendering", "optimizeSpeed"); + textElement.setAttribute("id", `burgLabel${labelData.i}`); + textElement.setAttribute("data-id", labelData.burgId.toString()); + textElement.setAttribute("x", labelData.x.toString()); + textElement.setAttribute("y", labelData.y.toString()); + textElement.setAttribute("dx", `${dx}em`); + textElement.setAttribute("dy", `${dy}em`); + textElement.textContent = labelData.text; labelsHTML.push( - `${labelData.text}`, + textElement ); } // Set all labels at once const groupNode = labelGroup.node(); if (groupNode) { - groupNode.innerHTML = labelsHTML.join(""); + groupNode.append(...labelsHTML); } } From d66d6d2e32aa97bce9957b5c980363bea153b502 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Wed, 6 May 2026 23:58:32 +0200 Subject: [PATCH 56/66] add group rendering to custom labels and update burg label rendering --- src/renderers/draw-burg-labels.ts | 2 +- src/renderers/draw-custom-labels.ts | 31 +++++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index 494b2691b..fda113d40 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -56,7 +56,7 @@ export function drawBurgLabels(): void { // Set all labels at once const groupNode = labelGroup.node(); if (groupNode) { - groupNode.append(...labelsHTML); + groupNode.replaceChildren(...labelsHTML); } } diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts index 0290d1a7d..7a647d694 100644 --- a/src/renderers/draw-custom-labels.ts +++ b/src/renderers/draw-custom-labels.ts @@ -10,20 +10,31 @@ window.drawCustomLabels = customLabelRenderer; // ------------------------------------------------------------------------------- export function customLabelRenderer() { - const customLabels = Labels.getAll().filter( + const allCustomLabels = Labels.getAll().filter( (labels) => labels.type === "custom", ); - const customLabelsHTML: string[] = []; - const pathGroup = defs.select("g#deftemp > g#textPaths"); - for (const labelData of customLabels) { - const pathId = addPathForLabel(labelData, pathGroup.node()!); - customLabelsHTML.push(constructLabelHTML(labelData, pathId).outerHTML); + const customLabelsByGroup = new Map(); + for (const label of allCustomLabels) { + if (!customLabelsByGroup.has(label.group)) { + customLabelsByGroup.set(label.group, []); + } + customLabelsByGroup.get(label.group)!.push(label); } - const customLabelsGroup = labels.select(`#addedLabels`); - const groupNode = customLabelsGroup.node(); - if (groupNode) { - groupNode.innerHTML = customLabelsHTML.join(""); + const customLabelsHTML: SVGTextElement[] = []; + const pathGroup = defs.select("g#deftemp > g#textPaths"); + + for (const [groupName, customLabels] of customLabelsByGroup) { + for (const labelData of customLabels) { + const pathId = addPathForLabel(labelData, pathGroup.node()!); + customLabelsHTML.push(constructLabelHTML(labelData, pathId)); + } + + const customLabelsGroup = labels.select(`#${groupName}`); + const groupNode = customLabelsGroup.node(); + if (groupNode) { + groupNode.replaceChildren(...customLabelsHTML); + } } } From d1a356bf05acca2a3f692896095eb6bcaa02fe1a Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 7 May 2026 01:17:08 +0200 Subject: [PATCH 57/66] fix state label draw call by expecting state id now --- src/renderers/draw-state-labels.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index 9b2e33ad0..bd90fd0ea 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -46,12 +46,17 @@ const stateLabelsRenderer = (list?: number[]): void => { const { states } = pack; // Get labels to render - const labelsToRender: StateLabel[] = - list && list.length > 0 - ? list - .map((idx) => Labels.get(idx)) - .filter((label) => label?.type === "state") - : Labels.getAll().filter((label) => label.type === "state"); + let labelsToRender: StateLabel[]; + if (list && list.length > 0) { + labelsToRender = list.flatMap( + (id) => + Labels.getAll().filter( + (label) => label.type === "state" && label.stateId === id, + ) as StateLabel[], + ); + } else { + labelsToRender = Labels.getAll().filter((label) => label.type === "state"); + } const letterLength = checkExampleLetterLength(); drawLabelPath(letterLength, labelsToRender); From 192a609900070c5af10b69a8c3289f7fccde9956 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 7 May 2026 01:17:16 +0200 Subject: [PATCH 58/66] apply formatter --- src/modules/burgs-generator.ts | 4 +++- src/renderers/draw-burg-labels.ts | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/modules/burgs-generator.ts b/src/modules/burgs-generator.ts index aa1c6cd46..0680339f7 100644 --- a/src/modules/burgs-generator.ts +++ b/src/modules/burgs-generator.ts @@ -742,7 +742,9 @@ class BurgModule { } removeBurgIcon(burg.i!); - const label = Labels.getAll().find((l) => l.type === "burg" && l.burgId === burgId); + const label = Labels.getAll().find( + (l) => l.type === "burg" && l.burgId === burgId, + ); if (label) Labels.remove(label.i); removeBurgLabel(burgId); } diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index fda113d40..c0c3d5ba6 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -39,7 +39,10 @@ export function drawBurgLabels(): void { const labelsHTML: SVGTextElement[] = []; for (const labelData of labels) { - const textElement = document.createElementNS("http://www.w3.org/2000/svg", "text"); + const textElement = document.createElementNS( + "http://www.w3.org/2000/svg", + "text", + ); textElement.setAttribute("text-rendering", "optimizeSpeed"); textElement.setAttribute("id", `burgLabel${labelData.i}`); textElement.setAttribute("data-id", labelData.burgId.toString()); @@ -48,9 +51,7 @@ export function drawBurgLabels(): void { textElement.setAttribute("dx", `${dx}em`); textElement.setAttribute("dy", `${dy}em`); textElement.textContent = labelData.text; - labelsHTML.push( - textElement - ); + labelsHTML.push(textElement); } // Set all labels at once From 74c86b07b209f285fe048055446ca69caeb26ed5 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 7 May 2026 01:23:25 +0200 Subject: [PATCH 59/66] add timeing to custom label drawing --- src/renderers/draw-custom-labels.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts index 7a647d694..1a1ed7a97 100644 --- a/src/renderers/draw-custom-labels.ts +++ b/src/renderers/draw-custom-labels.ts @@ -10,6 +10,7 @@ window.drawCustomLabels = customLabelRenderer; // ------------------------------------------------------------------------------- export function customLabelRenderer() { + TIME && console.time("drawCustomLabels"); const allCustomLabels = Labels.getAll().filter( (labels) => labels.type === "custom", ); @@ -36,6 +37,7 @@ export function customLabelRenderer() { groupNode.replaceChildren(...customLabelsHTML); } } + TIME && console.timeEnd("drawCustomLabels"); } function constructLabelHTML( From 84c4ff6694319b8373b6a9e3f72f95a185284901 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 7 May 2026 01:26:51 +0200 Subject: [PATCH 60/66] rename drawBurgFunction to pass linter, can be renamed when migrated --- src/renderers/draw-burg-labels.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderers/draw-burg-labels.ts b/src/renderers/draw-burg-labels.ts index c0c3d5ba6..77b44b3a2 100644 --- a/src/renderers/draw-burg-labels.ts +++ b/src/renderers/draw-burg-labels.ts @@ -10,10 +10,10 @@ declare global { var drawBurgLabels: () => void; } -window.drawBurgLabels = drawBurgLabels; +window.drawBurgLabels = drawBurgLabelsRenderer; // section end ------------------------------------------------------------------- -export function drawBurgLabels(): void { +export function drawBurgLabelsRenderer(): void { TIME && console.time("drawBurgLabels"); createLabelGroups(); @@ -68,7 +68,7 @@ export function drawBurgLabel(burgLabel: BurgLabel): void { // TODO: remove label group dependency - for now, if group is missing, redraw all labels to recreate the group const labelGroup = burgLabels.select(`#${burgLabel.group}`); if (labelGroup.empty()) { - drawBurgLabels(); + drawBurgLabelsRenderer(); return; // redraw all labels if group is missing } From e3500270d98616d3892dda7bac9ebfc26d55f919 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 7 May 2026 01:33:24 +0200 Subject: [PATCH 61/66] better null check for updated values in lables-editor --- public/modules/ui/labels-editor.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js index c6131096c..0efb62bb6 100644 --- a/public/modules/ui/labels-editor.js +++ b/public/modules/ui/labels-editor.js @@ -102,10 +102,10 @@ function editLabel() { function updateValues(textPath) { const labelData = getLabelData(elSelected.node()); if (labelData) { - ensureEl("labelText").value = labelData.text || ""; - ensureEl("labelStartOffset").value = labelData.startOffset || 50; - ensureEl("labelRelativeSize").value = labelData.fontSize || 100; - ensureEl("labelLetterSpacingSize").value = labelData.letterSpacing || 0; + ensureEl("labelText").value = labelData.text ?? ""; + ensureEl("labelStartOffset").value = labelData.startOffset ?? 50; + ensureEl("labelRelativeSize").value = labelData.fontSize ?? 100; + ensureEl("labelLetterSpacingSize").value = labelData.letterSpacing ?? 0; } } From b791da59da1b1df6b1d8eeee2641b8ac466f393c Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 7 May 2026 23:27:23 +0200 Subject: [PATCH 62/66] experimental getNextId --- src/modules/labels.ts | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index a603f361d..84cee6929 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -43,16 +43,36 @@ export interface CustomLabel { export type LabelData = StateLabel | BurgLabel | CustomLabel; class LabelsModule { - // Gets the next possible Label ID in O(n log n) time. Allows for non-sequential IDs + private freeIds: Set = new Set(); + private maxId: number = 0; + // initialization flag as the constructor version doesn't blocks other modules from beeing initialized. + private initialized: boolean = false; + + private getNextId(): number { - const labels = pack.labels; - if (labels.length === 0) return 0; + if (!this.initialized) { + this.initialized = true; + this.freeIds.clear(); + const existingIds = pack.labels.map((l) => l.i).sort((a, b) => a - b); + + for (let id = 0; id < existingIds[existingIds.length - 1]; id++) { + if (!existingIds.includes(id)) this.freeIds.add(id); + } - const existingIds = labels.map((l) => l.i).sort((a, b) => a - b); - for (let id = 0; id < existingIds[existingIds.length - 1]; id++) { - if (!existingIds.includes(id)) return id; + this.maxId = existingIds.length > 0 ? existingIds[existingIds.length - 1] + 1 : 0; } - return existingIds[existingIds.length - 1] + 1; + + if (this.freeIds.size > 0) { + // Get and remove the next available ID from the freeIds set + const id = this.freeIds.values().next().value!; + this.freeIds.delete(id); + return id; + } + + // maxId is always 1 greater than the current highest ID, so we can return it and then increment for the next call + const nextId = this.maxId; + this.maxId++; + return nextId; } generate(): void { @@ -113,14 +133,17 @@ class LabelsModule { remove(id: number): void { const index = pack.labels.findIndex((l) => l.i === id); + this.freeIds.add(id); if (index !== -1) pack.labels.splice(index, 1); } removeByType(type: LabelData["type"]): void { + this.initialized = false; pack.labels = pack.labels.filter((l) => l.type !== type); } removeByGroup(group: string): void { + this.initialized = false; pack.labels = pack.labels.filter( (l) => !((l.type === "burg" || l.type === "custom") && l.group === group), ); @@ -128,6 +151,7 @@ class LabelsModule { clear(): void { pack.labels = []; + this.initialized = false; } /** From 50955d8286561887b6f4b489253cbc96e897b8f3 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Thu, 7 May 2026 23:45:26 +0200 Subject: [PATCH 63/66] remove label text elements from DOM when layer is off --- public/modules/ui/layers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/modules/ui/layers.js b/public/modules/ui/layers.js index ec1359cd8..7c0bc5c8a 100644 --- a/public/modules/ui/layers.js +++ b/public/modules/ui/layers.js @@ -857,12 +857,12 @@ function toggleLabels(event) { if (!layerIsOn("toggleLabels")) { turnButtonOn("toggleLabels"); $("#labels").fadeIn(); - // don't redraw labels as they are not stored in data yet if (labels.selectAll("text").size() === 0) drawLabels(); if (event && isCtrlClick(event)) editStyle("labels"); } else { if (event && isCtrlClick(event)) return editStyle("labels"); turnButtonOff("toggleLabels"); + labels.selectAll("text").remove(); $("#labels").fadeOut(); } } From 2108b9a83609e0adce9bf12ae9cc50e488fcce7e Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 8 May 2026 23:42:04 +0200 Subject: [PATCH 64/66] custom label adding now uses Labels module --- public/modules/ui/tools.js | 34 ++++++++++------------------- src/renderers/draw-custom-labels.ts | 25 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/public/modules/ui/tools.js b/public/modules/ui/tools.js index 4d150d1eb..7fb98fae6 100644 --- a/public/modules/ui/tools.js +++ b/public/modules/ui/tools.js @@ -618,9 +618,9 @@ function addLabelOnClick() { // use most recently selected label group const lastSelected = labelGroupSelect.value; - const groupId = ["", "states", "burgLabels"].includes(lastSelected) ? "#addedLabels" : "#" + lastSelected; + const groupId = ["", "states", "burgLabels"].includes(lastSelected) ? "addedLabels" : lastSelected; - let group = labels.select(groupId); + let group = labels.select(`#${groupId}`); if (!group.size()) group = labels .append("g") @@ -637,26 +637,16 @@ function addLabelOnClick() { const example = group.append("text").attr("x", 0).attr("y", 0).text(name); const width = example.node().getBBox().width; example.remove(); - - group.classed("hidden", false); - group - .append("text") - .attr("text-rendering", "optimizeSpeed") - .attr("id", id) - .append("textPath") - .attr("text-rendering", "optimizeSpeed") - .attr("xlink:href", "#textPath_" + id) - .attr("startOffset", "50%") - .attr("font-size", "100%") - .append("tspan") - .attr("x", 0) - .text(name); - - defs - .select("#textPaths") - .append("path") - .attr("id", "textPath_" + id) - .attr("d", `M${point[0] - width},${point[1]} h${width * 2}`); + const newLabel = Labels.addCustomLabel({ + group: groupId, + text: name, + pathPoints: [ + [rn(point[0] - width), point[1]], + [rn(point[0] + width), point[1]] + ] + }) + + drawCustomLabel(newLabel); if (d3.event.shiftKey === false) unpressClickToAddButton(); } diff --git a/src/renderers/draw-custom-labels.ts b/src/renderers/draw-custom-labels.ts index 1a1ed7a97..a3f7c5904 100644 --- a/src/renderers/draw-custom-labels.ts +++ b/src/renderers/draw-custom-labels.ts @@ -4,12 +4,14 @@ import type { CustomLabel } from "../modules/labels"; // remove this section once layer.js is refactored-------------------------------- declare global { var drawCustomLabels: () => void; + var drawCustomLabel: (label: CustomLabel) => void; } -window.drawCustomLabels = customLabelRenderer; +window.drawCustomLabels = customLabelsRenderer; +window.drawCustomLabel = customLabelRenderer; // ------------------------------------------------------------------------------- -export function customLabelRenderer() { +export function customLabelsRenderer() { TIME && console.time("drawCustomLabels"); const allCustomLabels = Labels.getAll().filter( (labels) => labels.type === "custom", @@ -40,6 +42,25 @@ export function customLabelRenderer() { TIME && console.timeEnd("drawCustomLabels"); } +export function customLabelRenderer(label: CustomLabel) { + const pathGroup = defs.select("g#deftemp > g#textPaths"); + const pathId = addPathForLabel(label, pathGroup.node()!); + const labelHTML = constructLabelHTML(label, pathId); + + const customLabelsGroup = labels.select(`#${label.group}`); + const groupNode = customLabelsGroup.node(); + if (groupNode) { + const existingLabel = groupNode.querySelector( + `#customLabel${label.i}`, + ); + if (existingLabel) { + existingLabel.replaceWith(labelHTML); + } else { + groupNode.appendChild(labelHTML); + } + } +} + function constructLabelHTML( label: CustomLabel, pathId: string, From 18231da08fd3f23cf01dccae5b3dc9cdee427f46 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Fri, 8 May 2026 23:48:25 +0200 Subject: [PATCH 65/66] format + lint pass --- src/modules/labels.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/labels.ts b/src/modules/labels.ts index 84cee6929..ce2d6794c 100644 --- a/src/modules/labels.ts +++ b/src/modules/labels.ts @@ -48,7 +48,6 @@ class LabelsModule { // initialization flag as the constructor version doesn't blocks other modules from beeing initialized. private initialized: boolean = false; - private getNextId(): number { if (!this.initialized) { this.initialized = true; @@ -59,7 +58,8 @@ class LabelsModule { if (!existingIds.includes(id)) this.freeIds.add(id); } - this.maxId = existingIds.length > 0 ? existingIds[existingIds.length - 1] + 1 : 0; + this.maxId = + existingIds.length > 0 ? existingIds[existingIds.length - 1] + 1 : 0; } if (this.freeIds.size > 0) { @@ -72,7 +72,7 @@ class LabelsModule { // maxId is always 1 greater than the current highest ID, so we can return it and then increment for the next call const nextId = this.maxId; this.maxId++; - return nextId; + return nextId; } generate(): void { From 919a7667a337f9ae2a121044894de8855b220e89 Mon Sep 17 00:00:00 2001 From: StempunkDev Date: Sat, 9 May 2026 00:20:07 +0200 Subject: [PATCH 66/66] set text befor check in draw-state-labels --- src/renderers/draw-state-labels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index bd90fd0ea..eca8a0bae 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -124,7 +124,7 @@ const stateLabelsRenderer = (list?: number[]): void => { ); // Update label data with font size - Labels.update(labelData.i, { fontSize: ratio }); + Labels.update(labelData.i, { text: lines.join("|"), fontSize: ratio }); // prolongate path if it's too short const longestLineLength = max(lines.map((line) => line.length)) || 0;