mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
196 lines
5.9 KiB
TypeScript
196 lines
5.9 KiB
TypeScript
import * as d3 from "d3";
|
|
|
|
import {findCell} from "utils/graphUtils";
|
|
import {isState} from "utils/typeUtils";
|
|
import {drawPath, drawPoint} from "utils/debugUtils";
|
|
|
|
export function drawLabels() {
|
|
/* global */ const {cells, vertices, features, states, burgs} = pack;
|
|
/* global: findCell, graphWidth, graphHeight */
|
|
|
|
drawStateLabels(cells, features, states, vertices);
|
|
// drawBurgLabels(burgs);
|
|
// TODO: draw other labels
|
|
|
|
window.Zoom.invoke();
|
|
}
|
|
|
|
function drawBurgLabels(burgs: TBurgs) {
|
|
// remove old data
|
|
burgLabels.selectAll("text").remove();
|
|
|
|
const validBurgs = burgs.filter(burg => burg.i && !(burg as IBurg).removed) as IBurg[];
|
|
|
|
// capitals
|
|
const capitals = validBurgs.filter(burg => burg.capital);
|
|
const capitalSize = Number(burgIcons.select("#cities").attr("size")) || 1;
|
|
|
|
burgLabels
|
|
.select("#cities")
|
|
.selectAll("text")
|
|
.data(capitals)
|
|
.enter()
|
|
.append("text")
|
|
.attr("id", d => "burgLabel" + d.i)
|
|
.attr("data-id", d => d.i)
|
|
.attr("x", d => d.x)
|
|
.attr("y", d => d.y)
|
|
.attr("dy", `${capitalSize * -1.5}px`)
|
|
.text(d => d.name);
|
|
|
|
// towns
|
|
const towns = validBurgs.filter(burg => !burg.capital);
|
|
const townSize = Number(burgIcons.select("#towns").attr("size")) || 0.5;
|
|
|
|
burgLabels
|
|
.select("#towns")
|
|
.selectAll("text")
|
|
.data(towns)
|
|
.enter()
|
|
.append("text")
|
|
.attr("id", d => "burgLabel" + d.i)
|
|
.attr("data-id", d => d.i)
|
|
.attr("x", d => d.x)
|
|
.attr("y", d => d.y)
|
|
.attr("dy", `${townSize * -1.5}px`)
|
|
.text(d => d.name);
|
|
}
|
|
|
|
function drawStateLabels(cells: IPack["cells"], features: TPackFeatures, states: TStates, vertices: IGraphVertices) {
|
|
console.time("drawStateLabels");
|
|
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
|
|
const mode = options.stateLabelsMode || "auto";
|
|
|
|
// increase step to increase performarce and make more horyzontal, decrease to increase accuracy
|
|
const STEP = 9;
|
|
const raycast = precalculateAngles(STEP);
|
|
|
|
const INITIAL_DISTANCE = 5;
|
|
const DISTANCE_STEP = 15;
|
|
const MAX_ITERATIONS = 100;
|
|
|
|
const labelPaths = getLabelPaths();
|
|
|
|
function getLabelPaths() {
|
|
const labelPaths: [number, TPoints][] = [];
|
|
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
|
|
|
|
for (const state of states) {
|
|
if (!isState(state)) continue;
|
|
|
|
const offset = getOffsetWidth(state.cells);
|
|
const [x0, y0] = state.pole;
|
|
|
|
const offsetPoints = new Map(
|
|
(offset ? raycast : []).map(({angle, x: x1, y: y1}) => {
|
|
const [x, y] = [x0 + offset * x1, y0 + offset * y1];
|
|
return [angle, {x, y}];
|
|
})
|
|
);
|
|
|
|
const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => {
|
|
let distanceMin: number;
|
|
|
|
if (offset) {
|
|
const point1 = offsetPoints.get(angle + 90 >= 360 ? angle - 270 : angle + 90)!;
|
|
const distance1 = getMaxDistance(state.i, point1, dx, dy);
|
|
|
|
const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90)!;
|
|
const distance2 = getMaxDistance(state.i, point2, dx, dy);
|
|
distanceMin = Math.min(distance1, distance2);
|
|
} else {
|
|
distanceMin = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy);
|
|
}
|
|
|
|
const [x, y] = [x0 + distanceMin * dx, y0 + distanceMin * dy];
|
|
return {angle, distance: distanceMin * modifier, x, y};
|
|
});
|
|
|
|
const {angle, x, y} = distances.reduce(
|
|
(acc, {angle, distance, x, y}) => {
|
|
if (distance > acc.distance) return {angle, distance, x, y};
|
|
return acc;
|
|
},
|
|
{angle: 0, distance: 0, x: 0, y: 0}
|
|
);
|
|
|
|
const oppositeAngle = angle >= 180 ? angle - 180 : angle + 180;
|
|
const {x: x2, y: y2} = distances.reduce(
|
|
(acc, {angle, distance, x, y}) => {
|
|
const angleDif = getAnglesDif(angle, oppositeAngle);
|
|
const score = distance * getAngleModifier(angleDif);
|
|
if (score > acc.score) return {angle, score, x, y};
|
|
return acc;
|
|
},
|
|
{angle: 0, score: 0, x: 0, y: 0}
|
|
);
|
|
|
|
drawPath(lineGen([[x, y], state.pole, [x2, y2]])!, {stroke: "red", strokeWidth: 1});
|
|
|
|
const pathPoints: TPoints = [];
|
|
labelPaths.push([state.i, pathPoints]);
|
|
}
|
|
|
|
return labelPaths;
|
|
}
|
|
|
|
function getMaxDistance(stateId: number, point: {x: number; y: number}, dx: number, dy: number) {
|
|
let distance = INITIAL_DISTANCE;
|
|
|
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
const [x, y] = [point.x + distance * dx, point.y + distance * dy];
|
|
const cellId = findCell(x, y);
|
|
|
|
// const inside = cells.state[cellId] === stateId;
|
|
// drawPoint([x, y], {color: inside ? "blue" : "red", radius: 1});
|
|
|
|
if (cells.state[cellId] !== stateId) break;
|
|
distance += DISTANCE_STEP;
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
console.timeEnd("drawStateLabels");
|
|
}
|
|
|
|
// point offset to reduce label overlap with state borders
|
|
function getOffsetWidth(cellsNumber: number) {
|
|
if (cellsNumber < 80) return 0;
|
|
if (cellsNumber < 140) return 5;
|
|
if (cellsNumber < 200) return 15;
|
|
if (cellsNumber < 300) return 20;
|
|
if (cellsNumber < 500) return 25;
|
|
return 30;
|
|
}
|
|
|
|
// difference between two angles in range [0, 180]
|
|
function getAnglesDif(angle1: number, angle2: number) {
|
|
return 180 - Math.abs(Math.abs(angle1 - angle2) - 180);
|
|
}
|
|
|
|
// score multiplier based on angle difference betwee left and right sides
|
|
function getAngleModifier(angleDif: number) {
|
|
if (angleDif === 0) return 1;
|
|
if (angleDif <= 15) return 0.95;
|
|
if (angleDif <= 30) return 0.9;
|
|
if (angleDif <= 45) return 0.6;
|
|
if (angleDif <= 60) return 0.3;
|
|
if (angleDif <= 90) return 0.1;
|
|
return 0; // >90
|
|
}
|
|
|
|
function precalculateAngles(step: number) {
|
|
const RAD = Math.PI / 180;
|
|
const angles = [];
|
|
|
|
for (let angle = 0; angle < 360; angle += step) {
|
|
const x = Math.cos(angle * RAD);
|
|
const y = Math.sin(angle * RAD);
|
|
const angleDif = 90 - Math.abs((angle % 180) - 90);
|
|
const modifier = 1 - angleDif / 120; // [0.25, 1]
|
|
angles.push({angle, modifier, x, y});
|
|
}
|
|
|
|
return angles;
|
|
}
|