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; +}