feat: draw borders

This commit is contained in:
Azgaar 2022-10-01 15:32:00 +03:00
parent a6d777aecb
commit 50edcb4e30
8 changed files with 182 additions and 111 deletions

View file

@ -26,11 +26,11 @@
"filter": null
},
"#provinceBorders": {
"opacity": 0.8,
"opacity": 0.6,
"stroke": "#56566d",
"stroke-width": 0.5,
"stroke-dasharray": "0 2",
"stroke-linecap": "round",
"stroke-width": 0.2,
"stroke-dasharray": "1 1",
"stroke-linecap": "butt",
"filter": null
},
"#cells": {

View file

@ -14,8 +14,6 @@ export function drawBiomes() {
options: {fill: true, waterGap: true, halo: false}
});
console.log(paths);
const htmlPaths = paths.map(([index, {fill, waterGap}]) => {
const color = colors[Number(index)];

View file

@ -1,102 +0,0 @@
export function drawBorders() {
borders.selectAll("path").remove();
const {cells, vertices} = pack;
const n = cells.i.length;
const sPath = [];
const pPath = [];
const sUsed = new Array(pack.states.length).fill("").map(_ => []);
const pUsed = new Array(pack.provinces.length).fill("").map(_ => []);
for (let i = 0; i < cells.i.length; i++) {
if (!cells.state[i]) continue;
const p = cells.province[i];
const s = cells.state[i];
// if cell is on province border
const provToCell = cells.c[i].find(
n => cells.state[n] === s && p > cells.province[n] && pUsed[p][n] !== cells.province[n]
);
if (provToCell) {
const provTo = cells.province[provToCell];
pUsed[p][provToCell] = provTo;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === provTo));
const chain = connectVertices(vertex, p, cells.province, provTo, pUsed);
if (chain.length > 1) {
pPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
i--;
continue;
}
}
// if cell is on state border
const stateToCell = cells.c[i].find(n => cells.h[n] >= 20 && s > cells.state[n] && sUsed[s][n] !== cells.state[n]);
if (stateToCell !== undefined) {
const stateTo = cells.state[stateToCell];
sUsed[s][stateToCell] = stateTo;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] >= 20 && cells.state[i] === stateTo));
const chain = connectVertices(vertex, s, cells.state, stateTo, sUsed);
if (chain.length > 1) {
sPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
i--;
continue;
}
}
}
stateBorders.append("path").attr("d", sPath.join(" "));
provinceBorders.append("path").attr("d", pPath.join(" "));
// connect vertices to chain
function connectVertices(current, f, array, t, used) {
let chain = [];
const checkCell = c => c >= n || array[c] !== f;
const checkVertex = v =>
vertices.c[v].some(c => array[c] === f) && vertices.c[v].some(c => array[c] === t && cells.h[c] >= 20);
// find starting vertex
for (let i = 0; i < 1000; i++) {
if (i === 999) ERROR && console.error("Find starting vertex: limit is reached", current, f, t);
const p = chain[chain.length - 2] || -1; // previous vertex
const v = vertices.v[current],
c = vertices.c[current];
const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
if (v0 + v1 + v2 === 1) break;
current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
if (current === chain[0]) break;
if (current === p) return [];
chain.push(current);
}
chain = [current]; // vertices chain to form a path
// find path
for (let i = 0; i < 1000; i++) {
if (i === 999) ERROR && console.error("Find path: limit is reached", current, f, t);
const p = chain[chain.length - 2] || -1; // previous vertex
const v = vertices.v[current],
c = vertices.c[current];
c.filter(c => array[c] === t).forEach(c => (used[f][c] = t));
const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
if (current === p) break;
if (current === chain[chain.length - 1]) break;
if (chain.length > 1 && v0 + v1 + v2 < 2) break;
chain.push(current);
if (current === chain[0]) break;
}
return chain;
}
}

View file

