mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-23 12:31:24 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
d0d8015c96
23 changed files with 980 additions and 351 deletions
|
|
@ -1,20 +1,43 @@
|
|||
"use strict";
|
||||
|
||||
window.ThreeD = (function () {
|
||||
// set default options
|
||||
const options = {scale: 50, lightness: 0.7, shadow: 0.5, sun: {x: 100, y: 600, z: 1000}, rotateMesh: 0, rotateGlobe: 0.5, skyColor: "#9ecef5", waterColor: "#466eab", extendedWater: 0, labels3d: 0, resolution: 2};
|
||||
const options = {
|
||||
scale: 50,
|
||||
lightness: 0.7,
|
||||
shadow: 0.5,
|
||||
sun: {x: 100, y: 600, z: 1000},
|
||||
rotateMesh: 0,
|
||||
rotateGlobe: 0.5,
|
||||
skyColor: "#9ecef5",
|
||||
waterColor: "#466eab",
|
||||
extendedWater: 0,
|
||||
labels3d: 0,
|
||||
resolution: 2
|
||||
};
|
||||
|
||||
// set variables
|
||||
let Renderer, scene, camera, controls, animationFrame, material, texture, geometry, mesh, ambientLight, spotLight, waterPlane, waterMaterial, waterMesh, raycaster;
|
||||
|
||||
const drawCtx = document.createElement("canvas").getContext("2d");
|
||||
const drawSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
document.body.appendChild(drawSVG);
|
||||
let Renderer,
|
||||
scene,
|
||||
camera,
|
||||
controls,
|
||||
animationFrame,
|
||||
material,
|
||||
texture,
|
||||
geometry,
|
||||
mesh,
|
||||
ambientLight,
|
||||
spotLight,
|
||||
waterPlane,
|
||||
waterMaterial,
|
||||
waterMesh,
|
||||
raycaster;
|
||||
|
||||
let labels = [];
|
||||
let icons = [];
|
||||
let lines = [];
|
||||
|
||||
const context2d = document.createElement("canvas").getContext("2d");
|
||||
|
||||
// initiate 3d scene
|
||||
const create = async function (canvas, type = "viewMesh") {
|
||||
options.isOn = true;
|
||||
|
|
@ -210,16 +233,16 @@ window.ThreeD = (function () {
|
|||
}
|
||||
|
||||
async function createTextLabel({text, font, size, color, quality}) {
|
||||
drawCtx.font = `${size * quality}px ${font}`;
|
||||
drawCtx.canvas.width = drawCtx.measureText(text).width;
|
||||
drawCtx.canvas.height = size * quality * 1.25; // 25% margin as text can overflow the font size
|
||||
drawCtx.clearRect(0, 0, drawCtx.canvas.width, drawCtx.canvas.height);
|
||||
context2d.font = `${size * quality}px ${font}`;
|
||||
context2d.canvas.width = context2d.measureText(text).width;
|
||||
context2d.canvas.height = size * quality * 1.25; // 25% margin as text can overflow the font size
|
||||
context2d.clearRect(0, 0, context2d.canvas.width, context2d.canvas.height);
|
||||
|
||||
drawCtx.font = `${size * quality}px ${font}`;
|
||||
drawCtx.fillStyle = color;
|
||||
drawCtx.fillText(text, 0, size * quality);
|
||||
context2d.font = `${size * quality}px ${font}`;
|
||||
context2d.fillStyle = color;
|
||||
context2d.fillText(text, 0, size * quality);
|
||||
|
||||
return textureToSprite(drawCtx.canvas.toDataURL(), drawCtx.canvas.width / quality, drawCtx.canvas.height / quality);
|
||||
return textureToSprite(context2d.canvas.toDataURL(), context2d.canvas.width / quality, context2d.canvas.height / quality);
|
||||
}
|
||||
|
||||
function get3dCoords(baseX, baseY) {
|
||||
|
|
@ -578,5 +601,21 @@ window.ThreeD = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
return {create, redraw, update, stop, options, setScale, setLightness, setSun, setRotation, toggleLabels, toggleSky, setResolution, setColors, saveScreenshot, saveOBJ};
|
||||
return {
|
||||
create,
|
||||
redraw,
|
||||
update,
|
||||
stop,
|
||||
options,
|
||||
setScale,
|
||||
setLightness,
|
||||
setSun,
|
||||
setRotation,
|
||||
toggleLabels,
|
||||
toggleSky,
|
||||
setResolution,
|
||||
setColors,
|
||||
saveScreenshot,
|
||||
saveOBJ
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ class Battle {
|
|||
}
|
||||
|
||||
getInitialMorale() {
|
||||
const powerFee = diff => Math.min(Math.max(100 - diff ** 1.5 * 10 + 10, 50), 100);
|
||||
const powerFee = diff => minmax(100 - diff ** 1.5 * 10 + 10, 50, 100);
|
||||
const distanceFee = dist => Math.min(d3.mean(dist) / 50, 15);
|
||||
const powerDiff = this.defenders.power / this.attackers.power;
|
||||
this.attackers.morale = powerFee(powerDiff) - distanceFee(this.attackers.distances);
|
||||
|
|
|
|||
|
|
@ -621,7 +621,7 @@ function editHeightmap() {
|
|||
const interpolate = d3.interpolateRound(power, 1);
|
||||
const land = changeOnlyLand.checked;
|
||||
function lim(v) {
|
||||
return Math.max(Math.min(v, 100), land ? 20 : 0);
|
||||
return minmax(v, land ? 20 : 0, 100);
|
||||
}
|
||||
const h = grid.cells.h;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ document.addEventListener("keydown", handleKeydown);
|
|||
document.addEventListener("keyup", handleKeyup);
|
||||
|
||||
function handleKeydown(event) {
|
||||
const {key, code, ctrlKey, altKey} = event;
|
||||
const {code, ctrlKey, altKey} = event;
|
||||
if (altKey && !ctrlKey) event.preventDefault(); // disallow alt key combinations
|
||||
if (ctrlKey && ["KeyS", "KeyC"].includes(code)) event.preventDefault(); // disallow CTRL + S and CTRL + C
|
||||
if (["F1", "F2", "F6", "F9", "Tab"].includes(key)) event.preventDefault(); // disallow default Fn and Tab
|
||||
if (["F1", "F2", "F6", "F9", "Tab"].includes(code)) event.preventDefault(); // disallow default Fn and Tab
|
||||
}
|
||||
|
||||
function handleKeyup(event) {
|
||||
|
|
@ -19,83 +19,82 @@ function handleKeyup(event) {
|
|||
if (document.getSelection().toString()) return; // don't trigger if user selects text
|
||||
event.stopPropagation();
|
||||
|
||||
const {ctrlKey, metaKey, shiftKey, altKey} = event;
|
||||
const key = event.key.toUpperCase();
|
||||
const {code, key, ctrlKey, metaKey, shiftKey, altKey} = event;
|
||||
const ctrl = ctrlKey || metaKey || key === "Control";
|
||||
const shift = shiftKey || key === "Shift";
|
||||
const alt = altKey || key === "Alt";
|
||||
|
||||
if (key === "F1") showInfo();
|
||||
else if (key === "F2") regeneratePrompt("hotkey");
|
||||
else if (key === "F6") quickSave();
|
||||
else if (key === "F9") quickLoad();
|
||||
else if (key === "TAB") toggleOptions(event);
|
||||
else if (key === "ESCAPE") closeAllDialogs();
|
||||
else if (key === "DELETE") removeElementOnKey();
|
||||
else if (key === "O" && document.getElementById("canvas3d")) toggle3dOptions();
|
||||
else if (ctrl && key === "Q") toggleSaveReminder();
|
||||
else if (ctrl && key === "S") dowloadMap();
|
||||
else if (ctrl && key === "C") saveToDropbox();
|
||||
else if (ctrl && key === "Z" && undo.offsetParent) undo.click();
|
||||
else if (ctrl && key === "Y" && redo.offsetParent) redo.click();
|
||||
else if (shift && key === "H") editHeightmap();
|
||||
else if (shift && key === "B") editBiomes();
|
||||
else if (shift && key === "S") editStates();
|
||||
else if (shift && key === "P") editProvinces();
|
||||
else if (shift && key === "D") editDiplomacy();
|
||||
else if (shift && key === "C") editCultures();
|
||||
else if (shift && key === "N") editNamesbase();
|
||||
else if (shift && key === "Z") editZones();
|
||||
else if (shift && key === "R") editReligions();
|
||||
else if (shift && key === "Y") openEmblemEditor();
|
||||
else if (shift && key === "Q") editUnits();
|
||||
else if (shift && key === "O") editNotes();
|
||||
else if (shift && key === "T") overviewBurgs();
|
||||
else if (shift && key === "V") overviewRivers();
|
||||
else if (shift && key === "M") overviewMilitary();
|
||||
else if (shift && key === "K") overviewMarkers();
|
||||
else if (shift && key === "E") viewCellDetails();
|
||||
else if (shift && key === "1") toggleAddBurg();
|
||||
else if (shift && key === "2") toggleAddLabel();
|
||||
else if (shift && key === "3") toggleAddRiver();
|
||||
else if (shift && key === "4") toggleAddRoute();
|
||||
else if (shift && key === "5") toggleAddMarker();
|
||||
else if (alt && key === "B") console.table(pack.burgs);
|
||||
else if (alt && key === "S") console.table(pack.states);
|
||||
else if (alt && key === "C") console.table(pack.cultures);
|
||||
else if (alt && key === "R") console.table(pack.religions);
|
||||
else if (alt && key === "F") console.table(pack.features);
|
||||
else if (key === "X") toggleTexture();
|
||||
else if (key === "H") toggleHeight();
|
||||
else if (key === "B") toggleBiomes();
|
||||
else if (key === "E") toggleCells();
|
||||
else if (key === "G") toggleGrid();
|
||||
else if (key === "O") toggleCoordinates();
|
||||
else if (key === "W") toggleCompass();
|
||||
else if (key === "V") toggleRivers();
|
||||
else if (key === "F") toggleRelief();
|
||||
else if (key === "C") toggleCultures();
|
||||
else if (key === "S") toggleStates();
|
||||
else if (key === "P") toggleProvinces();
|
||||
else if (key === "Z") toggleZones();
|
||||
else if (key === "D") toggleBorders();
|
||||
else if (key === "R") toggleReligions();
|
||||
else if (key === "U") toggleRoutes();
|
||||
else if (key === "T") toggleTemp();
|
||||
else if (key === "N") togglePopulation();
|
||||
else if (key === "J") toggleIce();
|
||||
else if (key === "A") togglePrec();
|
||||
else if (key === "Y") toggleEmblems();
|
||||
else if (key === "L") toggleLabels();
|
||||
else if (key === "I") toggleIcons();
|
||||
else if (key === "M") toggleMilitary();
|
||||
else if (key === "K") toggleMarkers();
|
||||
else if (key === "=") toggleRulers();
|
||||
else if (key === "/") toggleScaleBar();
|
||||
else if (key === "ARROWLEFT") zoom.translateBy(svg, 10, 0);
|
||||
else if (key === "ARROWRIGHT") zoom.translateBy(svg, -10, 0);
|
||||
else if (key === "ARROWUP") zoom.translateBy(svg, 0, 10);
|
||||
else if (key === "ARROWDOWN") zoom.translateBy(svg, 0, -10);
|
||||
if (code === "F1") showInfo();
|
||||
else if (code === "F2") regeneratePrompt("hotkey");
|
||||
else if (code === "F6") quickSave();
|
||||
else if (code === "F9") quickLoad();
|
||||
else if (code === "Tab") toggleOptions(event);
|
||||
else if (code === "Escape") closeAllDialogs();
|
||||
else if (code === "Delete") removeElementOnKey();
|
||||
else if (code === "KeyO" && document.getElementById("canvas3d")) toggle3dOptions();
|
||||
else if (ctrl && code === "KeyQ") toggleSaveReminder();
|
||||
else if (ctrl && code === "KeyS") dowloadMap();
|
||||
else if (ctrl && code === "KeyC") saveToDropbox();
|
||||
else if (ctrl && code === "KeyZ" && undo.offsetParent) undo.click();
|
||||
else if (ctrl && code === "KeyY" && redo.offsetParent) redo.click();
|
||||
else if (shift && code === "KeyH") editHeightmap();
|
||||
else if (shift && code === "KeyB") editBiomes();
|
||||
else if (shift && code === "KeyS") editStates();
|
||||
else if (shift && code === "KeyP") editProvinces();
|
||||
else if (shift && code === "KeyD") editDiplomacy();
|
||||
else if (shift && code === "KeyC") editCultures();
|
||||
else if (shift && code === "KeyN") editNamesbase();
|
||||
else if (shift && code === "KeyZ") editZones();
|
||||
else if (shift && code === "KeyR") editReligions();
|
||||
else if (shift && code === "KeyY") openEmblemEditor();
|
||||
else if (shift && code === "KeyQ") editUnits();
|
||||
else if (shift && code === "KeyO") editNotes();
|
||||
else if (shift && code === "KeyT") overviewBurgs();
|
||||
else if (shift && code === "KeyV") overviewRivers();
|
||||
else if (shift && code === "KeyM") overviewMilitary();
|
||||
else if (shift && code === "KeyK") overviewMarkers();
|
||||
else if (shift && code === "KeyE") viewCellDetails();
|
||||
else if (key === "!") toggleAddBurg();
|
||||
else if (key === "@") toggleAddLabel();
|
||||
else if (key === "#") toggleAddRiver();
|
||||
else if (key === "$") toggleAddRoute();
|
||||
else if (key === "%") toggleAddMarker();
|
||||
else if (alt && code === "KeyB") console.table(pack.burgs);
|
||||
else if (alt && code === "KeyS") console.table(pack.states);
|
||||
else if (alt && code === "KeyC") console.table(pack.cultures);
|
||||
else if (alt && code === "KeyR") console.table(pack.religions);
|
||||
else if (alt && code === "KeyF") console.table(pack.features);
|
||||
else if (code === "KeyX") toggleTexture();
|
||||
else if (code === "KeyH") toggleHeight();
|
||||
else if (code === "KeyB") toggleBiomes();
|
||||
else if (code === "KeyE") toggleCells();
|
||||
else if (code === "KeyG") toggleGrid();
|
||||
else if (code === "KeyO") toggleCoordinates();
|
||||
else if (code === "KeyW") toggleCompass();
|
||||
else if (code === "KeyV") toggleRivers();
|
||||
else if (code === "KeyF") toggleRelief();
|
||||
else if (code === "KeyC") toggleCultures();
|
||||
else if (code === "KeyS") toggleStates();
|
||||
else if (code === "KeyP") toggleProvinces();
|
||||
else if (code === "KeyZ") toggleZones();
|
||||
else if (code === "KeyD") toggleBorders();
|
||||
else if (code === "KeyR") toggleReligions();
|
||||
else if (code === "KeyU") toggleRoutes();
|
||||
else if (code === "KeyT") toggleTemp();
|
||||
else if (code === "KeyN") togglePopulation();
|
||||
else if (code === "KeyJ") toggleIce();
|
||||
else if (code === "KeyA") togglePrec();
|
||||
else if (code === "KeyY") toggleEmblems();
|
||||
else if (code === "KeyL") toggleLabels();
|
||||
else if (code === "KeyI") toggleIcons();
|
||||
else if (code === "KeyM") toggleMilitary();
|
||||
else if (code === "KeyK") toggleMarkers();
|
||||
else if (code === "Equal") toggleRulers();
|
||||
else if (code === "Slash") toggleScaleBar();
|
||||
else if (code === "ArrowLeft") zoom.translateBy(svg, 10, 0);
|
||||
else if (code === "ArrowRight") zoom.translateBy(svg, -10, 0);
|
||||
else if (code === "ArrowUp") zoom.translateBy(svg, 0, 10);
|
||||
else if (code === "ArrowDown") zoom.translateBy(svg, 0, -10);
|
||||
else if (key === "+" || key === "-") pressNumpadSign(key);
|
||||
else if (key === "0") resetZoom(1000);
|
||||
else if (key === "1") zoom.scaleTo(svg, 1);
|
||||
|
|
@ -123,7 +122,7 @@ function pressNumpadSign(key) {
|
|||
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
|
||||
|
||||
if (brush) {
|
||||
const value = Math.max(Math.min(+brush.value + change, +brush.max), +brush.min);
|
||||
const value = minmax(+brush.value + change, +brush.min, +brush.max);
|
||||
brush.value = document.getElementById(brush.id + "Number").value = value;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,7 @@ function changePreset(preset) {
|
|||
.querySelectorAll("li")
|
||||
.forEach(function (e) {
|
||||
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click();
|
||||
// turn on
|
||||
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
|
||||
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click();
|
||||
});
|
||||
layersPreset.value = preset;
|
||||
localStorage.setItem("preset", preset);
|
||||
|
|
@ -121,6 +120,7 @@ function restoreLayers() {
|
|||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
if (layerIsOn("toggleIce")) drawIce();
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
||||
if (layerIsOn("toggleMarkers")) drawMarkers();
|
||||
|
||||
// some layers are rendered each time, remove them if they are not on
|
||||
if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove();
|
||||
|
|
@ -1435,8 +1435,8 @@ function toggleTexture(event) {
|
|||
turnButtonOn("toggleTexture");
|
||||
// append default texture image selected by default. Don't append on load to not harm performance
|
||||
if (!texture.selectAll("*").size()) {
|
||||
const x = +styleTextureShiftX.value,
|
||||
y = +styleTextureShiftY.value;
|
||||
const x = +styleTextureShiftX.value;
|
||||
const y = +styleTextureShiftY.value;
|
||||
const image = texture
|
||||
.append("image")
|
||||
.attr("id", "textureImage")
|
||||
|
|
@ -1444,18 +1444,14 @@ function toggleTexture(event) {
|
|||
.attr("y", y)
|
||||
.attr("width", graphWidth - x)
|
||||
.attr("height", graphHeight - y)
|
||||
.attr("xlink:href", getDefaultTexture())
|
||||
.attr("preserveAspectRatio", "xMidYMid slice");
|
||||
if (styleTextureInput.value !== "default") getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
|
||||
getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
|
||||
}
|
||||
$("#texture").fadeIn();
|
||||
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||
if (event && isCtrlClick(event)) editStyle("texture");
|
||||
} else {
|
||||
if (event && isCtrlClick(event)) {
|
||||
editStyle("texture");
|
||||
return;
|
||||
}
|
||||
if (event && isCtrlClick(event)) return editStyle("texture");
|
||||
$("#texture").fadeOut();
|
||||
turnButtonOff("toggleTexture");
|
||||
}
|
||||
|
|
@ -1563,7 +1559,7 @@ const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
|
|||
function drawMarker(marker, rescale = 1) {
|
||||
const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
|
||||
const id = `marker${i}`;
|
||||
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : 1;
|
||||
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
|
||||
const viewX = rn(x - zoomSize / 2, 1);
|
||||
const viewY = rn(y - zoomSize, 1);
|
||||
const pinHTML = getPin(pin, fill, stroke);
|
||||
|
|
@ -1674,21 +1670,21 @@ function drawEmblems() {
|
|||
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coaSize != 0);
|
||||
|
||||
const getStateEmblemsSize = () => {
|
||||
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100);
|
||||
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
|
||||
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
|
||||
const sizeMod = +document.getElementById("emblemsStateSizeInput").value || 1;
|
||||
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
|
||||
};
|
||||
|
||||
const getProvinceEmblemsSize = () => {
|
||||
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 100, 5), 70);
|
||||
const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
|
||||
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
|
||||
const sizeMod = +document.getElementById("emblemsProvinceSizeInput").value || 1;
|
||||
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
|
||||
};
|
||||
|
||||
const getBurgEmblemSize = () => {
|
||||
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 185, 2), 50);
|
||||
const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
|
||||
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
|
||||
const sizeMod = +document.getElementById("emblemsBurgSizeInput").value || 1;
|
||||
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ function editMarker(markerI) {
|
|||
|
||||
elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
|
||||
|
||||
if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
|
||||
|
||||
// dom elements
|
||||
const markerType = document.getElementById("markerType");
|
||||
const markerIcon = document.getElementById("markerIcon");
|
||||
|
|
|
|||
|
|
@ -75,14 +75,21 @@ function overviewMilitary() {
|
|||
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
|
||||
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(" ");
|
||||
|
||||
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
|
||||
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
|
||||
lines += `<div class="states" data-id=${s.i} data-state="${
|
||||
s.name
|
||||
}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
|
||||
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
|
||||
s.color
|
||||
}" class="fillRect"></svg>
|
||||
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
|
||||
${lineData}
|
||||
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div>
|
||||
<div data-type="population" data-tip="State population">${si(population)}</div>
|
||||
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div>
|
||||
<input data-tip="War Alert. Editable modifier to military forces number, depends of political situation" style="width:4.1em" type="number" min=0 step=.01 value="${rn(s.alert, 2)}">
|
||||
<input data-tip="War Alert. Editable modifier to military forces number, depends of political situation" style="width:4.1em" type="number" min=0 step=.01 value="${rn(
|
||||
s.alert,
|
||||
2
|
||||
)}">
|
||||
<span data-tip="Show regiments list" class="icon-list-bullet pointer"></span>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -145,7 +152,15 @@ function overviewMilitary() {
|
|||
if (!layerIsOn("toggleStates")) return;
|
||||
const d = regions.select("#state" + state).attr("d");
|
||||
|
||||
const path = debug.append("path").attr("class", "highlight").attr("d", d).attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1).attr("filter", "url(#blur1)");
|
||||
const path = debug
|
||||
.append("path")
|
||||
.attr("class", "highlight")
|
||||
.attr("d", d)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", "red")
|
||||
.attr("stroke-width", 1)
|
||||
.attr("opacity", 1)
|
||||
.attr("filter", "url(#blur1)");
|
||||
|
||||
const l = path.node().getTotalLength(),
|
||||
dur = (l + 5000) / 2;
|
||||
|
|
@ -199,9 +214,9 @@ function overviewMilitary() {
|
|||
|
||||
function militaryCustomize() {
|
||||
const types = ["melee", "ranged", "mounted", "machinery", "naval", "armored", "aviation", "magical"];
|
||||
const table = document.getElementById("militaryOptions").querySelector("tbody");
|
||||
const tableBody = document.getElementById("militaryOptions").querySelector("tbody");
|
||||
removeUnitLines();
|
||||
options.military.map(u => addUnitLine(u));
|
||||
options.military.map(unit => addUnitLine(unit));
|
||||
|
||||
$("#militaryOptions").dialog({
|
||||
title: "Edit Military Units",
|
||||
|
|
@ -218,43 +233,132 @@ function overviewMilitary() {
|
|||
},
|
||||
open: function () {
|
||||
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
|
||||
buttons[0].addEventListener("mousemove", () => tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>"));
|
||||
buttons[0].addEventListener("mousemove", () =>
|
||||
tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>")
|
||||
);
|
||||
buttons[1].addEventListener("mousemove", () => tip("Add new military unit to the table"));
|
||||
buttons[2].addEventListener("mousemove", () => tip("Restore default military units and settings"));
|
||||
buttons[3].addEventListener("mousemove", () => tip("Close the window without saving the changes"));
|
||||
}
|
||||
});
|
||||
|
||||
if (modules.overviewMilitaryCustomize) return;
|
||||
modules.overviewMilitaryCustomize = true;
|
||||
|
||||
tableBody.addEventListener("click", event => {
|
||||
const el = event.target;
|
||||
if (el.tagName !== "BUTTON") return;
|
||||
const type = el.dataset.type;
|
||||
|
||||
if (type === "icon") return selectIcon(el.innerHTML, v => (el.innerHTML = v));
|
||||
if (type === "biomes") {
|
||||
const {i, name, color} = biomesData;
|
||||
const biomesArray = Array(i.length).fill(null);
|
||||
const biomes = biomesArray.map((_, i) => ({i, name: name[i], color: color[i]}));
|
||||
return selectLimitation(el, biomes);
|
||||
}
|
||||
if (type === "states") return selectLimitation(el, pack.states);
|
||||
if (type === "cultures") return selectLimitation(el, pack.cultures);
|
||||
if (type === "religions") return selectLimitation(el, pack.religions);
|
||||
});
|
||||
|
||||
function removeUnitLines() {
|
||||
table.querySelectorAll("tr").forEach(el => el.remove());
|
||||
tableBody.querySelectorAll("tr").forEach(el => el.remove());
|
||||
}
|
||||
|
||||
function addUnitLine(u) {
|
||||
function getLimitValue(attr) {
|
||||
return attr?.join(",") || "";
|
||||
}
|
||||
|
||||
function getLimitText(attr) {
|
||||
return attr?.length ? "some" : "all";
|
||||
}
|
||||
|
||||
function getLimitTip(attr, data) {
|
||||
if (!attr || !attr.length) return "";
|
||||
return attr.map(i => data?.[i]?.name || "").join(", ");
|
||||
}
|
||||
|
||||
function addUnitLine(unit) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon || " "}</button></td>
|
||||
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
|
||||
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
|
||||
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
|
||||
<td><input data-tip="Enter average number of people in crew (used for total personnel calculation)" type="number" min=1 step=1 value="${u.crew}"></td>
|
||||
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${u.power}"></td>
|
||||
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types.map(t => `<option ${u.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ")}</select></td>
|
||||
const typeOptions = types.map(t => `<option ${unit.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ");
|
||||
const getLimitButton = attr =>
|
||||
`<button
|
||||
data-tip="Select allowed ${attr}"
|
||||
data-type="${attr}"
|
||||
title="${getLimitTip(unit[attr], pack[attr])}"
|
||||
data-value="${getLimitValue(unit[attr])}">
|
||||
${getLimitText(unit[attr])}
|
||||
</button>`;
|
||||
|
||||
row.innerHTML = `<td><button data-type="icon" data-tip="Click to select unit icon">${unit.icon || " "}</button></td>
|
||||
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${unit.name}"></td>
|
||||
<td>${getLimitButton("biomes")}</td>
|
||||
<td>${getLimitButton("states")}</td>
|
||||
<td>${getLimitButton("cultures")}</td>
|
||||
<td>${getLimitButton("religions")}</td>
|
||||
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${unit.rural}"></td>
|
||||
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${unit.urban}"></td>
|
||||
<td><input data-tip="Enter average number of people in crew (for total personnel calculation)" type="number" min=1 step=1 value="${unit.crew}"></td>
|
||||
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${unit.power}"></td>
|
||||
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${typeOptions}</select></td>
|
||||
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
|
||||
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? "checked" : ""}>
|
||||
<label for="${u.name}Separate" class="checkbox-label"></label></td>
|
||||
<input id="${unit.name}Separate" type="checkbox" class="checkbox" ${unit.separate ? "checked" : ""}>
|
||||
<label for="${unit.name}Separate" class="checkbox-label"></label></td>
|
||||
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
|
||||
row.querySelector("button").addEventListener("click", function (e) {
|
||||
selectIcon(this.innerHTML, v => (this.innerHTML = v));
|
||||
});
|
||||
table.appendChild(row);
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
|
||||
function restoreDefaultUnits() {
|
||||
removeUnitLines();
|
||||
Military.getDefaultOptions().map(u => addUnitLine(u));
|
||||
Military.getDefaultOptions().map(unit => addUnitLine(unit));
|
||||
}
|
||||
|
||||
function selectLimitation(el, data) {
|
||||
const type = el.dataset.type;
|
||||
const value = el.dataset.value;
|
||||
const initial = value ? value.split(",").map(v => +v) : [];
|
||||
|
||||
const lines = data.slice(1).map(
|
||||
({i, name, fullName, color}) =>
|
||||
`<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td>
|
||||
<td><input id="el${i}" type="checkbox" class="checkbox" ${!initial.length || initial.includes(i) ? "checked" : ""} >
|
||||
<label for="el${i}" class="checkbox-label">${fullName || name}</label>
|
||||
</td></tr>`
|
||||
);
|
||||
alertMessage.innerHTML = `<b>Limit unit by ${type}:</b><div style="margin-top:.3em" class="table"><table><tbody>${lines.join("")}</tbody></table></div>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
width: fitContent(),
|
||||
title: `Limit unit`,
|
||||
buttons: {
|
||||
Invert: function () {
|
||||
alertMessage.querySelectorAll("input").forEach(el => (el.checked = !el.checked));
|
||||
},
|
||||
Apply: function () {
|
||||
const inputs = Array.from(alertMessage.querySelectorAll("input"));
|
||||
const selected = inputs.reduce((acc, input, index) => {
|
||||
if (input.checked) acc.push(index + 1);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (!selected.length) return tip("Select at least one element", false, "error");
|
||||
|
||||
const allAreSelected = selected.length === inputs.length;
|
||||
el.dataset.value = allAreSelected ? "" : selected.join(",");
|
||||
el.innerHTML = allAreSelected ? "all" : "some";
|
||||
el.setAttribute("title", getLimitTip(selected, data));
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyMilitaryOptions() {
|
||||
const unitLines = Array.from(table.querySelectorAll("tr"));
|
||||
const unitLines = Array.from(tableBody.querySelectorAll("tr"));
|
||||
const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_"));
|
||||
if (new Set(names).size !== names.length) {
|
||||
tip("All units should have unique names", false, "error");
|
||||
|
|
@ -263,14 +367,22 @@ function overviewMilitary() {
|
|||
|
||||
$("#militaryOptions").dialog("close");
|
||||
options.military = unitLines.map((r, i) => {
|
||||
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll("input, select, button")).map(d => {
|
||||
let value = d.value;
|
||||
if (d.type === "number") value = +d.value || 0;
|
||||
if (d.type === "checkbox") value = +d.checked || 0;
|
||||
if (d.type === "button") value = d.innerHTML || "⠀";
|
||||
return value;
|
||||
const elements = Array.from(r.querySelectorAll("input, button, select"));
|
||||
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map(el => {
|
||||
const {type, value} = el.dataset || {};
|
||||
if (type === "icon") return el.innerHTML || "⠀";
|
||||
if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
|
||||
if (el.type === "number") return +el.value || 0;
|
||||
if (el.type === "checkbox") return +el.checked || 0;
|
||||
return el.value;
|
||||
});
|
||||
return {icon, name: names[i], rural, urban, crew, power, type, separate};
|
||||
|
||||
const unit = {icon, name: names[i], rural, urban, crew, power, type, separate};
|
||||
if (biomes) unit.biomes = biomes;
|
||||
if (states) unit.states = states;
|
||||
if (cultures) unit.cultures = cultures;
|
||||
if (religions) unit.religions = religions;
|
||||
return unit;
|
||||
});
|
||||
localStorage.setItem("military", JSON.stringify(options.military));
|
||||
Military.generate();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"use strict";
|
||||
|
||||
function editNotes(id, name) {
|
||||
// update list of objects
|
||||
const select = document.getElementById("notesSelect");
|
||||
|
|
@ -8,11 +9,12 @@ function editNotes(id, name) {
|
|||
}
|
||||
|
||||
// initiate pell (html editor)
|
||||
const notesText = document.getElementById("notesText");
|
||||
notesText.innerHTML = "";
|
||||
const editor = Pell.init({
|
||||
element: document.getElementById("notesText"),
|
||||
element: notesText,
|
||||
onChange: html => {
|
||||
const id = document.getElementById("notesSelect").value;
|
||||
const note = notes.find(note => note.id === id);
|
||||
const note = notes.find(note => note.id === select.value);
|
||||
if (!note) return;
|
||||
note.legend = html;
|
||||
showNote(note);
|
||||
|
|
@ -43,8 +45,7 @@ function editNotes(id, name) {
|
|||
title: "Notes Editor",
|
||||
minWidth: "40em",
|
||||
width: "50vw",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
close: () => (notesText.innerHTML = "")
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
if (modules.editNotes) return;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ if (localStorage.getItem("disable_click_arrow_tooltip")) {
|
|||
|
||||
// Show options pane on trigger click
|
||||
function showOptions(event) {
|
||||
track("click", "show options");
|
||||
if (!localStorage.getItem("disable_click_arrow_tooltip")) {
|
||||
clearMainTip();
|
||||
localStorage.setItem("disable_click_arrow_tooltip", true);
|
||||
|
|
@ -76,7 +75,6 @@ document
|
|||
|
||||
// show popup with a list of Patreon supportes (updated manually, to be replaced with API call)
|
||||
function showSupporters() {
|
||||
track("click", "show supporters");
|
||||
const supporters = `Aaron Meyer,Ahmad Amerih,AstralJacks,aymeric,Billy Dean Goehring,Branndon Edwards,Chase Mayers,Curt Flood,cyninge,Dino Princip,
|
||||
E.M. White,es,Fondue,Fritjof Olsson,Gatsu,Johan Fröberg,Jonathan Moore,Joseph Miranda,Kate,KC138,Luke Nelson,Markus Finster,Massimo Vella,Mikey,
|
||||
Nathan Mitchell,Paavi1,Pat,Ryan Westcott,Sasquatch,Shawn Spencer,Sizz_TV,Timothée CALLET,UTG community,Vlad Tomash,Wil Sisney,William Merriott,
|
||||
|
|
@ -91,19 +89,18 @@ function showSupporters() {
|
|||
Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge,
|
||||
Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ,
|
||||
Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,
|
||||
Thirty-OneR ,ThatGuyGW ,Dee Chiu,MontyBoosh ,Achillain ,Jaden ,SashaTK,Steve Johnson,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,Thirty-OneR,
|
||||
ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill,
|
||||
Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,Alex Debus,Joshua Vaught,
|
||||
Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6,
|
||||
Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen,
|
||||
Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,Cooper Counts,Patrick Jones,Clonetone,
|
||||
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
|
||||
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
|
||||
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
|
||||
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 金,Chris Gray,Phoenix Boatwright,Mackenzie,
|
||||
Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,
|
||||
George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,Mike Conley,Xavier privé,Hope You're Well,
|
||||
Mark Sprietsma,Robert Landry,Nick Mowry"`;
|
||||
Thirty-OneR,ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,
|
||||
Andrew Rostaing,Daniel Gill,Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,
|
||||
Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,
|
||||
Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,
|
||||
Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,
|
||||
Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,
|
||||
Page One Project,Spencer Morris,Paul Ingram,Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,
|
||||
Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,
|
||||
PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,
|
||||
Nobody679,良义 金,Chris Gray,Phoenix Boatwright,Mackenzie,Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,
|
||||
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
|
||||
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,Dario Spadavecchia`;
|
||||
|
||||
const array = supporters
|
||||
.replace(/(?:\r\n|\r|\n)/g, "")
|
||||
|
|
@ -476,10 +473,10 @@ function changeDialogsTheme(themeColor, transparency) {
|
|||
}
|
||||
|
||||
function changeZoomExtent(value) {
|
||||
const min = Math.max(+zoomExtentMin.value, 0.01),
|
||||
max = Math.min(+zoomExtentMax.value, 200);
|
||||
const min = Math.max(+zoomExtentMin.value, 0.01);
|
||||
const max = Math.min(+zoomExtentMax.value, 200);
|
||||
zoom.scaleExtent([min, max]);
|
||||
const scale = Math.max(Math.min(+value, 200), 0.01);
|
||||
const scale = minmax(+value, 0.01, 200);
|
||||
zoom.scaleTo(svg, scale);
|
||||
}
|
||||
|
||||
|
|
@ -520,7 +517,7 @@ function applyStoredOptions() {
|
|||
|
||||
uiSizeInput.max = uiSizeOutput.max = getUImaxSize();
|
||||
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
|
||||
else changeUIsize(Math.max(Math.min(rn(mapWidthInput.value / 1280, 1), 2.5), 1));
|
||||
else changeUIsize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
|
||||
|
||||
// search params overwrite stored and default options
|
||||
const params = new URL(window.location.href).searchParams;
|
||||
|
|
|
|||
|
|
@ -335,7 +335,6 @@ styleFilterInput.addEventListener("change", function () {
|
|||
|
||||
styleTextureInput.addEventListener("change", function () {
|
||||
if (this.value === "none") texture.select("image").attr("xlink:href", "");
|
||||
if (this.value === "default") texture.select("image").attr("xlink:href", getDefaultTexture());
|
||||
else getBase64(this.value, base64 => texture.select("image").attr("xlink:href", base64));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -469,7 +469,10 @@ function addLabelOnClick() {
|
|||
const name = Names.getCulture(culture);
|
||||
const id = getNextId("label");
|
||||
|
||||
let group = labels.select("#addedLabels");
|
||||
// use most recently selected label group
|
||||
let selected = labelGroupSelect.value;
|
||||
const symbol = selected ? "#" + selected : "#addedLabels";
|
||||
let group = labels.select(symbol);
|
||||
if (!group.size())
|
||||
group = labels
|
||||
.append("g")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue