feat: zones - render zones as continuius line

This commit is contained in:
Azgaar 2024-08-30 16:45:51 +02:00
parent 492bcd9c9b
commit a63a60c0ea
5 changed files with 169 additions and 50 deletions

View file

@ -153,10 +153,6 @@ a {
fill-rule: evenodd; fill-rule: evenodd;
} }
#zones {
fill-rule: nonzero;
}
#coastline { #coastline {
fill: none; fill: none;
stroke-linejoin: round; stroke-linejoin: round;

View file

@ -7989,6 +7989,7 @@
<script src="utils/stringUtils.js?v=1.99.00"></script> <script src="utils/stringUtils.js?v=1.99.00"></script>
<script src="utils/languageUtils.js?v=1.99.00"></script> <script src="utils/languageUtils.js?v=1.99.00"></script>
<script src="utils/unitUtils.js?v=1.99.00"></script> <script src="utils/unitUtils.js?v=1.99.00"></script>
<script src="utils/pathUtils.js?v=1.100.00"></script>
<script defer src="utils/debugUtils.js?v=1.99.00"></script> <script defer src="utils/debugUtils.js?v=1.99.00"></script>
<script src="modules/voronoi.js"></script> <script src="modules/voronoi.js"></script>

View file

@ -1892,37 +1892,7 @@ function drawZones() {
} }
function drawZone({i, cells, type, color}) { function drawZone({i, cells, type, color}) {
// find a path connecting all cells of zone const path = getVertexPath(cells);
const path = getZonePath(cells);
if (!path) return;
function getZonePath(cells) {
const used = new Set();
const vertices = cells.map(c => pack.cells.v[c]).flat();
const points = vertices.map(v => pack.vertices.p[v]);
const boundary = getBoundaryPoints(points, used);
return boundary.length > 2 ? "M" + boundary.join("L") + "Z" : null;
}
function getBoundaryPoints(points, used) {
const boundary = [];
let currentPoint = points[0];
while (true) {
boundary.push(currentPoint);
used.add(currentPoint.toString());
let nextPoint = findNextPoint(currentPoint, points, used);
if (!nextPoint || nextPoint === boundary[0]) break;
currentPoint = nextPoint;
}
return boundary;
}
function findNextPoint(current, points, used) {
return points.find(p => !used.has(p.toString()) && Math.hypot(p[0] - current[0], p[1] - current[1]) < 20);
}
return `<path id="zone${i}" data-id="${i}" data-type="${type}" d="${path}" fill="${color}" />`; return `<path id="zone${i}" data-id="${i}" data-type="${type}" d="${path}" fill="${color}" />`;
} }

View file

