diff --git a/index.html b/index.html index 4806603e..8906d880 100644 --- a/index.html +++ b/index.html @@ -2604,8 +2604,8 @@
-
Avarage depth:
- +
Average depth:
+
diff --git a/src/dialogs/dialogs/lake-editor.js b/src/dialogs/dialogs/lake-editor.js index 71be417f..76961958 100644 --- a/src/dialogs/dialogs/lake-editor.js +++ b/src/dialogs/dialogs/lake-editor.js @@ -65,7 +65,7 @@ export function open({el}) { const heights = lakeCells.map(i => cells.h[i]); byId("lakeElevation").value = getHeight(l.height); - byId("lakeAvarageDepth").value = getHeight(d3.mean(heights), true); + byId("lakeAverageDepth").value = getHeight(d3.mean(heights), true); byId("lakeMaxDepth").value = getHeight(d3.min(heights), true); byId("lakeFlux").value = l.flux; diff --git a/src/layers/renderers/drawLabels.ts b/src/layers/renderers/drawLabels.ts index bcaecec2..0b6a8317 100644 --- a/src/layers/renderers/drawLabels.ts +++ b/src/layers/renderers/drawLabels.ts @@ -1,20 +1,15 @@ import * as d3 from "d3"; -import Delaunator from "delaunator"; -import FlatQueue from "flatqueue"; -import {simplify} from "scripts/simplify"; -import {Voronoi} from "modules/voronoi"; -import {MIN_LAND_HEIGHT} from "config/generation"; import {findCell} from "utils/graphUtils"; import {isState} from "utils/typeUtils"; -import {drawPath, drawPoint, drawPolyline} from "utils/debugUtils"; +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); + // drawBurgLabels(burgs); // TODO: draw other labels window.Zoom.invoke(); @@ -62,166 +57,140 @@ function drawBurgLabels(burgs: TBurgs) { } 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(); - console.log(labelPaths); function getLabelPaths() { const labelPaths: [number, TPoints][] = []; - const MIN_HULL_SIZE = 20; const lineGen = d3.line().curve(d3.curveBundle.beta(1)); for (const state of states) { if (!isState(state)) continue; - const used: Dict = {}; // mutable - const visualCenter = findCell(...state.pole); - const startingCell = cells.state[visualCenter] === state.i ? visualCenter : state.center; - const hull = getHull(startingCell, state.i, state.cells, used); - const points = [...hull].map(vertex => vertices.p[vertex]); + const offset = getOffsetWidth(state.cells); + const [x0, y0] = state.pole; - const delaunay = Delaunator.from(points); - const voronoi = new Voronoi(delaunay, points, points.length); - const chain = connectVertices(voronoi.vertices, state.pole, used); - - drawPoint(state.pole, {color: "blue", radius: 1}); - - if (state.i === 1) { - points.forEach(point => { - drawPoint(point, {color: "red", radius: 0.5}); - }); - } - - const pathPoints = simplify( - chain.map(i => voronoi.vertices.p[i]), - 30 + 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}]; + }) ); - drawPath(lineGen(pathPoints)!, {stroke: "red", strokeWidth: 0.5}); + 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 getHull(start: number, stateId: number, stateCells: number, used: Dict) { - const maxPassableLakeSize = stateCells / 10; - const queue = [start]; - - const hull = new Set(); - const addToHull = (cellId: number, index: number) => { - const vertex = cells.v[cellId][index]; - if (vertex) hull.add(vertex); - }; - - while (queue.length) { - const cellId = queue.pop()!; - - cells.c[cellId].forEach((neibCellId, index) => { - if (used[neibCellId]) return; - used[neibCellId] = true; - - if (isHullEdge(neibCellId)) return addToHull(cellId, index); - return queue.push(neibCellId); - }); - } - - return hull; - - function isHullEdge(cellId: number) { - if (cells.b[cellId]) return true; - - if (cells.h[cellId] < MIN_LAND_HEIGHT) { - const feature = features[cells.f[cellId]]; - if (!feature || feature.type !== "lake") return true; - if (feature.cells > maxPassableLakeSize) return true; - return false; - } - - if (cells.state[cellId] !== stateId) return true; - - if (hull.size > MIN_HULL_SIZE) { - // stop on narrow passages - const sameStateNeibs = cells.c[cellId].filter(c => cells.state[c] === stateId); - if (sameStateNeibs.length < 3) return true; - } - - return false; - } - } - - function connectVertices(vertices: Voronoi["vertices"], pole: TPoint, used: Dict) { - // check if vertex is inside the area - const inside = vertices.p.map(([x, y]) => { - if (x <= 0 || y <= 0 || x >= graphWidth || y >= graphHeight) return false; // out of the screen - return used[findCell(x, y)]; - }); - - const innerVertices = d3.range(vertices.p.length).filter(i => inside[i]); - if (innerVertices.length < 2) return [0]; - - const horyzontalShift = getHoryzontalShift(vertices.p.length); - const {right: start, left: end} = getEdgeVertices(innerVertices, vertices.p, pole, horyzontalShift); - - // connect leftmost and rightmost vertices with shortest path - const cost: number[] = []; - const from: number[] = []; - const queue = new FlatQueue(); - queue.push(start, 0); - - while (queue.length) { - const priority = queue.peekValue()!; - const next = queue.pop()!; - - if (next === end) break; - - for (const neibVertex of vertices.v[next]) { - if (neibVertex === -1) continue; - - const totalCost = priority + (inside[neibVertex] ? 1 : 100); - if (from[neibVertex] || totalCost >= cost[neibVertex]) continue; - - cost[neibVertex] = totalCost; - from[neibVertex] = next; - queue.push(neibVertex, totalCost); - } - } - - // restore path - const chain = [end]; - let cur = end; - while (cur !== start) { - cur = from[cur]; - if (inside[cur]) chain.push(cur); - } - return chain; - } - - function getHoryzontalShift(verticesNumber: number) { - console.log({verticesNumber}); - return 0; - if (verticesNumber < 100) return 1; - if (verticesNumber < 200) return 0.3; - if (verticesNumber < 300) return 0.1; - return 0; - } - - function getEdgeVertices(innerVertices: number[], points: TPoints, pole: TPoint, horyzontalShift: number) { - let leftmost = {value: Infinity, vertex: innerVertices.at(0)!}; - let rightmost = {value: -Infinity, vertex: innerVertices.at(-1)!}; - - for (const vertex of innerVertices) { - const [x, y] = points[vertex]; - const valueX = x - pole[0]; - const valueY = Math.abs(y - pole[1]) * horyzontalShift; - - if (valueX + valueY < leftmost.value) leftmost = {value: valueX + valueY, vertex}; - if (valueX - valueY > rightmost.value) rightmost = {value: valueX - valueY, vertex}; - } - - return {left: leftmost.vertex, right: rightmost.vertex}; - } } + + 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; } diff --git a/src/modules/burgs-and-states.js b/src/modules/burgs-and-states.js index 190322ea..a65061da 100644 --- a/src/modules/burgs-and-states.js +++ b/src/modules/burgs-and-states.js @@ -833,7 +833,7 @@ window.BurgsAndStates = (function () { valid.forEach(s => (s.diplomacy = new Array(states.length).fill("x"))); // clear all relationships if (valid.length < 2) return; // no states to renerate relations with - const areaMean = d3.mean(valid.map(s => s.area)); // avarage state area + const areaMean = d3.mean(valid.map(s => s.area)); // average state area // generic relations for (let f = 1; f < states.length; f++) { diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 5338eb5e..65fb65dd 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -67,8 +67,8 @@ async function generate(options?: IGenerationOptions) { // renderLayer("heightmap"); // renderLayer("rivers"); // renderLayer("biomes"); - renderLayer("burgs"); - renderLayer("routes"); + // renderLayer("burgs"); + // renderLayer("routes"); renderLayer("states"); renderLayer("labels"); diff --git a/src/scripts/getPolesOfInaccessibility.ts b/src/scripts/getPolesOfInaccessibility.ts index e7e7a414..e95500b0 100644 --- a/src/scripts/getPolesOfInaccessibility.ts +++ b/src/scripts/getPolesOfInaccessibility.ts @@ -15,7 +15,6 @@ export function getPolesOfInaccessibility(props: IGetPolesProps) { TIME && console.time("getPolesOfInaccessibility"); const multiPolygons = getMultiPolygons(props); const sortByLength = (a: unknown[], b: unknown[]) => b.length - a.length; - console.log(multiPolygons); const poles: Dict = Object.fromEntries( Object.entries(multiPolygons).map(([id, multiPolygon]) => {