@ -0,0 +1,168 @@
import {MIN_LAND_HEIGHT} from "config/generation";
export function drawBorders() {
/* global */ const {cells, vertices} = pack;
const statePath: string[] = [];
const provincePath: string[] = [];
const checkedStates: Dict<boolean> = {};
const checkedProvinces: Dict<boolean> = {};
const isLand = (i: number) => cells.h[i] >= MIN_LAND_HEIGHT;
for (let cellId = 0; cellId < cells.i.length; cellId++) {
if (!cells.state[cellId]) continue;
const provinceId = cells.province[cellId];
const stateId = cells.state[cellId];
// bordering cell of another province
const provToCell =
provinceId &&
cells.c[cellId].find(neibId => {
const neibProvinceId = cells.province[neibId];
return (
neibProvinceId &&
provinceId > neibProvinceId &&
!checkedProvinces[`${provinceId}-${neibProvinceId}-${cellId}`] &&
cells.state[neibId] === stateId
);
});
if (provToCell !== undefined) {
const addToChecked = (cellId: number) =>
(checkedProvinces[`${provinceId}-${cells.province[provToCell]}-${cellId}`] = true);
const border = getBorder({
cells,
vertices,
type: "province",
fromCell: cellId,
toCell: provToCell,
addToChecked
});
if (border) {
provincePath.push(border);
cellId--; // check the same cell again
continue;
}
}
// if cell is on state border
const stateToCell = cells.c[cellId].find(neibId => {
const neibStateId = cells.state[neibId];
return isLand(neibId) && stateId > neibStateId && !checkedStates[`${stateId}-${neibStateId}-${cellId}`];
});
if (stateToCell !== undefined) {
const addToChecked = (cellId: number) =>
(checkedStates[`${stateId}-${cells.state[stateToCell]}-${cellId}`] = true);
const border = getBorder({
cells,
vertices,
type: "state",
fromCell: cellId,
toCell: stateToCell,
addToChecked
});
if (border) {
statePath.push(border);
cellId--; // check the same cell again
continue;
}
}
}
svg.select("#borders").selectAll("path").remove();
svg.select("#stateBorders").append("path").attr("d", statePath.join(" "));
svg.select("#provinceBorders").append("path").attr("d", provincePath.join(" "));
}
function getBorder({
cells,
vertices,
type,
fromCell,
toCell,
addToChecked
}: {
cells: IGraphCells & IPackCells;
vertices: IGraphVertices;
type: "state" | "province";
fromCell: number;
toCell: number;
addToChecked: (cellId: number) => void;
}) {
const getType = (cellId: number) => cells[type][cellId];
const isLand = (i: number) => cells.h[i] >= MIN_LAND_HEIGHT;
const n = cells.i.length;
const isTypeFrom = (cellId: number) => cellId < n && getType(cellId) === getType(fromCell);
const istypeTo = (cellId: number) => cellId < n && getType(cellId) === getType(toCell);
addToChecked(fromCell);
const startingVertex = cells.v[fromCell].find(v => vertices.c[v].some(i => isLand(i) && istypeTo(i)));
if (startingVertex === undefined) return null;
const checkVertex = (vertex: number) =>
vertices.c[vertex].some(isTypeFrom) && vertices.c[vertex].some(c => isLand(c) && istypeTo(c));
const chain = getVerticesLine({vertices, startingVertex, checkCell: isTypeFrom, checkVertex, addToChecked});
if (chain.length > 1) return "M" + chain.map(cellId => vertices.p[cellId]).join(" ");
return null;
}
const MAX_ITERATIONS = 50000;
// connect vertices to chain to form a border
function getVerticesLine({
vertices,
startingVertex,
checkCell,
checkVertex,
addToChecked
}: {
vertices: IGraphVertices;
startingVertex: number;
checkCell: (cellId: number) => boolean;
checkVertex: (vertex: number) => boolean;
addToChecked: (cellId: number) => void;
}) {
let chain: number[] = []; // vertices chain to form a path
let next = startingVertex;
for (let run = 0; run < 2; run++) {
// first run: from any vertex to a border edge
// second run: from found border edge to another edge
chain = [];
for (let i = 0; i < MAX_ITERATIONS; i++) {
const previous = chain.at(-1);
const current = next;
chain.push(current);
const neibCells = vertices.c[current];
neibCells.map(addToChecked);
const [c1, c2, c3] = neibCells.map(checkCell);
const [v1, v2, v3] = vertices.v[current].map(checkVertex);
const [vertex1, vertex2, vertex3] = vertices.v[current];
if (v1 && vertex1 !== previous && c1 !== c2) next = vertex1;
else if (v2 && vertex2 !== previous && c2 !== c3) next = vertex2;
else if (v3 && vertex3 !== previous && c1 !== c3) next = vertex3;
if (next === current || next === startingVertex) {
if (next === startingVertex) chain.push(startingVertex);
startingVertex = next;
break;
}
}
}
return chain;
}

View file

@ -2,7 +2,7 @@ import * as d3 from "d3";
export function defineSvg(width, height) {
// append svg layers (in default order)
svg = d3.select("#map");
svg = d3.select("#map"); // to be: the only global var
defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox");
scaleBar = svg.select("#scaleBar");

View file

@ -568,7 +568,7 @@ export function randomizeOptions() {
// 'Options' settings
if (randomize || !locked("template")) randomizeHeightmapTemplate();
if (randomize || !locked("regions")) regionsInput.value = regionsOutput.value = gauss(18, 5, 2, 30);
if (randomize || !locked("provinces")) provincesInput.value = provincesOutput.value = gauss(20, 10, 20, 100);
if (randomize || !locked("provinces")) provincesInput.value = provincesOutput.value = gauss(5, 15, 3, 100);
if (randomize || !locked("manors")) {
manorsInput.value = 1000;
manorsOutput.value = "auto";

View file

@ -74,7 +74,8 @@ async function generate(options?: IGenerationOptions) {
// renderLayer("burgs");
// renderLayer("routes");
renderLayer("states");
renderLayer("labels");
renderLayer("borders");
// renderLayer("labels");
// pack.cells.route.forEach((route, index) => {
// if (route === 2) drawPoint(pack.cells.p[index], {color: "black"});

View file

@ -60,3 +60,9 @@ function getTypedArray(maxValue: number) {
if (maxValue <= UINT32_MAX) return Uint32Array;
return Uint32Array;
}
type Nested<T> = (T | Nested<T>)[];
export function createNestedArray<T>(length: number, depth: number, value: boolean | number | string): Nested<T> {
if (depth === 0) return new Array(length).fill(value);
return new Array(length).fill(value).map(_ => createNestedArray(length, depth - 1, value));
}