@ -33,34 +33,31 @@ function editZones() {
byId("zonesRemove").on("click", e => e.target.classList.toggle("pressed")); byId("zonesRemove").on("click", e => e.target.classList.toggle("pressed"));
body.on("click", function (ev) { body.on("click", function (ev) {
const el = ev.target; const line = ev.target.closest("div.states");
const classList = el.classList; const zone = pack.zones.find(z => z.i === +line.dataset.id);
const zoneId = +(classList.contains("states") ? el.dataset.id : el.parentNode.dataset.id);
const zone = pack.zones.find(z => z.i === zoneId);
if (!zone) return; if (!zone) return;
if (customization) { if (customization) {
if (zone.hidden) return; if (zone.hidden) return;
body.querySelector("div.selected").classList.remove("selected"); body.querySelector("div.selected").classList.remove("selected");
el.classList.add("selected"); line.classList.add("selected");
return; return;
} }
if (el.closest("fill-box")) changeFill(el.getAttribute("fill"), zone); if (ev.target.closest("fill-box")) changeFill(ev.target.closest("fill-box").getAttribute("fill"), zone);
else if (classList.contains("zonePopulation")) changePopulation(zone); else if (ev.target.classList.contains("zonePopulation")) changePopulation(zone);
else if (classList.contains("icon-trash-empty")) zoneRemove(zone); else if (ev.target.classList.contains("icon-trash-empty")) zoneRemove(zone);
else if (classList.contains("icon-eye")) toggleVisibility(zone); else if (ev.target.classList.contains("icon-eye")) toggleVisibility(zone);
else if (classList.contains("icon-pin")) toggleFog(zone, classList); else if (ev.target.classList.contains("icon-pin")) toggleFog(zone, ev.target.classList);
}); });
body.on("input", function (ev) { body.on("input", function (ev) {
const el = ev.target; const line = ev.target.closest("div.states");
const zoneId = +el.parentNode.dataset.id; const zone = pack.zones.find(z => z.i === +line.dataset.id);
const zone = pack.zones.find(z => z.i === zoneId);
if (!zone) return; if (!zone) return;
if (el.classList.contains("zoneName")) changeDescription(zone, el.value); if (ev.target.classList.contains("zoneName")) changeDescription(zone, ev.target.value);
else if (el.classList.contains("zoneType")) changeType(zone, el.value); else if (ev.target.classList.contains("zoneType")) changeType(zone, ev.target.value);
}); });
// update type filter with a list of used types // update type filter with a list of used types

155
utils/pathUtils.js Normal file
View file

@ -0,0 +1,155 @@
"use strict";
// get continuous paths for all cells at once based on getType(cellId) comparison
function getVertexPaths({getType, options}) {
const {cells, vertices} = pack;
const paths = {};
const checkedCells = new Uint8Array(cells.c.length);
const addToChecked = cellId => (checkedCells[cellId] = 1);
const isChecked = cellId => checkedCells[cellId] === 1;
for (let cellId = 0; cellId < cells.c.length; cellId++) {
if (isChecked(cellId) || getType(cellId) === 0) continue;
addToChecked(cellId);
const type = getType(cellId);
const ofSameType = cellId => getType(cellId) === type;
const ofDifferentType = cellId => getType(cellId) !== type;
const onborderCell = cells.c[cellId].find(ofDifferentType);
if (onborderCell === undefined) continue;
const feature = pack.features[cells.f[onborderCell]];
if (feature.type === "lake" && feature.shoreline.every(ofSameType)) continue; // inner lake
const startingVertex = cells.v[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({startingVertex, ofSameType, addToChecked, closeRing: true});
if (vertexChain.length < 3) continue;
addPath(type, vertexChain);
}
return Object.entries(paths);
function getBorderPath(vertexChain, discontinue) {
let discontinued = true;
let lastOperation = "";
const path = vertexChain.map(vertex => {
if (discontinue(vertex)) {
discontinued = true;
return "";
}
const operation = discontinued ? "M" : "L";
const command = operation === lastOperation ? "" : operation;
discontinued = false;
lastOperation = operation;
return ` ${command}${getVertexPoint(vertex)}`;
});
return path.join("").trim();
}
function isBorderVertex(vertex) {
const adjacentCells = vertices.c[vertex];
return adjacentCells.some(i => cells.b[i]);
}
function isLandVertex(vertex) {
const adjacentCells = vertices.c[vertex];
return adjacentCells.every(i => cells.h[i] >= MIN_LAND_HEIGHT);
}
function addPath(index, vertexChain) {
if (!paths[index]) paths[index] = {fill: "", waterGap: "", halo: ""};
if (options.fill) paths[index].fill += getFillPath(vertexChain);
if (options.halo) paths[index].halo += getBorderPath(vertexChain, isBorderVertex);
if (options.waterGap) paths[index].waterGap += getBorderPath(vertexChain, isLandVertex);
}
}
function getVertexPoint(vertexId) {
return pack.vertices.p[vertexId];
}
function getFillPath(vertexChain) {
const points = vertexChain.map(getVertexPoint);
const firstPoint = points.shift();
return `M${firstPoint} L${points.join(" ")}`;
}
// get single path for an non-continuous array of cells
function getVertexPath(cellsArray) {
const {cells, vertices} = pack;
const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true]));
const ofSameType = cellId => cellsObj[cellId];
const ofDifferentType = cellId => !cellsObj[cellId];
const checkedCells = new Uint8Array(cells.c.length);
const addToChecked = cellId => (checkedCells[cellId] = 1);
const isChecked = cellId => checkedCells[cellId] === 1;
let path = "";
for (const cellId of cellsArray) {
if (isChecked(cellId)) continue;
const onborderCell = cells.c[cellId].find(ofDifferentType);
if (onborderCell === undefined) continue;
const feature = pack.features[cells.f[onborderCell]];
if (feature.type === "lake" && feature.shoreline.every(ofSameType)) continue; // inner lake
const startingVertex = cells.v[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({startingVertex, ofSameType, addToChecked, closeRing: true});
if (vertexChain.length < 3) continue;
path += getFillPath(vertexChain);
}
return path;
}
function connectVertices({startingVertex, ofSameType, addToChecked, closeRing}) {
const vertices = pack.vertices;
const MAX_ITERATIONS = pack.cells.i.length;
const chain = []; // vertices chain to form a path
let next = startingVertex;
for (let i = 0; i === 0 || next !== startingVertex; i++) {
const previous = chain.at(-1);
const current = next;
chain.push(current);
const neibCells = vertices.c[current];
if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
const [c1, c2, c3] = neibCells.map(ofSameType);
const [v1, v2, v3] = vertices.v[current];
if (v1 !== previous && c1 !== c2) next = v1;
else if (v2 !== previous && c2 !== c3) next = v2;
else if (v3 !== previous && c1 !== c3) next = v3;
if (next === current) {
ERROR && console.error("ConnectVertices: next vertex is not found");
break;
}
if (i === MAX_ITERATIONS) {
ERROR && console.error("ConnectVertices: max iterations reached", MAX_ITERATIONS);
break;
}
}
if (closeRing) chain.push(startingVertex);
return chain;
}