mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-23 07:37:24 +01:00
feat: integrate label generation into main flow and enhance label data handling
This commit is contained in:
parent
c467f87df5
commit
94b638f3cb
4 changed files with 219 additions and 104 deletions
|
|
@ -650,6 +650,8 @@ async function generate(options) {
|
|||
Provinces.generate();
|
||||
Provinces.getPoles();
|
||||
|
||||
Labels.generate();
|
||||
|
||||
Rivers.specify();
|
||||
Lakes.defineNames();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<string, BurgLabelData[]>();
|
||||
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<SVGGElement>(`#${name}`);
|
||||
// Render each group and update label offsets from SVG attributes
|
||||
for (const [groupName, labels] of burgLabelsByGroup) {
|
||||
const labelGroup = burgLabels.select<SVGGElement>(`#${groupName}`);
|
||||
if (labelGroup.empty()) continue;
|
||||
|
||||
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;
|
||||
|
||||
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!);
|
||||
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<SVGGElement>(`#${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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
// list - an optional array of stateIds to regenerate
|
||||
function checkExampleLetterLength(): number {
|
||||
const textGroup = select<SVGGElement, unknown>("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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<SVGGElement, unknown>("g#labels > g#states");
|
||||
const pathGroup = select<SVGGElement, unknown>(
|
||||
"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<SVGGElement, unknown>("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<SVGGElement, unknown>("g#labels > g#states");
|
||||
const pathGroup = select<SVGGElement, unknown>(
|
||||
"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 = `<tspan x="0">${text}</tspan>`;
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue