From 19d7f239c1854f23b7056574e9e45362e4a878a1 Mon Sep 17 00:00:00 2001 From: max Date: Sat, 23 Jul 2022 15:02:25 +0300 Subject: [PATCH] fix: skip coastline out points wip --- src/layers/renderers/drawCoastline.ts | 30 ++++- .../curves/unused-basisFramedClosed.js | 107 ++++++++++++++++ src/scripts/events/index.ts | 7 +- src/scripts/generation/generation.ts | 1 - src/scripts/generation/pack/lakes.ts | 2 +- src/scripts/generation/pack/rivers.ts | 115 ++++++++++-------- src/utils/arrayUtils.ts | 21 ++++ src/utils/debugUtils.ts | 17 +++ src/utils/lineUtils.ts | 26 ++++ 9 files changed, 262 insertions(+), 64 deletions(-) create mode 100644 src/scripts/curves/unused-basisFramedClosed.js create mode 100644 src/utils/debugUtils.ts diff --git a/src/layers/renderers/drawCoastline.ts b/src/layers/renderers/drawCoastline.ts index 80169c79..f853fe15 100644 --- a/src/layers/renderers/drawCoastline.ts +++ b/src/layers/renderers/drawCoastline.ts @@ -1,7 +1,7 @@ import * as d3 from "d3"; import {simplify} from "scripts/simplify"; -import {clipPoly} from "utils/lineUtils"; +import {filterOutOfCanvasPoints} from "utils/lineUtils"; import {round} from "utils/stringUtils"; export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures) { @@ -11,14 +11,38 @@ export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures) const lineGen = d3.line().curve(d3.curveBasisClosed); const SIMPLIFICATION_TOLERANCE = 0.5; // px + // map edge rectangle + debug + .append("rect") + .attr("x", 0) + .attr("y", 0) + .attr("width", graphWidth) + .attr("height", graphHeight) + .attr("fill", "none") + .attr("stroke", "black") + .attr("stroke-width", 0.1); + for (const feature of features) { if (!feature) continue; if (feature.type === "ocean") continue; - const points = clipPoly(feature.vertices.map(vertex => vertices.p[vertex])); - const simplifiedPoints = simplify(points, SIMPLIFICATION_TOLERANCE); + const points = feature.vertices.map(vertex => vertices.p[vertex]); + const filteredPoints = filterOutOfCanvasPoints(points); + const simplifiedPoints = simplify(filteredPoints, SIMPLIFICATION_TOLERANCE); const path = round(lineGen(simplifiedPoints)!); + points.forEach(([x, y]) => { + debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "red"); + }); + + filteredPoints.forEach(([x, y]) => { + debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "blue"); + }); + + simplifiedPoints.forEach(([x, y]) => { + debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "green"); + }); + if (feature.type === "lake") { landMask .append("path") diff --git a/src/scripts/curves/unused-basisFramedClosed.js b/src/scripts/curves/unused-basisFramedClosed.js new file mode 100644 index 00000000..6cba6526 --- /dev/null +++ b/src/scripts/curves/unused-basisFramedClosed.js @@ -0,0 +1,107 @@ +// Custom fork of d3.curveBasisClosed +// The idea is to not interpolate (curve) line along the frame +// points = [[0, 833],[0, 0],[1007, 0],[1007, 833]] +// d3.line().curve(d3.curveBasisClosed)(points) // => M 167.8,138.8 C 335.7,0,671.3,0,839.2,138.8 C1007,277.7,1007,555.3,839.2,694.2C671.3,833,335.7,833,167.8,694.2 C0,555.3,0,277.7,167.8,138.8 +// d3.line().curve(d3.curveBasisFramedClosed)(points) // => M 0,833 L 0,0 L 1007,0 L 1007,833 + +const PRECISION = 2; +const round = number => Number(number.toFixed(PRECISION)); + +function noop() {} + +function point(that, x, y) { + that._context.bezierCurveTo( + round((2 * that._x0 + that._x1) / 3), + round((2 * that._y0 + that._y1) / 3), + round((that._x0 + 2 * that._x1) / 3), + round((that._y0 + 2 * that._y1) / 3), + round((that._x0 + 4 * that._x1 + x) / 6), + round((that._y0 + 4 * that._y1 + y) / 6) + ); +} + +function isEdge(x, y) { + return x <= 0 || y <= 0 || x >= graphWidth || y >= graphHeight; +} + +function BasisFramedClosed(context) { + this._context = context; +} + +BasisFramedClosed.prototype = { + areaStart: noop, + areaEnd: noop, + lineStart: function () { + this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = NaN; + this._point = 0; + }, + lineEnd: function () { + switch (this._point) { + case 1: { + this._context.moveTo(this._x2, this._y2); + this._context.closePath(); + break; + } + case 2: { + this._context.moveTo(round((this._x2 + 2 * this._x3) / 3), round((this._y2 + 2 * this._y3) / 3)); + this._context.lineTo(round((this._x3 + 2 * this._x2) / 3), round((this._y3 + 2 * this._y2) / 3)); + this._context.closePath(); + break; + } + case 3: { + this.point(this._x2, this._y2); + this.point(this._x3, this._y3); + this.point(this._x4, this._y4); + break; + } + } + }, + point: function (x, y) { + const edge = isEdge(x, y); + if (x <= 0) x -= 20; + if (y <= 0) y -= 20; + if (x >= graphWidth) x += 20; + if (y >= graphHeight) y += 20; + + switch (this._point) { + case 0: + this._point = 1; + this._x2 = x; + this._y2 = y; + break; + case 1: + this._point = 2; + this._x3 = x; + this._y3 = y; + break; + case 2: + this._point = 3; + this._x4 = x; + this._y4 = y; + + if (edge) { + this._context.moveTo(round((this._x1 + x) / 2), round((this._y1 + y) / 2)); + break; + } + + this._context.moveTo(round((this._x0 + 4 * this._x1 + x) / 6), round((this._y0 + 4 * this._y1 + y) / 6)); + break; + default: + if (edge) { + this._context.lineTo(x, y); + break; + } + + point(this, x, y); + break; + } + this._x0 = this._x1; + this._x1 = x; + this._y0 = this._y1; + this._y1 = y; + } +}; + +export function curveBasisFramedClosed(context) { + return new BasisFramedClosed(context); +} diff --git a/src/scripts/events/index.ts b/src/scripts/events/index.ts index 0ad46d8e..0d3d7c8c 100644 --- a/src/scripts/events/index.ts +++ b/src/scripts/events/index.ts @@ -10,11 +10,8 @@ import {clearLegend, dragLegendBox} from "modules/legend"; export function setDefaultEventHandlers() { window.Zoom.setZoomBehavior(); - viewbox - .style("cursor", "default") - .on(".drag", null) - .on("click", handleMapClick) - .on("touchmove mousemove", onMouseMove); + viewbox.style("cursor", "default").on(".drag", null).on("click", handleMapClick); + //.on("touchmove mousemove", onMouseMove); scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => openDialog("unitsEditor")); diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 00699cc0..259468e8 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -24,7 +24,6 @@ import {debounce} from "utils/functionUtils"; import {rn} from "utils/numberUtils"; import {generateSeed} from "utils/probabilityUtils"; import {byId} from "utils/shorthands"; -import {showStatistics} from "../statistics"; import {createGrid} from "./grid"; import {createPack} from "./pack/pack"; import {getInputValue, setInputValue} from "utils/nodeUtils"; diff --git a/src/scripts/generation/pack/lakes.ts b/src/scripts/generation/pack/lakes.ts index 333ea114..66b00738 100644 --- a/src/scripts/generation/pack/lakes.ts +++ b/src/scripts/generation/pack/lakes.ts @@ -15,7 +15,7 @@ export interface ILakeClimateData extends IPackFeatureLake { export const getClimateData = function ( lakes: IPackFeatureLake[], - heights: number[], + heights: Float32Array, drainableLakes: Dict, gridReference: IPack["cells"]["g"], precipitation: IGrid["cells"]["prec"], diff --git a/src/scripts/generation/pack/rivers.ts b/src/scripts/generation/pack/rivers.ts index 4d6132bd..77407122 100644 --- a/src/scripts/generation/pack/rivers.ts +++ b/src/scripts/generation/pack/rivers.ts @@ -1,6 +1,6 @@ import * as d3 from "d3"; -import {TIME, WARN} from "config/logging"; +import {INFO, TIME, WARN} from "config/logging"; import {rn} from "utils/numberUtils"; import {aleaPRNG} from "scripts/aleaPRNG"; import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation"; @@ -8,6 +8,7 @@ import {getInputNumber} from "utils/nodeUtils"; import {pick} from "utils/functionUtils"; import {byId} from "utils/shorthands"; import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes"; +import {drawArrow} from "utils/debugUtils"; const {Rivers} = window; const {LAND_COAST} = DISTANCE_FIELD; @@ -96,7 +97,7 @@ export function generateRivers( } lake.outlet = riverIds[lakeCell]; - flowDown(cellId, flux[lakeCell], lake.outlet); + flowDown(lakeCell, cellId, flux[lakeCell], lake.outlet); } if (lakesDrainingToCell.length && lakesDrainingToCell[0].outlet) { @@ -127,15 +128,6 @@ export function generateRivers( // cells is depressed if (currentCellHeights[cellId] <= currentCellHeights[min]) return; - debug - .append("line") - .attr("x1", cells.p[cellId][0]) - .attr("y1", cells.p[cellId][1]) - .attr("x2", cells.p[min][0]) - .attr("y2", cells.p[min][1]) - .attr("stroke", "#333") - .attr("stroke-width", 0.1); - if (flux[cellId] < MIN_FLUX_TO_FORM_RIVER) { // flux is too small to operate as a river if (currentCellHeights[min] >= MIN_LAND_HEIGHT) flux[min] += flux[cellId]; @@ -149,12 +141,14 @@ export function generateRivers( nextRiverId++; } - flowDown(min, flux[cellId], riverIds[cellId]); + flowDown(cellId, min, flux[cellId], riverIds[cellId]); }); return {flux, lakeData}; - function flowDown(toCell: number, fromFlux: number, riverId: number) { + function flowDown(fromCell: number, toCell: number, fromFlux: number, riverId: number) { + // drawArrow(cells.p[fromCell], cells.p[toCell]); + const toFlux = flux[toCell] - confluence[toCell]; const toRiver = riverIds[toCell]; @@ -262,7 +256,7 @@ export function generateRivers( return {r, conf, rivers}; } - function downcutRivers(heights: number[]) { + function downcutRivers(heights: Float32Array) { const MAX_DOWNCUT = 5; const MIN_HEIGHT_TO_DOWNCUT = 35; @@ -284,10 +278,10 @@ export function generateRivers( // add distance to water value to land cells to make map less depressed const applyDistanceField = ({h, c, t}: Pick) => { - return Array.from(h).map((height, index) => { - if (height < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return height; + return new Float32Array(h.length).map((_, index) => { + if (h[index] < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return h[index]; const mean = d3.mean(c[index].map(c => t[c])) || 0; - return height + t[index] / 100 + mean / 10000; + return h[index] + t[index] / 100 + mean / 10000; }); }; @@ -295,52 +289,55 @@ const applyDistanceField = ({h, c, t}: Pick) => const resolveDepressions = function ( cells: Pick, features: TPackFeatures, - heights: number[] -): [number[], Dict] { + initialCellHeights: Float32Array +): [Float32Array, Dict] { + TIME && console.time("resolveDepressions"); + const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput"); const checkLakeMaxIteration = MAX_INTERATIONS * 0.85; const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75; - const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput"); - const LAND_ELEVATION_INCREMENT = 0.1; const LAKE_ELEVATION_INCREMENT = 0.2; const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[]; lakes.sort((a, b) => a.height - b.height); // lowest lakes go first - const currentCellHeights = Array.from(heights); - const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height])); - const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i]; const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight)); + const getMinLandHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(i => currentCellHeights[i])); - const drainableLakes = checkLakesDrainability(); - - const landCells = cells.i.filter(i => heights[i] >= MIN_LAND_HEIGHT && !cells.b[i]); - landCells.sort((a, b) => heights[a] - heights[b]); // lowest cells go first + const landCells = cells.i.filter(i => initialCellHeights[i] >= MIN_LAND_HEIGHT && !cells.b[i]); + landCells.sort((a, b) => initialCellHeights[a] - initialCellHeights[b]); // lowest cells go first + const currentCellHeights = Float32Array.from(initialCellHeights); + const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height])); + const currentDrainableLakes = checkLakesDrainability(); const depressions: number[] = []; - for (let iteration = 0; iteration && depressions.at(-1) && iteration < MAX_INTERATIONS; iteration++) { + let bestDepressions = Infinity; + let bestCellHeights: typeof currentCellHeights | null = null; + let bestDrainableLakes: typeof currentDrainableLakes | null = null; + + for (let iteration = 0; depressions.at(-1) !== 0 && iteration < MAX_INTERATIONS; iteration++) { let depressionsLeft = 0; // elevate potentially drainable lakes if (iteration < checkLakeMaxIteration) { for (const lake of lakes) { - if (drainableLakes[lake.i] !== true) continue; + if (currentDrainableLakes[lake.i] !== true) continue; - const minShoreHeight = getMinHeight(lake.shoreline); - if (minShoreHeight >= MAX_HEIGHT || lake.height > minShoreHeight) continue; + const minShoreHeight = getMinLandHeight(lake.shoreline); + if (minShoreHeight >= MAX_HEIGHT || currentLakeHeights[lake.i] > minShoreHeight) continue; if (iteration > elevateLakeMaxIteration) { + // reset heights for (const shoreCellId of lake.shoreline) { - // reset heights - currentCellHeights[shoreCellId] = heights[shoreCellId]; - currentLakeHeights[lake.i] = lake.height; + currentCellHeights[shoreCellId] = initialCellHeights[shoreCellId]; } + currentLakeHeights[lake.i] = lake.height; - drainableLakes[lake.i] = false; + currentDrainableLakes[lake.i] = false; continue; } @@ -358,23 +355,36 @@ const resolveDepressions = function ( } depressions.push(depressionsLeft); - - // check depression resolving progress - if (depressions.length > 5) { - const depressionsInitial = depressions.at(0) || 0; - const depressiosRecently = depressions.at(-6) || 0; - - const isProgressingOverall = depressionsInitial < depressionsLeft; - if (!isProgressingOverall) return [heights, drainableLakes]; - - const isProgressingRecently = depressiosRecently < depressionsLeft; - if (!isProgressingRecently) return [currentCellHeights, drainableLakes]; + if (depressionsLeft < bestDepressions) { + bestDepressions = depressionsLeft; + bestCellHeights = Float32Array.from(currentCellHeights); + bestDrainableLakes = structuredClone(currentDrainableLakes); } } + TIME && console.timeEnd("resolveDepressions"); + + const depressionsLeft = depressions.at(-1); + if (depressionsLeft) { + if (bestCellHeights && bestDrainableLakes) { + WARN && + console.warn(`Cannot resolve all depressions. Depressions: ${depressions[0]}. Best result: ${bestDepressions}`); + return [bestCellHeights, bestDrainableLakes]; + } + + WARN && console.warn(`Cannot resolve depressions. Depressions: ${depressionsLeft}`); + return [initialCellHeights, {}]; + } + + INFO && + console.info(`ⓘ Resolved all depressions. Depressions: ${depressions[0]}. Interations: ${depressions.length}`); + return [currentCellHeights, currentDrainableLakes]; + // define lakes that potentially can be open (drained into another water body) function checkLakesDrainability() { const canBeDrained: Dict = {}; // all false by default + + const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput"); const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT; for (const lake of lakes) { @@ -385,7 +395,8 @@ const resolveDepressions = function ( canBeDrained[lake.i] = false; const minShoreHeight = getMinHeight(lake.shoreline); - const minHeightShoreCell = lake.shoreline.find(cellId => heights[cellId] === minShoreHeight) || lake.shoreline[0]; + const minHeightShoreCell = + lake.shoreline.find(cellId => initialCellHeights[cellId] === minShoreHeight) || lake.shoreline[0]; const queue = [minHeightShoreCell]; const checked = []; @@ -397,9 +408,9 @@ const resolveDepressions = function ( for (const neibCellId of cells.c[cellId]) { if (checked[neibCellId]) continue; - if (heights[neibCellId] >= breakableHeight) continue; + if (initialCellHeights[neibCellId] >= breakableHeight) continue; - if (heights[neibCellId] < MIN_LAND_HEIGHT) { + if (initialCellHeights[neibCellId] < MIN_LAND_HEIGHT) { const waterFeatureMet = features[cells.f[neibCellId]]; const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean"; const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake"; @@ -418,8 +429,4 @@ const resolveDepressions = function ( return canBeDrained; } - - depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`); - - return [currentCellHeights, drainableLakes]; }; diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts index 490617d5..a7874531 100644 --- a/src/utils/arrayUtils.ts +++ b/src/utils/arrayUtils.ts @@ -8,6 +8,27 @@ export function unique(array: T[]) { return [...new Set(array)]; } +export function sliceFragment(array: T[], index: number, zone: number) { + if (zone + 1 + zone >= array.length) { + return array.slice(); + } + + const start = index - zone; + const end = index + zone; + + if (start < 0) { + return array.slice(0, end).concat(array.slice(start)); + } + + if (end >= array.length) { + return array.slice(start).concat(array.slice(0, end % array.length)); + } + + return array.slice(start, end); +} + +window.sliceFragment = sliceFragment; + interface ICreateTypesArrayLength { maxValue: number; length: number; diff --git a/src/utils/debugUtils.ts b/src/utils/debugUtils.ts new file mode 100644 index 00000000..07ff89bb --- /dev/null +++ b/src/utils/debugUtils.ts @@ -0,0 +1,17 @@ +// utils used for debugging (not in PROD) only +export function drawArrow([x1, y1]: TPoint, [x2, y2]: TPoint, width = 1, color = "#444"): void { + const angle = Math.atan2(y2 - y1, x2 - x1); + const normal = angle + Math.PI / 2; + + const [xMid, yMid] = [(x1 + x2) / 2, (y1 + y2) / 2]; + + const [xLeft, yLeft] = [xMid + width * Math.cos(normal), yMid + width * Math.sin(normal)]; + const [xRight, yRight] = [xMid - width * Math.cos(normal), yMid - width * Math.sin(normal)]; + + debug + .append("path") + .attr("d", `M${x1},${y1} L${xMid},${yMid} ${xLeft},${yLeft} ${x2},${y2} ${xRight},${yRight} ${xMid},${yMid} Z`) + .attr("fill", color) + .attr("stroke", color) + .attr("stroke-width", width / 2); +} diff --git a/src/utils/lineUtils.ts b/src/utils/lineUtils.ts index d118e496..fc0362f8 100644 --- a/src/utils/lineUtils.ts +++ b/src/utils/lineUtils.ts @@ -52,3 +52,29 @@ export function getMiddlePoint(cell1: number, cell2: number) { return [x, y]; } + +function getOffCanvasSide([x, y]: TPoint) { + if (y <= 0) return "top"; + if (y >= graphHeight) return "bottom"; + if (x <= 0) return "left"; + if (x >= graphWidth) return "right"; + + return false; +} + +// remove intermediate out-of-canvas points from polyline +export function filterOutOfCanvasPoints(points: TPoints) { + const pointsOutSide = points.map(getOffCanvasSide); + const SAFE_ZONE = 3; + + const filterOutCanvasPoint = (i: number) => { + const pointSide = pointsOutSide[i]; + if (pointSide === false) return true; + if (pointsOutSide.slice(i - SAFE_ZONE, i + SAFE_ZONE).some(side => !side || side !== pointSide)) return true; + return false; + }; + + const result = points.filter((_, i) => filterOutCanvasPoint(i)); + + return result; +}