diff --git a/index.css b/index.css
index eb0eb73a..203fbf61 100644
--- a/index.css
+++ b/index.css
@@ -153,10 +153,6 @@ a {
fill-rule: evenodd;
}
-#zones {
- fill-rule: nonzero;
-}
-
#coastline {
fill: none;
stroke-linejoin: round;
diff --git a/index.html b/index.html
index f83db952..6025e3da 100644
--- a/index.html
+++ b/index.html
@@ -7989,6 +7989,7 @@
+
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index a921c36f..f0106d35 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -1892,37 +1892,7 @@ function drawZones() {
}
function drawZone({i, cells, type, color}) {
- // find a path connecting all cells of zone
- 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);
- }
-
+ const path = getVertexPath(cells);
return ``;
}
diff --git a/modules/ui/zones-editor.js b/modules/ui/zones-editor.js
index 6b466448..bcaaaa44 100644
--- a/modules/ui/zones-editor.js
+++ b/modules/ui/zones-editor.js
@@ -33,34 +33,31 @@ function editZones() {
byId("zonesRemove").on("click", e => e.target.classList.toggle("pressed"));
body.on("click", function (ev) {
- const el = ev.target;
- const classList = el.classList;
- const zoneId = +(classList.contains("states") ? el.dataset.id : el.parentNode.dataset.id);
- const zone = pack.zones.find(z => z.i === zoneId);
+ const line = ev.target.closest("div.states");
+ const zone = pack.zones.find(z => z.i === +line.dataset.id);
if (!zone) return;
if (customization) {
if (zone.hidden) return;
body.querySelector("div.selected").classList.remove("selected");
- el.classList.add("selected");
+ line.classList.add("selected");
return;
}
- if (el.closest("fill-box")) changeFill(el.getAttribute("fill"), zone);
- else if (classList.contains("zonePopulation")) changePopulation(zone);
- else if (classList.contains("icon-trash-empty")) zoneRemove(zone);
- else if (classList.contains("icon-eye")) toggleVisibility(zone);
- else if (classList.contains("icon-pin")) toggleFog(zone, classList);
+ if (ev.target.closest("fill-box")) changeFill(ev.target.closest("fill-box").getAttribute("fill"), zone);
+ else if (ev.target.classList.contains("zonePopulation")) changePopulation(zone);
+ else if (ev.target.classList.contains("icon-trash-empty")) zoneRemove(zone);
+ else if (ev.target.classList.contains("icon-eye")) toggleVisibility(zone);
+ else if (ev.target.classList.contains("icon-pin")) toggleFog(zone, ev.target.classList);
});
body.on("input", function (ev) {
- const el = ev.target;
- const zoneId = +el.parentNode.dataset.id;
- const zone = pack.zones.find(z => z.i === zoneId);
+ const line = ev.target.closest("div.states");
+ const zone = pack.zones.find(z => z.i === +line.dataset.id);
if (!zone) return;
- if (el.classList.contains("zoneName")) changeDescription(zone, el.value);
- else if (el.classList.contains("zoneType")) changeType(zone, el.value);
+ if (ev.target.classList.contains("zoneName")) changeDescription(zone, ev.target.value);
+ else if (ev.target.classList.contains("zoneType")) changeType(zone, ev.target.value);
});
// update type filter with a list of used types
diff --git a/utils/pathUtils.js b/utils/pathUtils.js
new file mode 100644
index 00000000..ff3bbf2a
--- /dev/null
+++ b/utils/pathUtils.js
@@ -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;
+}