From 50edcb4e3010c8e14325e05cfd0ee2b611b7b715 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 1 Oct 2022 15:32:00 +0300 Subject: [PATCH] feat: draw borders --- src/assets/styles/default.json | 8 +- src/layers/renderers/drawBiomes.ts | 2 - src/layers/renderers/drawBorders.js | 102 ---------------- src/layers/renderers/drawBorders.ts | 168 +++++++++++++++++++++++++++ src/modules/define-svg.js | 2 +- src/modules/ui/options.js | 2 +- src/scripts/generation/generation.ts | 3 +- src/utils/arrayUtils.ts | 6 + 8 files changed, 182 insertions(+), 111 deletions(-) delete mode 100644 src/layers/renderers/drawBorders.js create mode 100644 src/layers/renderers/drawBorders.ts diff --git a/src/assets/styles/default.json b/src/assets/styles/default.json index 775bdbd2..2424b654 100644 --- a/src/assets/styles/default.json +++ b/src/assets/styles/default.json @@ -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": { diff --git a/src/layers/renderers/drawBiomes.ts b/src/layers/renderers/drawBiomes.ts index 382f858f..5cfbc38c 100644 --- a/src/layers/renderers/drawBiomes.ts +++ b/src/layers/renderers/drawBiomes.ts @@ -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)]; diff --git a/src/layers/renderers/drawBorders.js b/src/layers/renderers/drawBorders.js deleted file mode 100644 index e2773827..00000000 --- a/src/layers/renderers/drawBorders.js +++ /dev/null @@ -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; - } -} diff --git a/src/layers/renderers/drawBorders.ts b/src/layers/renderers/drawBorders.ts new file mode 100644 index 00000000..89d0da62 --- /dev/null +++ b/src/layers/renderers/drawBorders.ts @@ -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 = {}; + const checkedProvinces: Dict = {}; + + 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; +} diff --git a/src/modules/define-svg.js b/src/modules/define-svg.js index 1e7acdef..1f2baa30 100644 --- a/src/modules/define-svg.js +++ b/src/modules/define-svg.js @@ -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"); diff --git a/src/modules/ui/options.js b/src/modules/ui/options.js index e3262bf0..466e0aa1 100644 --- a/src/modules/ui/options.js +++ b/src/modules/ui/options.js @@ -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"; diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 3ab3fa66..25b0f581 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -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"}); diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts index 29b3a68e..e3b3aa86 100644 --- a/src/utils/arrayUtils.ts +++ b/src/utils/arrayUtils.ts @@ -60,3 +60,9 @@ function getTypedArray(maxValue: number) { if (maxValue <= UINT32_MAX) return Uint32Array; return Uint32Array; } + +type Nested = (T | Nested)[]; +export function createNestedArray(length: number, depth: number, value: boolean | number | string): Nested { + if (depth === 0) return new Array(length).fill(value); + return new Array(length).fill(value).map(_ => createNestedArray(length, depth - 1, value)); +}