From 2744d32ca07bec2cbe5cbb0db59b80885c521572 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 13 Sep 2024 17:18:19 +0200 Subject: [PATCH] fefactor: edit coastline and lake --- modules/features.js | 4 +- modules/renderers/draw-features.js | 23 +++-- modules/ui/coastline-editor.js | 147 ++++++++++++++--------------- modules/ui/editors.js | 20 ++-- modules/ui/lakes-editor.js | 138 ++++++++++++++------------- 5 files changed, 164 insertions(+), 168 deletions(-) diff --git a/modules/features.js b/modules/features.js index 8371ba6c..6e5b40fb 100644 --- a/modules/features.js +++ b/modules/features.js @@ -82,7 +82,7 @@ window.Features = (function () { TIME && console.time("markupPack"); const {cells, vertices} = pack; - const {h: heights, c: neighbors, b: borderCells, i} = cells; + const {c: neighbors, b: borderCells, i} = cells; const packCellsNumber = i.length; if (!packCellsNumber) return; // no cells -> there is nothing to do @@ -191,7 +191,7 @@ window.Features = (function () { const startingVertex = cells.v[firstCell].find(v => vertices.c[v].some(ofDifferentType)); if (startingVertex === undefined) throw new Error(`Markup: startingVertex for cell ${firstCell} is not found`); - return connectVertices({vertices, startingVertex, ofSameType, closeRing: true}); + return connectVertices({vertices, startingVertex, ofSameType, closeRing: false}); } } } diff --git a/modules/renderers/draw-features.js b/modules/renderers/draw-features.js index c50ca77b..0221a1c1 100644 --- a/modules/renderers/draw-features.js +++ b/modules/renderers/draw-features.js @@ -2,24 +2,16 @@ function drawFeatures() { TIME && console.time("drawFeatures"); - const {vertices, features} = pack; - const featurePaths = defs.select("#featurePaths"); const landMask = defs.select("#land"); const waterMask = defs.select("#water"); - const lineGen = d3.line().curve(d3.curveBasisClosed); - for (const feature of features) { + for (const feature of pack.features) { if (!feature || feature.type === "ocean") continue; - const points = feature.vertices.map(vertex => vertices.p[vertex]); - const simplifiedPoints = simplify(points, 0.3); - const clippedPoints = clipPoly(simplifiedPoints, 1); - const path = round(lineGen(clippedPoints)); - featurePaths .append("path") - .attr("d", path) + .attr("d", getFeaturePath(feature)) .attr("id", "feature_" + feature.i) .attr("data-f", feature.i); @@ -56,3 +48,14 @@ function drawFeatures() { TIME && console.timeEnd("drawFeatures"); } + +function getFeaturePath(feature) { + const points = feature.vertices.map(vertex => pack.vertices.p[vertex]); + const simplifiedPoints = simplify(points, 0.3); + const clippedPoints = clipPoly(simplifiedPoints, 1); + + const lineGen = d3.line().curve(d3.curveBasisClosed); + const path = round(lineGen(clippedPoints)); + + return path; +} diff --git a/modules/ui/coastline-editor.js b/modules/ui/coastline-editor.js index 7b41405c..6ba14afe 100644 --- a/modules/ui/coastline-editor.js +++ b/modules/ui/coastline-editor.js @@ -1,6 +1,6 @@ "use strict"; -function editCoastline(node = d3.event.target) { +function editCoastline() { if (customization) return; closeDialogs(".stable"); if (layerIsOn("toggleCells")) toggleCells(); @@ -13,6 +13,7 @@ function editCoastline(node = d3.event.target) { }); debug.append("g").attr("id", "vertices"); + const node = d3.event.target; elSelected = d3.select(node); selectCoastlineGroup(node); drawCoastlineVertices(); @@ -22,96 +23,98 @@ function editCoastline(node = d3.event.target) { modules.editCoastline = true; // add listeners - document.getElementById("coastlineGroupsShow").addEventListener("click", showGroupSection); - document.getElementById("coastlineGroup").addEventListener("change", changeCoastlineGroup); - document.getElementById("coastlineGroupAdd").addEventListener("click", toggleNewGroupInput); - document.getElementById("coastlineGroupName").addEventListener("change", createNewGroup); - document.getElementById("coastlineGroupRemove").addEventListener("click", removeCoastlineGroup); - document.getElementById("coastlineGroupsHide").addEventListener("click", hideGroupSection); - document.getElementById("coastlineEditStyle").addEventListener("click", editGroupStyle); + byId("coastlineGroupsShow").on("click", showGroupSection); + byId("coastlineGroup").on("change", changeCoastlineGroup); + byId("coastlineGroupAdd").on("click", toggleNewGroupInput); + byId("coastlineGroupName").on("change", createNewGroup); + byId("coastlineGroupRemove").on("click", removeCoastlineGroup); + byId("coastlineGroupsHide").on("click", hideGroupSection); + byId("coastlineEditStyle").on("click", editGroupStyle); function drawCoastlineVertices() { - const f = +elSelected.attr("data-f"); // feature id - const v = pack.features[f].vertices; // coastline outer vertices + const featureId = +elSelected.attr("data-f"); + const {vertices, area} = pack.features[featureId]; - const l = pack.cells.i.length; - const c = [...new Set(v.map(v => pack.vertices.c[v]).flat())].filter(c => c < l); + const cellsNumber = pack.cells.i.length; + const neibCells = unique(vertices.map(v => pack.vertices.c[v]).flat()).filter(cellId => cellId < cellsNumber); debug .select("#vertices") .selectAll("polygon") - .data(c) + .data(neibCells) .enter() .append("polygon") - .attr("points", d => getPackPolygon(d)) + .attr("points", getPackPolygon) .attr("data-c", d => d); debug .select("#vertices") .selectAll("circle") - .data(v) + .data(vertices) .enter() .append("circle") .attr("cx", d => pack.vertices.p[d][0]) .attr("cy", d => pack.vertices.p[d][1]) .attr("r", 0.4) .attr("data-v", d => d) - .call(d3.drag().on("drag", dragVertex)) + .call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd)) .on("mousemove", () => - tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights") + tip("Drag to move the vertex. Please use for fine-tuning only. Edit heightmap to change actual cell heights!") ); - const area = pack.features[f].area; coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit(); } - function dragVertex() { - const x = rn(d3.event.x, 2), - y = rn(d3.event.y, 2); + function handleVertexDrag() { + const {vertices, features} = pack; + + const x = rn(d3.event.x, 2); + const y = rn(d3.event.y, 2); this.setAttribute("cx", x); this.setAttribute("cy", y); - const v = +this.dataset.v; - pack.vertices.p[v] = [x, y]; - debug - .select("#vertices") - .selectAll("polygon") - .attr("points", d => getPackPolygon(d)); - redrawCoastline(); + const vertexId = d3.select(this).datum(); + vertices.p[vertexId] = [x, y]; + + const featureId = +elSelected.attr("data-f"); + const feature = features[featureId]; + + // change coastline path + defs.select("#featurePaths > path#feature_" + featureId).attr("d", getFeaturePath(feature)); + + // update area + const points = feature.vertices.map(vertex => vertices.p[vertex]); + feature.area = Math.abs(d3.polygonArea(points)); + coastlineArea.innerHTML = si(getArea(feature.area)) + " " + getAreaUnit(); + + // update cell + debug.select("#vertices").selectAll("polygon").attr("points", getPackPolygon); } - function redrawCoastline() { - lineGen.curve(d3.curveBasisClosed); - const f = +elSelected.attr("data-f"); - const vertices = pack.features[f].vertices; - const points = clipPoly( - vertices.map(v => pack.vertices.p[v]), - 1 - ); - const d = round(lineGen(points)); - elSelected.attr("d", d); - defs.select("mask#land > path#land_" + f).attr("d", d); // update land mask - defs.select("mask#water > path#water_" + f).attr("d", d); // update water mask - - const area = Math.abs(d3.polygonArea(points)); - coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit(); + function handleVertexDragEnd() { + if (layerIsOn("toggleStates")) drawStates(); + if (layerIsOn("toggleProvinces")) drawProvinces(); + if (layerIsOn("toggleBorders")) drawBorders(); + if (layerIsOn("toggleBiomes")) drawBiomes(); + if (layerIsOn("toggleReligions")) drawReligions(); + if (layerIsOn("toggleCultures")) drawCultures(); } function showGroupSection() { document.querySelectorAll("#coastlineEditor > button").forEach(el => (el.style.display = "none")); - document.getElementById("coastlineGroupsSelection").style.display = "inline-block"; + byId("coastlineGroupsSelection").style.display = "inline-block"; } function hideGroupSection() { document.querySelectorAll("#coastlineEditor > button").forEach(el => (el.style.display = "inline-block")); - document.getElementById("coastlineGroupsSelection").style.display = "none"; - document.getElementById("coastlineGroupName").style.display = "none"; - document.getElementById("coastlineGroupName").value = ""; - document.getElementById("coastlineGroup").style.display = "inline-block"; + byId("coastlineGroupsSelection").style.display = "none"; + byId("coastlineGroupName").style.display = "none"; + byId("coastlineGroupName").value = ""; + byId("coastlineGroup").style.display = "inline-block"; } function selectCoastlineGroup(node) { const group = node.parentNode.id; - const select = document.getElementById("coastlineGroup"); + const select = byId("coastlineGroup"); select.options.length = 0; // remove all options coastline.selectAll("g").each(function () { @@ -120,7 +123,7 @@ function editCoastline(node = d3.event.target) { } function changeCoastlineGroup() { - document.getElementById(this.value).appendChild(elSelected.node()); + byId(this.value).appendChild(elSelected.node()); } function toggleNewGroupInput() { @@ -135,54 +138,44 @@ function editCoastline(node = d3.event.target) { } function createNewGroup() { - if (!this.value) { - tip("Please provide a valid group name"); - return; - } + if (!this.value) return tip("Please provide a valid group name"); + const group = this.value .toLowerCase() .replace(/ /g, "_") .replace(/[^\w\s]/gi, ""); - if (document.getElementById(group)) { - tip("Element with this id already exists. Please provide a unique name", false, "error"); - return; - } + if (byId(group)) return tip("Element with this id already exists. Please provide a unique name", false, "error"); - if (Number.isFinite(+group.charAt(0))) { - tip("Group name should start with a letter", false, "error"); - return; - } + if (Number.isFinite(+group.charAt(0))) return tip("Group name should start with a letter", false, "error"); // just rename if only 1 element left const oldGroup = elSelected.node().parentNode; const basic = ["sea_island", "lake_island"].includes(oldGroup.id); if (!basic && oldGroup.childElementCount === 1) { - document.getElementById("coastlineGroup").selectedOptions[0].remove(); - document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true)); + byId("coastlineGroup").selectedOptions[0].remove(); + byId("coastlineGroup").options.add(new Option(group, group, false, true)); oldGroup.id = group; toggleNewGroupInput(); - document.getElementById("coastlineGroupName").value = ""; + byId("coastlineGroupName").value = ""; return; } // create a new group const newGroup = elSelected.node().parentNode.cloneNode(false); - document.getElementById("coastline").appendChild(newGroup); + byId("coastline").appendChild(newGroup); newGroup.id = group; - document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true)); - document.getElementById(group).appendChild(elSelected.node()); + byId("coastlineGroup").options.add(new Option(group, group, false, true)); + byId(group).appendChild(elSelected.node()); toggleNewGroupInput(); - document.getElementById("coastlineGroupName").value = ""; + byId("coastlineGroupName").value = ""; } function removeCoastlineGroup() { const group = elSelected.node().parentNode.id; - if (["sea_island", "lake_island"].includes(group)) { - tip("This is one of the default groups, it cannot be removed", false, "error"); - return; - } + if (["sea_island", "lake_island"].includes(group)) + return tip("This is one of the default groups, it cannot be removed", false, "error"); const count = elSelected.node().parentNode.childElementCount; alertMessage.innerHTML = /* html */ `Are you sure you want to remove the group? All coastline elements of the group (${count}) will be moved under @@ -194,14 +187,14 @@ function editCoastline(node = d3.event.target) { buttons: { Remove: function () { $(this).dialog("close"); - const sea = document.getElementById("sea_island"); - const groupEl = document.getElementById(group); + const sea = byId("sea_island"); + const groupEl = byId(group); while (groupEl.childNodes.length) { sea.appendChild(groupEl.childNodes[0]); } groupEl.remove(); - document.getElementById("coastlineGroup").selectedOptions[0].remove(); - document.getElementById("coastlineGroup").value = "sea_island"; + byId("coastlineGroup").selectedOptions[0].remove(); + byId("coastlineGroup").value = "sea_island"; }, Cancel: function () { $(this).dialog("close"); diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 6d90971d..2e761d7a 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -10,31 +10,27 @@ function restoreDefaultEvents() { legend.call(d3.drag().on("start", dragLegendBox)); } -// on viewbox click event - run function based on target +// handle viewbox click function clicked() { const el = d3.event.target; - if (!el || !el.parentElement || !el.parentElement.parentElement) return; - const parent = el.parentElement; - const grand = parent.parentElement; - const great = grand.parentElement; - const p = d3.mouse(this); - const i = findCell(p[0], p[1]); + const parent = el?.parentElement; + const grand = parent?.parentElement; + const great = grand?.parentElement; + const ancestor = great?.parentElement; + if (!ancestor) return; if (grand.id === "emblems") editEmblem(); else if (parent.id === "rivers") editRiver(el.id); else if (grand.id === "routes") editRoute(el.id); - else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); + else if (ancestor.id === "labels" && el.tagName === "tspan") editLabel(); else if (grand.id === "burgLabels") editBurg(); else if (grand.id === "burgIcons") editBurg(); else if (parent.id === "ice") editIce(); else if (parent.id === "terrain") editReliefIcon(); else if (grand.id === "markers" || great.id === "markers") editMarker(); else if (grand.id === "coastline") editCoastline(); + else if (grand.id === "lakes") editLake(); else if (great.id === "armies") editRegiment(); - else if (pack.cells.t[i] === 1) { - const node = byId("island_" + pack.cells.f[i]); - editCoastline(node); - } else if (grand.id === "lakes") editLake(); } // clear elSelected variable diff --git a/modules/ui/lakes-editor.js b/modules/ui/lakes-editor.js index e4977624..a6ce80d1 100644 --- a/modules/ui/lakes-editor.js +++ b/modules/ui/lakes-editor.js @@ -23,17 +23,15 @@ function editLake() { modules.editLake = true; // add listeners - document.getElementById("lakeName").addEventListener("input", changeName); - document.getElementById("lakeNameCulture").addEventListener("click", generateNameCulture); - document.getElementById("lakeNameRandom").addEventListener("click", generateNameRandom); - - document.getElementById("lakeGroup").addEventListener("change", changeLakeGroup); - document.getElementById("lakeGroupAdd").addEventListener("click", toggleNewGroupInput); - document.getElementById("lakeGroupName").addEventListener("change", createNewGroup); - document.getElementById("lakeGroupRemove").addEventListener("click", removeLakeGroup); - - document.getElementById("lakeEditStyle").addEventListener("click", editGroupStyle); - document.getElementById("lakeLegend").addEventListener("click", editLakeLegend); + byId("lakeName").on("input", changeName); + byId("lakeNameCulture").on("click", generateNameCulture); + byId("lakeNameRandom").on("click", generateNameRandom); + byId("lakeGroup").on("change", changeLakeGroup); + byId("lakeGroupAdd").on("click", toggleNewGroupInput); + byId("lakeGroupName").on("change", createNewGroup); + byId("lakeGroupRemove").on("click", removeLakeGroup); + byId("lakeEditStyle").on("click", editGroupStyle); + byId("lakeLegend").on("click", editLakeLegend); function getLake() { const lakeId = +elSelected.attr("data-f"); @@ -41,85 +39,91 @@ function editLake() { } function updateLakeValues() { - const cells = pack.cells; + const {cells, vertices, rivers} = pack; const l = getLake(); - document.getElementById("lakeName").value = l.name; - document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit(); + byId("lakeName").value = l.name; + byId("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit(); - const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v])); - document.getElementById("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value; + const length = d3.polygonLength(l.vertices.map(v => vertices.p[v])); + byId("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value; const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i)); const heights = lakeCells.map(i => cells.h[i]); - document.getElementById("lakeElevation").value = getHeight(l.height); - document.getElementById("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs"); - document.getElementById("lakeMaxDepth").value = getHeight(d3.min(heights), "abs"); + byId("lakeElevation").value = getHeight(l.height); + byId("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs"); + byId("lakeMaxDepth").value = getHeight(d3.min(heights), "abs"); - document.getElementById("lakeFlux").value = l.flux; - document.getElementById("lakeEvaporation").value = l.evaporation; + byId("lakeFlux").value = l.flux; + byId("lakeEvaporation").value = l.evaporation; - const inlets = l.inlets && l.inlets.map(inlet => pack.rivers.find(river => river.i === inlet)?.name); - const outlet = l.outlet ? pack.rivers.find(river => river.i === l.outlet)?.name : "no"; - document.getElementById("lakeInlets").value = inlets ? inlets.length : "no"; - document.getElementById("lakeInlets").title = inlets ? inlets.join(", ") : ""; - document.getElementById("lakeOutlet").value = outlet; + const inlets = l.inlets && l.inlets.map(inlet => rivers.find(river => river.i === inlet)?.name); + const outlet = l.outlet ? rivers.find(river => river.i === l.outlet)?.name : "no"; + byId("lakeInlets").value = inlets ? inlets.length : "no"; + byId("lakeInlets").title = inlets ? inlets.join(", ") : ""; + byId("lakeOutlet").value = outlet; } function drawLakeVertices() { - const v = getLake().vertices; // lake outer vertices + const vertices = getLake().vertices; - const c = [...new Set(v.map(v => pack.vertices.c[v]).flat())]; + const neibCells = unique(vertices.map(v => pack.vertices.c[v]).flat()); debug .select("#vertices") .selectAll("polygon") - .data(c) + .data(neibCells) .enter() .append("polygon") - .attr("points", d => getPackPolygon(d)) + .attr("points", getPackPolygon) .attr("data-c", d => d); debug .select("#vertices") .selectAll("circle") - .data(v) + .data(vertices) .enter() .append("circle") .attr("cx", d => pack.vertices.p[d][0]) .attr("cy", d => pack.vertices.p[d][1]) .attr("r", 0.4) .attr("data-v", d => d) - .call(d3.drag().on("drag", dragVertex)) + .call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd)) .on("mousemove", () => - tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights") + tip("Drag to move the vertex. Please use for fine-tuning only! Edit heightmap to change actual cell heights") ); } - function dragVertex() { - const x = rn(d3.event.x, 2), - y = rn(d3.event.y, 2); + function handleVertexDrag() { + const x = rn(d3.event.x, 2); + const y = rn(d3.event.y, 2); this.setAttribute("cx", x); this.setAttribute("cy", y); - const v = +this.dataset.v; - pack.vertices.p[v] = [x, y]; - debug - .select("#vertices") - .selectAll("polygon") - .attr("points", d => getPackPolygon(d)); - redrawLake(); + + const vertexId = d3.select(this).datum(); + pack.vertices.p[vertexId] = [x, y]; + + const feature = getLake(); + + // update lake path + defs.select("#featurePaths > path#feature_" + feature.i).attr("d", getFeaturePath(feature)); + + // update area + const points = feature.vertices.map(vertex => pack.vertices.p[vertex]); + feature.area = Math.abs(d3.polygonArea(points)); + byId("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit(); + + // update cell + debug.select("#vertices").selectAll("polygon").attr("points", getPackPolygon); } - function redrawLake() { - lineGen.curve(d3.curveBasisClosed); - const feature = getLake(); - const points = feature.vertices.map(v => pack.vertices.p[v]); - const d = round(lineGen(points)); - elSelected.attr("d", d); - defs.select("mask#land > path#land_" + feature.i).attr("d", d); // update land mask - - feature.area = Math.abs(d3.polygonArea(points)); - document.getElementById("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit(); + function handleVertexDragEnd() { + if (layerIsOn("toggleStates")) drawStates(); + if (layerIsOn("toggleProvinces")) drawProvinces(); + if (layerIsOn("toggleBorders")) drawBorders(); + if (layerIsOn("toggleBiomes")) drawBiomes(); + if (layerIsOn("toggleReligions")) drawReligions(); + if (layerIsOn("toggleCultures")) drawCultures(); } function changeName() { @@ -138,7 +142,7 @@ function editLake() { function selectLakeGroup(node) { const group = node.parentNode.id; - const select = document.getElementById("lakeGroup"); + const select = byId("lakeGroup"); select.options.length = 0; // remove all options lakes.selectAll("g").each(function () { @@ -147,7 +151,7 @@ function editLake() { } function changeLakeGroup() { - document.getElementById(this.value).appendChild(elSelected.node()); + byId(this.value).appendChild(elSelected.node()); getLake().group = this.value; } @@ -172,7 +176,7 @@ function editLake() { .replace(/ /g, "_") .replace(/[^\w\s]/gi, ""); - if (document.getElementById(group)) { + if (byId(group)) { tip("Element with this id already exists. Please provide a unique name", false, "error"); return; } @@ -186,23 +190,23 @@ function editLake() { const oldGroup = elSelected.node().parentNode; const basic = ["freshwater", "salt", "sinkhole", "frozen", "lava", "dry"].includes(oldGroup.id); if (!basic && oldGroup.childElementCount === 1) { - document.getElementById("lakeGroup").selectedOptions[0].remove(); - document.getElementById("lakeGroup").options.add(new Option(group, group, false, true)); + byId("lakeGroup").selectedOptions[0].remove(); + byId("lakeGroup").options.add(new Option(group, group, false, true)); oldGroup.id = group; toggleNewGroupInput(); - document.getElementById("lakeGroupName").value = ""; + byId("lakeGroupName").value = ""; return; } // create a new group const newGroup = elSelected.node().parentNode.cloneNode(false); - document.getElementById("lakes").appendChild(newGroup); + byId("lakes").appendChild(newGroup); newGroup.id = group; - document.getElementById("lakeGroup").options.add(new Option(group, group, false, true)); - document.getElementById(group).appendChild(elSelected.node()); + byId("lakeGroup").options.add(new Option(group, group, false, true)); + byId(group).appendChild(elSelected.node()); toggleNewGroupInput(); - document.getElementById("lakeGroupName").value = ""; + byId("lakeGroupName").value = ""; } function removeLakeGroup() { @@ -221,14 +225,14 @@ function editLake() { buttons: { Remove: function () { $(this).dialog("close"); - const freshwater = document.getElementById("freshwater"); - const groupEl = document.getElementById(group); + const freshwater = byId("freshwater"); + const groupEl = byId(group); while (groupEl.childNodes.length) { freshwater.appendChild(groupEl.childNodes[0]); } groupEl.remove(); - document.getElementById("lakeGroup").selectedOptions[0].remove(); - document.getElementById("lakeGroup").value = "freshwater"; + byId("lakeGroup").selectedOptions[0].remove(); + byId("lakeGroup").value = "freshwater"; }, Cancel: function () { $(this).dialog("close");