mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
refactor: render provinces
This commit is contained in:
parent
4dc5648310
commit
b2ab699843
12 changed files with 136 additions and 127 deletions
|
|
@ -169,6 +169,7 @@ a {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#provincesBody,
|
||||||
#statesBody {
|
#statesBody {
|
||||||
stroke-width: 3;
|
stroke-width: 3;
|
||||||
}
|
}
|
||||||
|
|
@ -179,10 +180,6 @@ a {
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
#provincesBody {
|
|
||||||
stroke-width: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
#statesBody,
|
#statesBody,
|
||||||
#provincesBody,
|
#provincesBody,
|
||||||
#relig,
|
#relig,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import {getProvincesVertices} from "./drawProvinces";
|
|
||||||
import {minmax, rn} from "utils/numberUtils";
|
import {minmax, rn} from "utils/numberUtils";
|
||||||
import {byId} from "utils/shorthands";
|
import {byId} from "utils/shorthands";
|
||||||
|
|
||||||
|
|
@ -42,7 +41,7 @@ export function drawEmblems() {
|
||||||
|
|
||||||
const sizeProvinces = getProvinceEmblemsSize();
|
const sizeProvinces = getProvinceEmblemsSize();
|
||||||
const provinceCOAs = validProvinces.map(province => {
|
const provinceCOAs = validProvinces.map(province => {
|
||||||
if (!province.pole) getProvincesVertices();
|
if (!province.pole) throw "Pole is not defined";
|
||||||
const [x, y] = province.pole || pack.cells.p[province.center];
|
const [x, y] = province.pole || pack.cells.p[province.center];
|
||||||
const size = province.coaSize || 1;
|
const size = province.coaSize || 1;
|
||||||
const shift = (sizeProvinces * size) / 2;
|
const shift = (sizeProvinces * size) / 2;
|
||||||
|
|
|
||||||
|
|
@ -1,122 +1,45 @@
|
||||||
import polylabel from "polylabel";
|
import {pick} from "utils/functionUtils";
|
||||||
|
import {byId} from "utils/shorthands";
|
||||||
|
import {isProvince} from "utils/typeUtils";
|
||||||
|
import {getPaths} from "./utils/getVertexPaths";
|
||||||
|
|
||||||
export function drawProvinces() {
|
export function drawProvinces() {
|
||||||
const labelsOn = provs.attr("data-labels") == 1;
|
/* global */ const {cells, vertices, features, provinces} = pack;
|
||||||
provs.selectAll("*").remove();
|
|
||||||
|
|
||||||
const provinces = pack.provinces;
|
const paths = getPaths({
|
||||||
const {body, gap} = getProvincesVertices();
|
getType: (cellId: number) => cells.province[cellId],
|
||||||
|
cells: pick(cells, "c", "v", "b", "h", "f"),
|
||||||
const g = provs.append("g").attr("id", "provincesBody");
|
vertices,
|
||||||
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
|
features,
|
||||||
g.selectAll("path")
|
options: {fill: true, waterGap: true, halo: false}
|
||||||
.data(bodyData)
|
|
||||||
.enter()
|
|
||||||
.append("path")
|
|
||||||
.attr("d", d => d[0])
|
|
||||||
.attr("fill", d => d[2])
|
|
||||||
.attr("stroke", "none")
|
|
||||||
.attr("id", d => "province" + d[1]);
|
|
||||||
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
|
|
||||||
g.selectAll(".path")
|
|
||||||
.data(gapData)
|
|
||||||
.enter()
|
|
||||||
.append("path")
|
|
||||||
.attr("d", d => d[0])
|
|
||||||
.attr("fill", "none")
|
|
||||||
.attr("stroke", d => d[2])
|
|
||||||
.attr("id", d => "province-gap" + d[1]);
|
|
||||||
|
|
||||||
const labels = provs.append("g").attr("id", "provinceLabels");
|
|
||||||
labels.style("display", `${labelsOn ? "block" : "none"}`);
|
|
||||||
const labelData = provinces.filter(p => p.i && !p.removed && p.pole);
|
|
||||||
labels
|
|
||||||
.selectAll(".path")
|
|
||||||
.data(labelData)
|
|
||||||
.enter()
|
|
||||||
.append("text")
|
|
||||||
.attr("x", d => d.pole[0])
|
|
||||||
.attr("y", d => d.pole[1])
|
|
||||||
.attr("id", d => "provinceLabel" + d.i)
|
|
||||||
.text(d => d.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProvincesVertices() {
|
|
||||||
const cells = pack.cells,
|
|
||||||
vertices = pack.vertices,
|
|
||||||
provinces = pack.provinces,
|
|
||||||
n = cells.i.length;
|
|
||||||
const used = new Uint8Array(cells.i.length);
|
|
||||||
const vArray = new Array(provinces.length); // store vertices array
|
|
||||||
const body = new Array(provinces.length).fill(""); // store path around each province
|
|
||||||
const gap = new Array(provinces.length).fill(""); // store path along water for each province to fill the gaps
|
|
||||||
|
|
||||||
for (const i of cells.i) {
|
|
||||||
if (!cells.province[i] || used[i]) continue;
|
|
||||||
const p = cells.province[i];
|
|
||||||
const onborder = cells.c[i].some(n => cells.province[n] !== p);
|
|
||||||
if (!onborder) continue;
|
|
||||||
|
|
||||||
const borderWith = cells.c[i].map(c => cells.province[c]).find(n => n !== p);
|
|
||||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === borderWith));
|
|
||||||
const chain = connectVertices(vertex, p, borderWith);
|
|
||||||
if (chain.length < 3) continue;
|
|
||||||
const points = chain.map(v => vertices.p[v[0]]);
|
|
||||||
if (!vArray[p]) vArray[p] = [];
|
|
||||||
vArray[p].push(points);
|
|
||||||
body[p] += "M" + points.join("L");
|
|
||||||
gap[p] +=
|
|
||||||
"M" +
|
|
||||||
vertices.p[chain[0][0]] +
|
|
||||||
chain.reduce(
|
|
||||||
(r, v, i, d) =>
|
|
||||||
!i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + "M" + vertices.p[v[0]] : r,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// find province visual center
|
|
||||||
vArray.forEach((ar, i) => {
|
|
||||||
const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
|
|
||||||
provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {body, gap};
|
const getColor = (i: string) => (provinces[Number(i)] as IProvince).color;
|
||||||
|
|
||||||
// connect vertices to chain
|
const getLabels = () => {
|
||||||
function connectVertices(start, t, province) {
|
const renderLabels = byId("provs")!.getAttribute("data-labels") === "1";
|
||||||
const chain = []; // vertices chain to form a path
|
if (!renderLabels) return [];
|
||||||
let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.province[c] !== t);
|
|
||||||
function check(i) {
|
|
||||||
province = cells.province[i];
|
|
||||||
land = cells.h[i] >= 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
|
return provinces.filter(isProvince).map(({i, pole: [x, y], name}) => {
|
||||||
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
|
return `<text x="${x}" y="${y}" id="provinceLabel${i}">${name}</text>`;
|
||||||
chain.push([current, province, land]); // add current vertex to sequence
|
});
|
||||||
const c = vertices.c[current]; // cells adjacent to vertex
|
};
|
||||||
c.filter(c => cells.province[c] === t).forEach(c => (used[c] = 1));
|
|
||||||
const c0 = c[0] >= n || cells.province[c[0]] !== t;
|
const htmlPaths = paths.map(([index, {fill, waterGap}]) => {
|
||||||
const c1 = c[1] >= n || cells.province[c[1]] !== t;
|
const color = getColor(index);
|
||||||
const c2 = c[2] >= n || cells.province[c[2]] !== t;
|
|
||||||
const v = vertices.v[current]; // neighboring vertices
|
return /* html */ `
|
||||||
if (v[0] !== prev && c0 !== c1) {
|
<path d="${waterGap}" fill="none" stroke="${color}" id="province-gap${index}" />
|
||||||
current = v[0];
|
<path d="${fill}" fill="${color}" stroke="none" id="province${index}" />
|
||||||
check(c0 ? c[0] : c[1]);
|
`;
|
||||||
} else if (v[1] !== prev && c1 !== c2) {
|
});
|
||||||
current = v[1];
|
|
||||||
check(c1 ? c[1] : c[2]);
|
byId("provs")!.innerHTML = /* html*/ `
|
||||||
} else if (v[2] !== prev && c0 !== c2) {
|
<g id="provincesBody">
|
||||||
current = v[2];
|
${htmlPaths.join("")}
|
||||||
check(c2 ? c[2] : c[0]);
|
</g>
|
||||||
}
|
<g id="provinceLabels">
|
||||||
if (current === chain[chain.length - 1][0]) {
|
${getLabels().join("")}
|
||||||
ERROR && console.error("Next vertex is not found");
|
</g>
|
||||||
break;
|
`;
|
||||||
}
|
|
||||||
}
|
|
||||||
chain.push([start, province, land]); // add starting vertex to sequence to close the path
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,9 +143,10 @@ export function createPack(grid: IGrid): IPack {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, mergedFeatures, {
|
const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, mergedFeatures, vertices, {
|
||||||
i: cells.i,
|
i: cells.i,
|
||||||
c: cells.c,
|
c: cells.c,
|
||||||
|
v: cells.v,
|
||||||
h: heights,
|
h: heights,
|
||||||
t: distanceField,
|
t: distanceField,
|
||||||
f: featureIds,
|
f: featureIds,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import {brighter, getMixedColor} from "utils/colorUtils";
|
||||||
import {gauss, P, rw} from "utils/probabilityUtils";
|
import {gauss, P, rw} from "utils/probabilityUtils";
|
||||||
import {isBurg, isState} from "utils/typeUtils";
|
import {isBurg, isState} from "utils/typeUtils";
|
||||||
import {provinceForms} from "./config";
|
import {provinceForms} from "./config";
|
||||||
import {generateProvinceName, generateProvinceEmblem} from "./utils";
|
|
||||||
|
|
||||||
const {COA, Names} = window;
|
const {COA, Names} = window;
|
||||||
|
|
||||||
|
|
@ -37,7 +36,7 @@ export function generateCoreProvinces(states: TStates, burgs: TBurgs, cultures:
|
||||||
const color = brighter(getMixedColor(state.color, 0.2), 0.3);
|
const color = brighter(getMixedColor(state.color, 0.2), 0.3);
|
||||||
const coa = generateEmblem(nameByBurg, burgEmblem, type, cultures, cultureId, state);
|
const coa = generateEmblem(nameByBurg, burgEmblem, type, cultures, cultureId, state);
|
||||||
|
|
||||||
provinces.push({i: provinces.length, name, formName, center, burg, state: state.i, fullName, color, coa});
|
provinces.push({i: provinces.length + 1, name, formName, center, burg, state: state.i, fullName, color, coa});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@ import {getInputNumber} from "utils/nodeUtils";
|
||||||
import {expandProvinces} from "./expandProvinces";
|
import {expandProvinces} from "./expandProvinces";
|
||||||
import {generateCoreProvinces} from "./generateCoreProvinces";
|
import {generateCoreProvinces} from "./generateCoreProvinces";
|
||||||
import {generateWildProvinces} from "./generateWildProvinces";
|
import {generateWildProvinces} from "./generateWildProvinces";
|
||||||
|
import {specifyProvinces} from "./specifyProvinces";
|
||||||
|
|
||||||
export function generateProvinces(
|
export function generateProvinces(
|
||||||
states: TStates,
|
states: TStates,
|
||||||
burgs: TBurgs,
|
burgs: TBurgs,
|
||||||
cultures: TCultures,
|
cultures: TCultures,
|
||||||
features: TPackFeatures,
|
features: TPackFeatures,
|
||||||
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "f" | "culture" | "state" | "burg">
|
vertices: IGraphVertices,
|
||||||
|
cells: Pick<IPack["cells"], "i" | "c" | "v" | "h" | "t" | "f" | "culture" | "state" | "burg">
|
||||||
): {provinceIds: Uint16Array; provinces: TProvinces} {
|
): {provinceIds: Uint16Array; provinces: TProvinces} {
|
||||||
TIME && console.time("generateProvinces");
|
TIME && console.time("generateProvinces");
|
||||||
|
|
||||||
|
|
@ -30,7 +32,7 @@ export function generateProvinces(
|
||||||
cells
|
cells
|
||||||
}); // mutates provinceIds
|
}); // mutates provinceIds
|
||||||
|
|
||||||
const provinces: TProvinces = [0, ...coreProvinces, ...wildProvinces];
|
const provinces = specifyProvinces(provinceIds, coreProvinces, wildProvinces, vertices, cells.c, cells.v);
|
||||||
|
|
||||||
TIME && console.timeEnd("generateProvinces");
|
TIME && console.timeEnd("generateProvinces");
|
||||||
return {provinceIds, provinces};
|
return {provinceIds, provinces};
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export function generateWildProvinces({
|
||||||
|
|
||||||
let noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === state.i);
|
let noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === state.i);
|
||||||
while (noProvinceCellsInState.length) {
|
while (noProvinceCellsInState.length) {
|
||||||
const provinceId = coreProvinces.length + wildProvinces.length;
|
const provinceId = coreProvinces.length + wildProvinces.length + 1;
|
||||||
const burgCell = noProvinceCellsInState.find(i => cells.burg[i]);
|
const burgCell = noProvinceCellsInState.find(i => cells.burg[i]);
|
||||||
const center = burgCell || noProvinceCellsInState[0];
|
const center = burgCell || noProvinceCellsInState[0];
|
||||||
const cultureId = cells.culture[center];
|
const cultureId = cells.culture[center];
|
||||||
|
|
|
||||||
20
src/scripts/generation/pack/provinces/specifyProvinces.ts
Normal file
20
src/scripts/generation/pack/provinces/specifyProvinces.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import {getPolesOfInaccessibility} from "scripts/getPolesOfInaccessibility";
|
||||||
|
|
||||||
|
export function specifyProvinces(
|
||||||
|
provinceIds: Uint16Array,
|
||||||
|
coreProvinces: IProvince[],
|
||||||
|
wildProvinces: IProvince[],
|
||||||
|
vertices: IGraphVertices,
|
||||||
|
cellNeighbors: number[][],
|
||||||
|
cellVertices: number[][]
|
||||||
|
): TProvinces {
|
||||||
|
const getType = (cellId: number) => provinceIds[cellId];
|
||||||
|
const poles = getPolesOfInaccessibility({vertices, getType, cellNeighbors, cellVertices});
|
||||||
|
|
||||||
|
const provinces = [...coreProvinces, ...wildProvinces].map(province => {
|
||||||
|
const pole = poles[province.i];
|
||||||
|
return {...province, pole};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [0, ...provinces];
|
||||||
|
}
|
||||||
64
src/scripts/getPolesOfInaccessibility.ts
Normal file
64
src/scripts/getPolesOfInaccessibility.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import polylabel from "polylabel";
|
||||||
|
|
||||||
|
import {TIME} from "config/logging";
|
||||||
|
import {connectVertices} from "./connectVertices";
|
||||||
|
|
||||||
|
interface IGetPolesProps {
|
||||||
|
vertices: IGraphVertices;
|
||||||
|
cellNeighbors: number[][];
|
||||||
|
cellVertices: number[][];
|
||||||
|
getType: (cellId: number) => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPolesOfInaccessibility(props: IGetPolesProps) {
|
||||||
|
TIME && console.time("getPolesOfInaccessibility");
|
||||||
|
const multiPolygons = getMultiPolygons(props);
|
||||||
|
|
||||||
|
const poles: Dict<TPoint> = Object.fromEntries(
|
||||||
|
Object.entries(multiPolygons).map(([id, multiPolygon]) => {
|
||||||
|
const [x, y] = polylabel(multiPolygon, 20);
|
||||||
|
return [id, [x, y]];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
TIME && console.timeEnd("getPolesOfInaccessibility");
|
||||||
|
return poles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMultiPolygons({vertices, getType, cellNeighbors, cellVertices}: IGetPolesProps) {
|
||||||
|
const multiPolygons: Dict<number[][][]> = {};
|
||||||
|
|
||||||
|
const checkedCells = new Uint8Array(cellNeighbors.length);
|
||||||
|
const addToChecked = (cellId: number) => {
|
||||||
|
checkedCells[cellId] = 1;
|
||||||
|
};
|
||||||
|
const isChecked = (cellId: number) => checkedCells[cellId] === 1;
|
||||||
|
|
||||||
|
for (let cellId = 0; cellId < cellNeighbors.length; cellId++) {
|
||||||
|
if (isChecked(cellId) || getType(cellId) === 0) continue;
|
||||||
|
addToChecked(cellId);
|
||||||
|
|
||||||
|
const type = getType(cellId);
|
||||||
|
const ofSameType = (cellId: number) => getType(cellId) === type;
|
||||||
|
const ofDifferentType = (cellId: number) => getType(cellId) !== type;
|
||||||
|
|
||||||
|
const onborderCell = cellNeighbors[cellId].find(ofDifferentType);
|
||||||
|
if (onborderCell === undefined) continue;
|
||||||
|
|
||||||
|
const startingVertex = cellVertices[cellId].find(v => vertices.c[v].some(ofDifferentType));
|
||||||
|
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||||
|
|
||||||
|
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||||
|
if (vertexChain.length < 3) continue;
|
||||||
|
|
||||||
|
addPolygon(type, vertexChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiPolygons;
|
||||||
|
|
||||||
|
function addPolygon(id: number, vertexChain: number[]) {
|
||||||
|
if (!multiPolygons[id]) multiPolygons[id] = [];
|
||||||
|
const polygon = vertexChain.map(vertex => vertices.p[vertex]);
|
||||||
|
multiPolygons[id].push(polygon);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/types/pack/provinces.d.ts
vendored
1
src/types/pack/provinces.d.ts
vendored
|
|
@ -7,6 +7,7 @@ interface IProvince {
|
||||||
color: Hex | CssUrls;
|
color: Hex | CssUrls;
|
||||||
state: number;
|
state: number;
|
||||||
center: number;
|
center: number;
|
||||||
|
pole: TPoint;
|
||||||
coa: ICoa | string;
|
coa: ICoa | string;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
src/types/pack/states.d.ts
vendored
2
src/types/pack/states.d.ts
vendored
|
|
@ -10,8 +10,8 @@ interface IState {
|
||||||
form: TStateForm;
|
form: TStateForm;
|
||||||
formName: string;
|
formName: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
pole: TPoint;
|
||||||
coa: ICoa | string;
|
coa: ICoa | string;
|
||||||
// pole: TPoint ?
|
|
||||||
area: number;
|
area: number;
|
||||||
cells: number;
|
cells: number;
|
||||||
burgs: number;
|
burgs: number;
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,6 @@ export const isBurg = (burg: TNoBurg | IBurg): burg is IBurg => burg.i !== 0 &&
|
||||||
|
|
||||||
export const isReligion = (religion: TNoReligion | IReligion): religion is IReligion =>
|
export const isReligion = (religion: TNoReligion | IReligion): religion is IReligion =>
|
||||||
religion.i !== 0 && !(religion as IReligion).removed;
|
religion.i !== 0 && !(religion as IReligion).removed;
|
||||||
|
|
||||||
|
export const isProvince = (province: TNoProvince | IProvince): province is IProvince =>
|
||||||
|
province !== 0 && !(province as IProvince).removed;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue