mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-04-05 06:57:24 +02:00
feat: relief three.js renderer
This commit is contained in:
parent
7a49098425
commit
7481a2843e
19 changed files with 828 additions and 120 deletions
67
package-lock.json
generated
67
package-lock.json
generated
|
|
@ -9,10 +9,12 @@
|
||||||
"version": "1.113.5",
|
"version": "1.113.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"alea": "^1.0.1",
|
"alea": "^1.0.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"delaunator": "^5.0.1",
|
"delaunator": "^5.0.1",
|
||||||
"polylabel": "^2.0.1"
|
"polylabel": "^2.0.1",
|
||||||
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.3.13",
|
"@biomejs/biome": "2.3.13",
|
||||||
|
|
@ -195,6 +197,12 @@
|
||||||
"node": ">=14.21.3"
|
"node": ">=14.21.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dimforge/rapier3d-compat": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||||
|
|
@ -1024,6 +1032,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "23.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
|
@ -1365,6 +1379,33 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stats.js": {
|
||||||
|
"version": "0.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.183.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
|
||||||
|
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": ">=0.5.17",
|
||||||
|
"@webgpu/types": "*",
|
||||||
|
"fflate": "~0.8.2",
|
||||||
|
"meshoptimizer": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/webxr": {
|
||||||
|
"version": "0.5.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||||
|
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitest/browser": {
|
"node_modules/@vitest/browser": {
|
||||||
"version": "4.0.18",
|
"version": "4.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz",
|
||||||
|
|
@ -1524,6 +1565,12 @@
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.69",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
||||||
|
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/alea": {
|
"node_modules/alea": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz",
|
||||||
|
|
@ -2057,6 +2104,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
|
@ -2103,6 +2156,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/meshoptimizer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mrmime": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
|
|
@ -2388,6 +2447,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.183.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||||
|
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,12 @@
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"alea": "^1.0.1",
|
"alea": "^1.0.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"delaunator": "^5.0.1",
|
"delaunator": "^5.0.1",
|
||||||
"polylabel": "^2.0.1"
|
"polylabel": "^2.0.1",
|
||||||
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
|
|
|
||||||
BIN
public/images/relief/colored.png
Normal file
BIN
public/images/relief/colored.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 401 KiB |
BIN
public/images/relief/gray.png
Normal file
BIN
public/images/relief/gray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 420 KiB |
BIN
public/images/relief/simple.png
Normal file
BIN
public/images/relief/simple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
|
|
@ -231,7 +231,6 @@ t,
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#terrain,
|
|
||||||
#burgIcons {
|
#burgIcons {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -459,6 +459,8 @@ function handleZoom(isScaleChanged, isPositionChanged) {
|
||||||
fitScaleBar(scaleBar, svgWidth, svgHeight);
|
fitScaleBar(scaleBar, svgWidth, svgHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (layerIsOn("toggleRelief")) renderReliefIcons();
|
||||||
|
|
||||||
// zoom image converter overlay
|
// zoom image converter overlay
|
||||||
if (customization === 1) {
|
if (customization === 1) {
|
||||||
const canvas = byId("canvas");
|
const canvas = byId("canvas");
|
||||||
|
|
@ -1234,7 +1236,7 @@ function showStatistics() {
|
||||||
INFO && console.info(stats);
|
INFO && console.info(stats);
|
||||||
|
|
||||||
// Dispatch event for test automation and external integrations
|
// Dispatch event for test automation and external integrations
|
||||||
window.dispatchEvent(new CustomEvent('map:generated', { detail: { seed, mapId } }));
|
window.dispatchEvent(new CustomEvent("map:generated", {detail: {seed, mapId}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const regenerateMap = debounce(async function (options) {
|
const regenerateMap = debounce(async function (options) {
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,10 @@ async function getMapURL(
|
||||||
fullMap = false
|
fullMap = false
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
|
// Temporarily inject <use> elements so the clone includes relief icon data
|
||||||
|
if (typeof prepareReliefForSave === "function") prepareReliefForSave();
|
||||||
const cloneEl = byId("map").cloneNode(true); // clone svg
|
const cloneEl = byId("map").cloneNode(true); // clone svg
|
||||||
|
if (typeof restoreReliefAfterSave === "function") restoreReliefAfterSave();
|
||||||
cloneEl.id = "fantasyMap";
|
cloneEl.id = "fantasyMap";
|
||||||
document.body.appendChild(cloneEl);
|
document.body.appendChild(cloneEl);
|
||||||
const clone = d3.select(cloneEl);
|
const clone = d3.select(cloneEl);
|
||||||
|
|
@ -286,13 +289,13 @@ async function getMapURL(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add relief icons
|
// add relief icons (from <use> elements – canvas <image> is excluded)
|
||||||
if (cloneEl.getElementById("terrain")) {
|
if (cloneEl.getElementById("terrain")) {
|
||||||
const uniqueElements = new Set();
|
const uniqueElements = new Set();
|
||||||
const terrainNodes = cloneEl.getElementById("terrain").childNodes;
|
const terrainUses = cloneEl.getElementById("terrain").querySelectorAll("use");
|
||||||
for (let i = 0; i < terrainNodes.length; i++) {
|
for (let i = 0; i < terrainUses.length; i++) {
|
||||||
const href = terrainNodes[i].getAttribute("href") || terrainNodes[i].getAttribute("xlink:href");
|
const href = terrainUses[i].getAttribute("href") || terrainUses[i].getAttribute("xlink:href");
|
||||||
uniqueElements.add(href);
|
if (href && href.startsWith("#")) uniqueElements.add(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defsRelief = svgDefs.getElementById("defs-relief");
|
const defsRelief = svgDefs.getElementById("defs-relief");
|
||||||
|
|
@ -424,7 +427,8 @@ async function getMapURL(
|
||||||
|
|
||||||
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
|
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
|
||||||
function removeUnusedElements(clone) {
|
function removeUnusedElements(clone) {
|
||||||
if (!terrain.selectAll("use").size()) clone.select("#defs-relief")?.remove();
|
// Check the clone (not the live terrain) so canvas-mode maps export correctly
|
||||||
|
if (!clone.select("#terrain use").size()) clone.select("#defs-relief")?.remove();
|
||||||
|
|
||||||
for (let empty = 1; empty; ) {
|
for (let empty = 1; empty; ) {
|
||||||
empty = 0;
|
empty = 0;
|
||||||
|
|
@ -583,8 +587,8 @@ function saveGeoJsonZones() {
|
||||||
// Handles multiple disconnected components and holes properly
|
// Handles multiple disconnected components and holes properly
|
||||||
function getZonePolygonCoordinates(zoneCells) {
|
function getZonePolygonCoordinates(zoneCells) {
|
||||||
const cellsInZone = new Set(zoneCells);
|
const cellsInZone = new Set(zoneCells);
|
||||||
const ofSameType = (cellId) => cellsInZone.has(cellId);
|
const ofSameType = cellId => cellsInZone.has(cellId);
|
||||||
const ofDifferentType = (cellId) => !cellsInZone.has(cellId);
|
const ofDifferentType = cellId => !cellsInZone.has(cellId);
|
||||||
|
|
||||||
const checkedCells = new Set();
|
const checkedCells = new Set();
|
||||||
const rings = []; // Array of LinearRings (each ring is an array of coordinates)
|
const rings = []; // Array of LinearRings (each ring is an array of coordinates)
|
||||||
|
|
@ -623,8 +627,8 @@ function saveGeoJsonZones() {
|
||||||
vertices,
|
vertices,
|
||||||
startingVertex,
|
startingVertex,
|
||||||
ofSameType,
|
ofSameType,
|
||||||
addToChecked: (cellId) => checkedCells.add(cellId),
|
addToChecked: cellId => checkedCells.add(cellId),
|
||||||
closeRing: false, // We'll close it manually after converting to coordinates
|
closeRing: false // We'll close it manually after converting to coordinates
|
||||||
});
|
});
|
||||||
|
|
||||||
if (vertexChain.length < 3) continue;
|
if (vertexChain.length < 3) continue;
|
||||||
|
|
|
||||||
|
|
@ -440,7 +440,12 @@ async function parseLoadedData(data, mapVersion) {
|
||||||
if (hasChildren(coordinates)) turnOn("toggleCoordinates");
|
if (hasChildren(coordinates)) turnOn("toggleCoordinates");
|
||||||
if (isVisible(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
|
if (isVisible(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
|
||||||
if (hasChildren(rivers)) turnOn("toggleRivers");
|
if (hasChildren(rivers)) turnOn("toggleRivers");
|
||||||
if (isVisible(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
|
if (isVisible(terrain) && hasChildren(terrain)) {
|
||||||
|
turnOn("toggleRelief");
|
||||||
|
}
|
||||||
|
// Migrate any legacy SVG <use> elements to canvas rendering
|
||||||
|
// (runs regardless of visibility to handle maps loaded with relief layer off)
|
||||||
|
if (typeof migrateReliefFromSvg === "function") migrateReliefFromSvg();
|
||||||
if (hasChildren(relig)) turnOn("toggleReligions");
|
if (hasChildren(relig)) turnOn("toggleReligions");
|
||||||
if (hasChildren(cults)) turnOn("toggleCultures");
|
if (hasChildren(cults)) turnOn("toggleCultures");
|
||||||
if (hasChildren(statesBody)) turnOn("toggleStates");
|
if (hasChildren(statesBody)) turnOn("toggleStates");
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,12 @@ async function saveMap(method) {
|
||||||
$(this).dialog("close");
|
$(this).dialog("close");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
position: { my: "center", at: "center", of: "svg" }
|
position: {my: "center", at: "center", of: "svg"}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareMapData() {
|
function prepareMapData() {
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||||
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||||
|
|
@ -79,7 +78,10 @@ function prepareMapData() {
|
||||||
const fonts = JSON.stringify(getUsedFonts(svg.node()));
|
const fonts = JSON.stringify(getUsedFonts(svg.node()));
|
||||||
|
|
||||||
// save svg
|
// save svg
|
||||||
|
// Temporarily inject <use> elements so the SVG snapshot includes relief icon data
|
||||||
|
if (typeof prepareReliefForSave === "function") prepareReliefForSave();
|
||||||
const cloneEl = document.getElementById("map").cloneNode(true);
|
const cloneEl = document.getElementById("map").cloneNode(true);
|
||||||
|
if (typeof restoreReliefAfterSave === "function") restoreReliefAfterSave();
|
||||||
|
|
||||||
// reset transform values to default
|
// reset transform values to default
|
||||||
cloneEl.setAttribute("width", graphWidth);
|
cloneEl.setAttribute("width", graphWidth);
|
||||||
|
|
@ -90,8 +92,8 @@ function prepareMapData() {
|
||||||
|
|
||||||
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
|
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
|
||||||
|
|
||||||
const { spacing, cellsX, cellsY, boundary, points, features, cellsDesired } = grid;
|
const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid;
|
||||||
const gridGeneral = JSON.stringify({ spacing, cellsX, cellsY, boundary, points, features, cellsDesired });
|
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired});
|
||||||
const packFeatures = JSON.stringify(pack.features);
|
const packFeatures = JSON.stringify(pack.features);
|
||||||
const cultures = JSON.stringify(pack.cultures);
|
const cultures = JSON.stringify(pack.cultures);
|
||||||
const states = JSON.stringify(pack.states);
|
const states = JSON.stringify(pack.states);
|
||||||
|
|
@ -165,14 +167,14 @@ function prepareMapData() {
|
||||||
|
|
||||||
// save map file to indexedDB
|
// save map file to indexedDB
|
||||||
async function saveToStorage(mapData, showTip = false) {
|
async function saveToStorage(mapData, showTip = false) {
|
||||||
const blob = new Blob([mapData], { type: "text/plain" });
|
const blob = new Blob([mapData], {type: "text/plain"});
|
||||||
await ldb.set("lastMap", blob);
|
await ldb.set("lastMap", blob);
|
||||||
showTip && tip("Map is saved to the browser storage", false, "success");
|
showTip && tip("Map is saved to the browser storage", false, "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
// download map file
|
// download map file
|
||||||
function saveToMachine(mapData, filename) {
|
function saveToMachine(mapData, filename) {
|
||||||
const blob = new Blob([mapData], { type: "text/plain" });
|
const blob = new Blob([mapData], {type: "text/plain"});
|
||||||
const URL = window.URL.createObjectURL(blob);
|
const URL = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ function clicked() {
|
||||||
else if (grand.id === "burgLabels") editBurg();
|
else if (grand.id === "burgLabels") editBurg();
|
||||||
else if (grand.id === "burgIcons") editBurg();
|
else if (grand.id === "burgIcons") editBurg();
|
||||||
else if (parent.id === "ice") editIce(el);
|
else if (parent.id === "ice") editIce(el);
|
||||||
else if (parent.id === "terrain") editReliefIcon();
|
|
||||||
else if (grand.id === "markers" || great.id === "markers") editMarker();
|
else if (grand.id === "markers" || great.id === "markers") editMarker();
|
||||||
else if (grand.id === "coastline") editCoastline();
|
else if (grand.id === "coastline") editCoastline();
|
||||||
else if (grand.id === "lakes") editLake();
|
else if (grand.id === "lakes") editLake();
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,6 @@ function showMapTooltip(point, e, i, g) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group === "terrain") return tip("Click to edit the Relief Icon");
|
|
||||||
|
|
||||||
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
|
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
|
||||||
const burgId = +path[path.length - 10].dataset.id;
|
const burgId = +path[path.length - 10].dataset.id;
|
||||||
if (burgId) {
|
if (burgId) {
|
||||||
|
|
@ -346,7 +344,8 @@ function getFriendlyHeight([x, y]) {
|
||||||
function getHeight(h, abs) {
|
function getHeight(h, abs) {
|
||||||
const unit = heightUnit.value;
|
const unit = heightUnit.value;
|
||||||
let unitRatio = 3.281; // default calculations are in feet
|
let unitRatio = 3.281; // default calculations are in feet
|
||||||
if (unit === "m") unitRatio = 1; // if meter
|
if (unit === "m")
|
||||||
|
unitRatio = 1; // if meter
|
||||||
else if (unit === "f") unitRatio = 0.5468; // if fathom
|
else if (unit === "f") unitRatio = 0.5468; // if fathom
|
||||||
|
|
||||||
let height = -990;
|
let height = -990;
|
||||||
|
|
|
||||||
|
|
@ -699,7 +699,16 @@ function toggleCompass(event) {
|
||||||
function toggleRelief(event) {
|
function toggleRelief(event) {
|
||||||
if (!layerIsOn("toggleRelief")) {
|
if (!layerIsOn("toggleRelief")) {
|
||||||
turnButtonOn("toggleRelief");
|
turnButtonOn("toggleRelief");
|
||||||
if (!terrain.selectAll("*").size()) drawReliefIcons();
|
if (!terrain.selectAll("*").size()) {
|
||||||
|
drawReliefIcons();
|
||||||
|
} else if (
|
||||||
|
terrain.selectAll("use").size() &&
|
||||||
|
!terrain.select("#terrainCanvasImage").size() &&
|
||||||
|
!terrain.select("#terrainGlFo").size()
|
||||||
|
) {
|
||||||
|
// Legacy SVG use elements present but no canvas/GL render yet – migrate now
|
||||||
|
if (typeof migrateReliefFromSvg === "function") migrateReliefFromSvg();
|
||||||
|
}
|
||||||
$("#terrain").fadeIn();
|
$("#terrain").fadeIn();
|
||||||
if (event && isCtrlClick(event)) editStyle("terrain");
|
if (event && isCtrlClick(event)) editStyle("terrain");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,16 @@ function editReliefIcon() {
|
||||||
closeDialogs(".stable");
|
closeDialogs(".stable");
|
||||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||||
|
|
||||||
|
// Switch from canvas image to editable SVG <use> elements
|
||||||
|
if (typeof enterReliefSvgEditMode === "function") enterReliefSvgEditMode();
|
||||||
|
|
||||||
terrain.selectAll("use").call(d3.drag().on("drag", dragReliefIcon)).classed("draggable", true);
|
terrain.selectAll("use").call(d3.drag().on("drag", dragReliefIcon)).classed("draggable", true);
|
||||||
elSelected = d3.select(d3.event.target);
|
|
||||||
|
// When called from the Tools button there is no d3 click event; fall back to the first <use>.
|
||||||
|
// When called from a map click, prefer the actual clicked element if it is a <use>.
|
||||||
|
const clickTarget = d3.event && d3.event.target;
|
||||||
|
const useTarget = clickTarget && clickTarget.tagName === "use" ? clickTarget : terrain.select("use").node();
|
||||||
|
elSelected = d3.select(useTarget);
|
||||||
|
|
||||||
restoreEditMode();
|
restoreEditMode();
|
||||||
updateReliefIconSelected();
|
updateReliefIconSelected();
|
||||||
|
|
@ -59,6 +67,7 @@ function editReliefIcon() {
|
||||||
function updateReliefIconSelected() {
|
function updateReliefIconSelected() {
|
||||||
const type = elSelected.attr("href") || elSelected.attr("data-type");
|
const type = elSelected.attr("href") || elSelected.attr("data-type");
|
||||||
const button = reliefIconsDiv.querySelector("svg[data-type='" + type + "']");
|
const button = reliefIconsDiv.querySelector("svg[data-type='" + type + "']");
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed"));
|
reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
button.classList.add("pressed");
|
button.classList.add("pressed");
|
||||||
|
|
@ -260,7 +269,9 @@ function editReliefIcon() {
|
||||||
const type = reliefIconsDiv.querySelector("svg.pressed")?.dataset.type;
|
const type = reliefIconsDiv.querySelector("svg.pressed")?.dataset.type;
|
||||||
selection = type ? terrain.selectAll("use[href='" + type + "']") : terrain.selectAll("use");
|
selection = type ? terrain.selectAll("use[href='" + type + "']") : terrain.selectAll("use");
|
||||||
const size = selection.size();
|
const size = selection.size();
|
||||||
alertMessage.innerHTML = type ? `Are you sure you want to remove all ${type} icons (${size})?` : `Are you sure you want to remove all icons (${size})?`;
|
alertMessage.innerHTML = type
|
||||||
|
? `Are you sure you want to remove all ${type} icons (${size})?`
|
||||||
|
: `Are you sure you want to remove all icons (${size})?`;
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#alert").dialog({
|
$("#alert").dialog({
|
||||||
|
|
@ -284,5 +295,7 @@ function editReliefIcon() {
|
||||||
removeCircle();
|
removeCircle();
|
||||||
unselect();
|
unselect();
|
||||||
clearMainTip();
|
clearMainTip();
|
||||||
|
// Read back edits and switch terrain to canvas rendering
|
||||||
|
if (typeof exitReliefSvgEditMode === "function") exitReliefSvgEditMode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
},
|
},
|
||||||
"#terrain": {
|
"#terrain": {
|
||||||
"opacity": null,
|
"opacity": null,
|
||||||
"set": "simple",
|
"set": "colored",
|
||||||
"size": 1,
|
"size": 1,
|
||||||
"density": 0.4,
|
"density": 0.4,
|
||||||
"filter": null,
|
"filter": null,
|
||||||
|
|
|
||||||
232
scripts/generate-relief-atlases.js
Normal file
232
scripts/generate-relief-atlases.js
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate pre-rasterised PNG sprite atlases for the three relief icon sets
|
||||||
|
* (simple, gray, colored) from the SVG <symbol> definitions in src/index.html.
|
||||||
|
*
|
||||||
|
* Output: public/images/relief/{simple,gray,colored}.png
|
||||||
|
*
|
||||||
|
* Each atlas is a grid of SPRITE_SIZE × SPRITE_SIZE tiles arranged
|
||||||
|
* left-to-right, top-to-bottom. Tile order matches the SET_SYMBOLS arrays
|
||||||
|
* in src/renderers/draw-relief-icons.ts — keep the two in sync.
|
||||||
|
*
|
||||||
|
* Rendering strategy: each symbol is placed as a standalone <svg> element
|
||||||
|
* positioned in a CSS grid inside a regular HTML page, then Playwright's
|
||||||
|
* native page.screenshot() captures the result. This avoids the headless-
|
||||||
|
* Chromium restriction that prevents SVG blob/data URLs from rendering when
|
||||||
|
* drawn to a <canvas> element.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/generate-relief-atlases.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const {chromium} = require("playwright");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Configuration — keep in sync with SET_SYMBOLS in draw-relief-icons.ts
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SPRITE_SIZE = 512; // px per symbol tile
|
||||||
|
|
||||||
|
const SET_SYMBOLS = {
|
||||||
|
simple: [
|
||||||
|
"relief-mount-1",
|
||||||
|
"relief-hill-1",
|
||||||
|
"relief-conifer-1",
|
||||||
|
"relief-deciduous-1",
|
||||||
|
"relief-acacia-1",
|
||||||
|
"relief-palm-1",
|
||||||
|
"relief-grass-1",
|
||||||
|
"relief-swamp-1",
|
||||||
|
"relief-dune-1"
|
||||||
|
],
|
||||||
|
gray: [
|
||||||
|
"relief-mount-2-bw",
|
||||||
|
"relief-mount-3-bw",
|
||||||
|
"relief-mount-4-bw",
|
||||||
|
"relief-mount-5-bw",
|
||||||
|
"relief-mount-6-bw",
|
||||||
|
"relief-mount-7-bw",
|
||||||
|
"relief-mountSnow-1-bw",
|
||||||
|
"relief-mountSnow-2-bw",
|
||||||
|
"relief-mountSnow-3-bw",
|
||||||
|
"relief-mountSnow-4-bw",
|
||||||
|
"relief-mountSnow-5-bw",
|
||||||
|
"relief-mountSnow-6-bw",
|
||||||
|
"relief-hill-2-bw",
|
||||||
|
"relief-hill-3-bw",
|
||||||
|
"relief-hill-4-bw",
|
||||||
|
"relief-hill-5-bw",
|
||||||
|
"relief-conifer-2-bw",
|
||||||
|
"relief-coniferSnow-1-bw",
|
||||||
|
"relief-swamp-2-bw",
|
||||||
|
"relief-swamp-3-bw",
|
||||||
|
"relief-cactus-1-bw",
|
||||||
|
"relief-cactus-2-bw",
|
||||||
|
"relief-cactus-3-bw",
|
||||||
|
"relief-deadTree-1-bw",
|
||||||
|
"relief-deadTree-2-bw",
|
||||||
|
"relief-vulcan-1-bw",
|
||||||
|
"relief-vulcan-2-bw",
|
||||||
|
"relief-vulcan-3-bw",
|
||||||
|
"relief-dune-2-bw",
|
||||||
|
"relief-grass-2-bw",
|
||||||
|
"relief-acacia-2-bw",
|
||||||
|
"relief-palm-2-bw",
|
||||||
|
"relief-deciduous-2-bw",
|
||||||
|
"relief-deciduous-3-bw"
|
||||||
|
],
|
||||||
|
colored: [
|
||||||
|
"relief-mount-2",
|
||||||
|
"relief-mount-3",
|
||||||
|
"relief-mount-4",
|
||||||
|
"relief-mount-5",
|
||||||
|
"relief-mount-6",
|
||||||
|
"relief-mount-7",
|
||||||
|
"relief-mountSnow-1",
|
||||||
|
"relief-mountSnow-2",
|
||||||
|
"relief-mountSnow-3",
|
||||||
|
"relief-mountSnow-4",
|
||||||
|
"relief-mountSnow-5",
|
||||||
|
"relief-mountSnow-6",
|
||||||
|
"relief-hill-2",
|
||||||
|
"relief-hill-3",
|
||||||
|
"relief-hill-4",
|
||||||
|
"relief-hill-5",
|
||||||
|
"relief-conifer-2",
|
||||||
|
"relief-coniferSnow-1",
|
||||||
|
"relief-swamp-2",
|
||||||
|
"relief-swamp-3",
|
||||||
|
"relief-cactus-1",
|
||||||
|
"relief-cactus-2",
|
||||||
|
"relief-cactus-3",
|
||||||
|
"relief-deadTree-1",
|
||||||
|
"relief-deadTree-2",
|
||||||
|
"relief-vulcan-1",
|
||||||
|
"relief-vulcan-2",
|
||||||
|
"relief-vulcan-3",
|
||||||
|
"relief-dune-2",
|
||||||
|
"relief-grass-2",
|
||||||
|
"relief-acacia-2",
|
||||||
|
"relief-palm-2",
|
||||||
|
"relief-deciduous-2",
|
||||||
|
"relief-deciduous-3"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Paths
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, "..");
|
||||||
|
const INDEX_HTML = path.join(ROOT, "src", "index.html");
|
||||||
|
const OUTPUT_DIR = path.join(ROOT, "public", "images", "relief");
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Extract every <symbol id="relief-*"> element from index.html
|
||||||
|
// Returns a map of symbolId → {viewBox, innerSvg}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function extractSymbols(html) {
|
||||||
|
const symbols = {};
|
||||||
|
// Match each <symbol ...>...</symbol> block.
|
||||||
|
// <symbol> elements never nest, so a lazy [\s\S]*? is safe here.
|
||||||
|
const re = /<symbol\s([^>]+)>([\s\S]*?)<\/symbol>/g;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(html)) !== null) {
|
||||||
|
const attrs = m[1];
|
||||||
|
const body = m[2];
|
||||||
|
|
||||||
|
const idM = attrs.match(/id="(relief-[^"]+)"/);
|
||||||
|
if (!idM) continue;
|
||||||
|
|
||||||
|
const vbM = attrs.match(/viewBox="([^"]+)"/);
|
||||||
|
symbols[idM[1]] = {
|
||||||
|
viewBox: vbM ? vbM[1] : "0 0 100 100",
|
||||||
|
body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return symbols;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = fs.readFileSync(INDEX_HTML, "utf8");
|
||||||
|
const allSymbols = extractSymbols(html);
|
||||||
|
|
||||||
|
const found = Object.keys(allSymbols).length;
|
||||||
|
if (found === 0) {
|
||||||
|
console.error("ERROR: no relief symbols found in src/index.html");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`Extracted ${found} relief symbols from index.html`);
|
||||||
|
|
||||||
|
fs.mkdirSync(OUTPUT_DIR, {recursive: true});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main — render each atlas via Playwright's native screenshot
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
for (const [set, ids] of Object.entries(SET_SYMBOLS)) {
|
||||||
|
const n = ids.length;
|
||||||
|
const cols = Math.ceil(Math.sqrt(n));
|
||||||
|
const rows = Math.ceil(n / cols);
|
||||||
|
const W = cols * SPRITE_SIZE;
|
||||||
|
const H = rows * SPRITE_SIZE;
|
||||||
|
|
||||||
|
// Build one SVG per symbol, absolutely positioned in a grid.
|
||||||
|
// Using standalone <svg> elements (not <use>) means the symbols are
|
||||||
|
// self-contained and render correctly regardless of CSP restrictions.
|
||||||
|
const svgTiles = ids
|
||||||
|
.map((symbolId, idx) => {
|
||||||
|
const sym = allSymbols[symbolId];
|
||||||
|
if (!sym) {
|
||||||
|
console.warn(` WARNING: symbol "${symbolId}" not found — tile will be blank`);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const col = idx % cols;
|
||||||
|
const row = Math.floor(idx / cols);
|
||||||
|
const x = col * SPRITE_SIZE;
|
||||||
|
const y = row * SPRITE_SIZE;
|
||||||
|
return (
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg"` +
|
||||||
|
` style="position:absolute;left:${x}px;top:${y}px"` +
|
||||||
|
` width="${SPRITE_SIZE}" height="${SPRITE_SIZE}"` +
|
||||||
|
` viewBox="${sym.viewBox}">${sym.body}</svg>`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const pageHtml =
|
||||||
|
`<!DOCTYPE html><html><body style="margin:0;padding:0;` +
|
||||||
|
`width:${W}px;height:${H}px;position:relative;overflow:hidden;` +
|
||||||
|
`background:transparent">` +
|
||||||
|
svgTiles +
|
||||||
|
`</body></html>`;
|
||||||
|
|
||||||
|
await page.setViewportSize({width: W, height: H});
|
||||||
|
await page.setContent(pageHtml, {waitUntil: "load"});
|
||||||
|
|
||||||
|
const buffer = await page.screenshot({
|
||||||
|
type: "png",
|
||||||
|
fullPage: false,
|
||||||
|
clip: {x: 0, y: 0, width: W, height: H},
|
||||||
|
omitBackground: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const outFile = path.join(OUTPUT_DIR, `${set}.png`);
|
||||||
|
fs.writeFileSync(outFile, buffer);
|
||||||
|
console.log(`✓ ${set}.png — ${cols}×${rows} tiles (${W}×${H} px, ${(buffer.length / 1024).toFixed(0)} KB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log("\nDone. Files saved to public/images/relief/");
|
||||||
|
})().catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -2140,6 +2140,9 @@
|
||||||
<button id="editProvincesButton" data-tip="Click to open Provinces Editor" data-shortcut="Shift + P">
|
<button id="editProvincesButton" data-tip="Click to open Provinces Editor" data-shortcut="Shift + P">
|
||||||
Provinces
|
Provinces
|
||||||
</button>
|
</button>
|
||||||
|
<button id="editReliefButton" data-tip="Click to open Relief Icons Editor" onclick="editReliefIcon()">
|
||||||
|
Relief
|
||||||
|
</button>
|
||||||
<button id="editReligions" data-tip="Click to open Religions Editor" data-shortcut="Shift + R">
|
<button id="editReligions" data-tip="Click to open Religions Editor" data-shortcut="Shift + R">
|
||||||
Religions
|
Religions
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,430 @@
|
||||||
import { extent, polygonContains } from "d3";
|
import {extent, polygonContains} from "d3";
|
||||||
import { minmax, rand, rn } from "../utils";
|
import * as THREE from "three";
|
||||||
|
import {minmax, rand, rn} from "../utils";
|
||||||
|
|
||||||
interface ReliefIcon {
|
interface ReliefIcon {
|
||||||
i: string;
|
i: string; // e.g. "#relief-mount-1"
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
s: number;
|
s: number; // size (width = height in map units)
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var drawReliefIcons: () => void;
|
var drawReliefIcons: () => void;
|
||||||
var terrain: import("d3").Selection<SVGGElement, unknown, null, undefined>;
|
var terrain: import("d3").Selection<SVGGElement, unknown, null, undefined>;
|
||||||
var getPackPolygon: (i: number) => [number, number][];
|
var getPackPolygon: (i: number) => [number, number][];
|
||||||
|
var graphWidth: number;
|
||||||
|
var graphHeight: number;
|
||||||
|
var poissonDiscSampler: (x0: number, y0: number, x1: number, y1: number, r: number) => Iterable<[number, number]>;
|
||||||
|
|
||||||
|
var renderReliefIcons: () => void;
|
||||||
|
var enterReliefSvgEditMode: () => void;
|
||||||
|
var exitReliefSvgEditMode: () => void;
|
||||||
|
var prepareReliefForSave: () => void;
|
||||||
|
var restoreReliefAfterSave: () => void;
|
||||||
|
var migrateReliefFromSvg: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module state
|
||||||
|
let reliefIconData: ReliefIcon[] = [];
|
||||||
|
let svgEditMode = false;
|
||||||
|
let fo: SVGForeignObjectElement | null = null;
|
||||||
|
let renderer: any = null; // THREE.WebGLRenderer
|
||||||
|
let camera: any = null; // THREE.OrthographicCamera
|
||||||
|
let scene: any = null; // THREE.Scene
|
||||||
|
|
||||||
|
const textureCache = new Map<string, any>(); // set name → THREE.Texture
|
||||||
|
|
||||||
|
const RELIEF_SYMBOLS: Record<string, string[]> = {
|
||||||
|
simple: [
|
||||||
|
"relief-mount-1",
|
||||||
|
"relief-hill-1",
|
||||||
|
"relief-conifer-1",
|
||||||
|
"relief-deciduous-1",
|
||||||
|
"relief-acacia-1",
|
||||||
|
"relief-palm-1",
|
||||||
|
"relief-grass-1",
|
||||||
|
"relief-swamp-1",
|
||||||
|
"relief-dune-1"
|
||||||
|
],
|
||||||
|
gray: [
|
||||||
|
"relief-mount-2-bw",
|
||||||
|
"relief-mount-3-bw",
|
||||||
|
"relief-mount-4-bw",
|
||||||
|
"relief-mount-5-bw",
|
||||||
|
"relief-mount-6-bw",
|
||||||
|
"relief-mount-7-bw",
|
||||||
|
"relief-mountSnow-1-bw",
|
||||||
|
"relief-mountSnow-2-bw",
|
||||||
|
"relief-mountSnow-3-bw",
|
||||||
|
"relief-mountSnow-4-bw",
|
||||||
|
"relief-mountSnow-5-bw",
|
||||||
|
"relief-mountSnow-6-bw",
|
||||||
|
"relief-hill-2-bw",
|
||||||
|
"relief-hill-3-bw",
|
||||||
|
"relief-hill-4-bw",
|
||||||
|
"relief-hill-5-bw",
|
||||||
|
"relief-conifer-2-bw",
|
||||||
|
"relief-coniferSnow-1-bw",
|
||||||
|
"relief-swamp-2-bw",
|
||||||
|
"relief-swamp-3-bw",
|
||||||
|
"relief-cactus-1-bw",
|
||||||
|
"relief-cactus-2-bw",
|
||||||
|
"relief-cactus-3-bw",
|
||||||
|
"relief-deadTree-1-bw",
|
||||||
|
"relief-deadTree-2-bw",
|
||||||
|
"relief-vulcan-1-bw",
|
||||||
|
"relief-vulcan-2-bw",
|
||||||
|
"relief-vulcan-3-bw",
|
||||||
|
"relief-dune-2-bw",
|
||||||
|
"relief-grass-2-bw",
|
||||||
|
"relief-acacia-2-bw",
|
||||||
|
"relief-palm-2-bw",
|
||||||
|
"relief-deciduous-2-bw",
|
||||||
|
"relief-deciduous-3-bw"
|
||||||
|
],
|
||||||
|
colored: [
|
||||||
|
"relief-mount-2",
|
||||||
|
"relief-mount-3",
|
||||||
|
"relief-mount-4",
|
||||||
|
"relief-mount-5",
|
||||||
|
"relief-mount-6",
|
||||||
|
"relief-mount-7",
|
||||||
|
"relief-mountSnow-1",
|
||||||
|
"relief-mountSnow-2",
|
||||||
|
"relief-mountSnow-3",
|
||||||
|
"relief-mountSnow-4",
|
||||||
|
"relief-mountSnow-5",
|
||||||
|
"relief-mountSnow-6",
|
||||||
|
"relief-hill-2",
|
||||||
|
"relief-hill-3",
|
||||||
|
"relief-hill-4",
|
||||||
|
"relief-hill-5",
|
||||||
|
"relief-conifer-2",
|
||||||
|
"relief-coniferSnow-1",
|
||||||
|
"relief-swamp-2",
|
||||||
|
"relief-swamp-3",
|
||||||
|
"relief-cactus-1",
|
||||||
|
"relief-cactus-2",
|
||||||
|
"relief-cactus-3",
|
||||||
|
"relief-deadTree-1",
|
||||||
|
"relief-deadTree-2",
|
||||||
|
"relief-vulcan-1",
|
||||||
|
"relief-vulcan-2",
|
||||||
|
"relief-vulcan-3",
|
||||||
|
"relief-dune-2",
|
||||||
|
"relief-grass-2",
|
||||||
|
"relief-acacia-2",
|
||||||
|
"relief-palm-2",
|
||||||
|
"relief-deciduous-2",
|
||||||
|
"relief-deciduous-3"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveSprite(symbolHref: string) {
|
||||||
|
const id = symbolHref.startsWith("#") ? symbolHref.slice(1) : symbolHref;
|
||||||
|
for (const [set, ids] of Object.entries(RELIEF_SYMBOLS)) {
|
||||||
|
const idx = ids.indexOf(id);
|
||||||
|
if (idx !== -1) return {set, tileIndex: idx};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Relief: unknown symbol href "${symbolHref}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTexture(set: string): Promise<any> {
|
||||||
|
if (textureCache.has(set)) return Promise.resolve(textureCache.get(set));
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const loader = new THREE.TextureLoader();
|
||||||
|
loader.load(
|
||||||
|
`images/relief/${set}.png`,
|
||||||
|
texture => {
|
||||||
|
texture.flipY = false;
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
|
texture.magFilter = THREE.LinearFilter;
|
||||||
|
texture.generateMipmaps = true;
|
||||||
|
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
|
textureCache.set(set, texture);
|
||||||
|
resolve(texture);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
() => {
|
||||||
|
console.warn(`Relief: atlas not found for "${set}". Run: npm run generate-atlases`);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preloadTextures(): Promise<void> {
|
||||||
|
const sets = new Set<string>();
|
||||||
|
for (const r of reliefIconData) sets.add(resolveSprite(r.i).set);
|
||||||
|
await Promise.all([...sets].map(loadTexture));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRenderer(): boolean {
|
||||||
|
if (renderer) {
|
||||||
|
// Recover from WebGL context loss (can happen when canvas is detached from DOM)
|
||||||
|
if (renderer.getContext().isContextLost()) {
|
||||||
|
renderer.forceContextRestore();
|
||||||
|
renderer.dispose();
|
||||||
|
renderer = null;
|
||||||
|
camera = null;
|
||||||
|
scene = null;
|
||||||
|
disposeTextureCache();
|
||||||
|
// fall through to recreate
|
||||||
|
} else {
|
||||||
|
if (fo && !fo.isConnected) terrain.node()!.appendChild(fo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// foreignObject hosts the WebGL canvas inside the SVG.
|
||||||
|
// Dimensions are set here so the browser can start compositing before renderFrame runs.
|
||||||
|
fo = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject") as unknown as SVGForeignObjectElement;
|
||||||
|
fo.id = "terrainFo";
|
||||||
|
fo.setAttribute("x", "0");
|
||||||
|
fo.setAttribute("y", "0");
|
||||||
|
fo.setAttribute("width", String(graphWidth));
|
||||||
|
fo.setAttribute("height", String(graphHeight));
|
||||||
|
|
||||||
|
// IMPORTANT: use document.createElement, not createElementNS.
|
||||||
|
// createElementNS with XHTML namespace can produce an Element without getContext().
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.id = "terrainGlCanvas";
|
||||||
|
(fo as unknown as Element).appendChild(canvas);
|
||||||
|
terrain.node()!.appendChild(fo);
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderer = new THREE.WebGLRenderer({canvas, alpha: true, antialias: false, preserveDrawingBuffer: true});
|
||||||
|
renderer.setClearColor(0x000000, 0);
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio || 1);
|
||||||
|
// setSize sets canvas.width/height (physical) and canvas.style.width/height (CSS px).
|
||||||
|
// We override the CSS size to 100% so the canvas always fills the foreignObject.
|
||||||
|
renderer.setSize(graphWidth, graphHeight);
|
||||||
|
canvas.style.cssText = "display:block;pointer-events:none;position:absolute;top:0;left:0;width:100%;height:100%;";
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Relief: WebGL init failed", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera in SVG coordinate space: (left, right, top, bottom, near, far).
|
||||||
|
// top=0, bottom=H puts map y=0 at screen-top and map y=H at screen-bottom.
|
||||||
|
camera = new THREE.OrthographicCamera(0, graphWidth, 0, graphHeight, -1, 1);
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Scene / geometry ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a single BufferGeometry with all icon quads for one atlas set.
|
||||||
|
* Geometry order matches the painter's-order sort of reliefIconData,
|
||||||
|
* so depth is correct within the set without needing depth testing.
|
||||||
|
*
|
||||||
|
* UV layout (texture.flipY = false means v=0 is top of image):
|
||||||
|
* u = col/cols … (col+1)/cols
|
||||||
|
* v = row/rows … (row+1)/rows
|
||||||
|
*/
|
||||||
|
function buildSetMesh(icons: ReliefIcon[], set: string, texture: any): any {
|
||||||
|
const ids = RELIEF_SYMBOLS[set] ?? [];
|
||||||
|
const n = ids.length || 1;
|
||||||
|
const cols = Math.ceil(Math.sqrt(n));
|
||||||
|
const rows = Math.ceil(n / cols);
|
||||||
|
|
||||||
|
const positions = new Float32Array(icons.length * 4 * 3);
|
||||||
|
const uvs = new Float32Array(icons.length * 4 * 2);
|
||||||
|
const indices = new Uint32Array(icons.length * 6);
|
||||||
|
|
||||||
|
let vi = 0,
|
||||||
|
ii = 0;
|
||||||
|
for (const r of icons) {
|
||||||
|
const {tileIndex} = resolveSprite(r.i);
|
||||||
|
const col = tileIndex % cols;
|
||||||
|
const row = Math.floor(tileIndex / cols);
|
||||||
|
const u0 = col / cols,
|
||||||
|
u1 = (col + 1) / cols;
|
||||||
|
const v0 = row / rows,
|
||||||
|
v1 = (row + 1) / rows;
|
||||||
|
const x0 = r.x,
|
||||||
|
x1 = r.x + r.s;
|
||||||
|
const y0 = r.y,
|
||||||
|
y1 = r.y + r.s;
|
||||||
|
|
||||||
|
const base = vi;
|
||||||
|
// TL
|
||||||
|
positions.set([x0, y0, 0], vi * 3);
|
||||||
|
uvs.set([u0, v0], vi * 2);
|
||||||
|
vi++;
|
||||||
|
// TR
|
||||||
|
positions.set([x1, y0, 0], vi * 3);
|
||||||
|
uvs.set([u1, v0], vi * 2);
|
||||||
|
vi++;
|
||||||
|
// BL
|
||||||
|
positions.set([x0, y1, 0], vi * 3);
|
||||||
|
uvs.set([u0, v1], vi * 2);
|
||||||
|
vi++;
|
||||||
|
// BR
|
||||||
|
positions.set([x1, y1, 0], vi * 3);
|
||||||
|
uvs.set([u1, v1], vi * 2);
|
||||||
|
vi++;
|
||||||
|
|
||||||
|
// Two CCW triangles (winding doesn't matter with DoubleSide, but consistent)
|
||||||
|
indices.set([base, base + 1, base + 3, base, base + 3, base + 2], ii);
|
||||||
|
ii += 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
const geo = new THREE.BufferGeometry();
|
||||||
|
geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||||
|
geo.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
||||||
|
geo.setIndex(new THREE.BufferAttribute(indices, 1));
|
||||||
|
|
||||||
|
const mat = new THREE.MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
transparent: true,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return new THREE.Mesh(geo, mat);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeTextureCache(): void {
|
||||||
|
for (const tex of textureCache.values()) tex?.dispose();
|
||||||
|
textureCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeScene(): void {
|
||||||
|
if (!scene) return;
|
||||||
|
while (scene.children.length) {
|
||||||
|
const mesh = scene.children[0];
|
||||||
|
scene.remove(mesh);
|
||||||
|
mesh.geometry?.dispose();
|
||||||
|
// dispose material but NOT the texture (shared in textureCache, explicitly cleared separately)
|
||||||
|
if (mesh.material) {
|
||||||
|
mesh.material.map = null;
|
||||||
|
mesh.material.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildScene(): void {
|
||||||
|
if (!scene) return;
|
||||||
|
disposeScene();
|
||||||
|
|
||||||
|
// Group icons by set (normally all icons belong to the same set)
|
||||||
|
const bySet = new Map<string, ReliefIcon[]>();
|
||||||
|
for (const r of reliefIconData) {
|
||||||
|
const {set} = resolveSprite(r.i);
|
||||||
|
let arr = bySet.get(set);
|
||||||
|
if (!arr) {
|
||||||
|
arr = [];
|
||||||
|
bySet.set(set, arr);
|
||||||
|
}
|
||||||
|
arr.push(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [set, icons] of bySet) {
|
||||||
|
const texture = textureCache.get(set);
|
||||||
|
if (!texture) continue;
|
||||||
|
scene.add(buildSetMesh(icons, set, texture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFrame(): void {
|
||||||
|
if (!renderer || !camera || !scene || !fo) return;
|
||||||
|
|
||||||
|
const x = -viewX / scale;
|
||||||
|
const y = -viewY / scale;
|
||||||
|
const w = graphWidth / scale;
|
||||||
|
const h = graphHeight / scale;
|
||||||
|
|
||||||
|
// Position the foreignObject to cancel the D3 zoom transform applied to #viewbox.
|
||||||
|
fo.setAttribute("x", String(x));
|
||||||
|
fo.setAttribute("y", String(y));
|
||||||
|
fo.setAttribute("width", String(w));
|
||||||
|
fo.setAttribute("height", String(h));
|
||||||
|
|
||||||
|
// Camera frustum = the map region currently visible through the fo aperture
|
||||||
|
camera.left = x;
|
||||||
|
camera.right = x + w;
|
||||||
|
camera.top = y;
|
||||||
|
camera.bottom = y + h;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterSvgEditMode(): void {
|
||||||
|
if (svgEditMode) return;
|
||||||
|
svgEditMode = true;
|
||||||
|
terrain
|
||||||
|
.node()!
|
||||||
|
.insertAdjacentHTML(
|
||||||
|
"afterbegin",
|
||||||
|
reliefIconData.map(r => `<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`).join("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitSvgEditMode(): void {
|
||||||
|
if (!svgEditMode) return;
|
||||||
|
reliefIconData = [];
|
||||||
|
terrain.selectAll<SVGUseElement, unknown>("use").each(function () {
|
||||||
|
const href = this.getAttribute("href") || (this as any).getAttribute("xlink:href") || "";
|
||||||
|
reliefIconData.push({
|
||||||
|
i: href,
|
||||||
|
x: +this.getAttribute("x")!,
|
||||||
|
y: +this.getAttribute("y")!,
|
||||||
|
s: +this.getAttribute("width")!
|
||||||
|
});
|
||||||
|
});
|
||||||
|
terrain.selectAll("use").remove();
|
||||||
|
svgEditMode = false;
|
||||||
|
preloadTextures().then(() => {
|
||||||
|
buildScene();
|
||||||
|
renderFrame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareReliefForSave(): void {
|
||||||
|
if (svgEditMode) return;
|
||||||
|
terrain
|
||||||
|
.node()!
|
||||||
|
.insertAdjacentHTML(
|
||||||
|
"afterbegin",
|
||||||
|
reliefIconData.map(r => `<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`).join("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreReliefAfterSave(): void {
|
||||||
|
if (!svgEditMode) terrain.selectAll("use").remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
const reliefIconsRenderer = (): void => {
|
const reliefIconsRenderer = (): void => {
|
||||||
TIME && console.time("drawRelief");
|
TIME && console.time("drawRelief");
|
||||||
|
|
||||||
terrain.selectAll("*").remove();
|
terrain.selectAll("*").remove();
|
||||||
|
disposeTextureCache();
|
||||||
|
disposeScene();
|
||||||
|
reliefIconData = [];
|
||||||
|
svgEditMode = false;
|
||||||
|
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
const density = Number(terrain.attr("density")) || 0.4;
|
const density = Number(terrain.attr("density")) || 0.4;
|
||||||
const size = 2 * (Number(terrain.attr("size")) || 1);
|
const size = 2 * (Number(terrain.attr("size")) || 1);
|
||||||
const mod = 0.2 * size; // size modifier
|
const mod = 0.2 * size;
|
||||||
const relief: ReliefIcon[] = [];
|
const relief: ReliefIcon[] = [];
|
||||||
|
|
||||||
for (const i of cells.i) {
|
for (const i of cells.i) {
|
||||||
const height = cells.h[i];
|
const height = cells.h[i];
|
||||||
if (height < 20) continue; // no icons on water
|
if (height < 20 || cells.r[i]) continue;
|
||||||
if (cells.r[i]) continue; // no icons on rivers
|
|
||||||
const biome = cells.biome[i];
|
const biome = cells.biome[i];
|
||||||
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue; // no icons for this biome
|
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue;
|
||||||
|
|
||||||
const polygon = getPackPolygon(i);
|
const polygon = getPackPolygon(i);
|
||||||
const [minX, maxX] = extent(polygon, (p) => p[0]) as [number, number];
|
const [minX, maxX] = extent(polygon, p => p[0]) as [number, number];
|
||||||
const [minY, maxY] = extent(polygon, (p) => p[1]) as [number, number];
|
const [minY, maxY] = extent(polygon, p => p[1]) as [number, number];
|
||||||
|
|
||||||
if (height < 50) placeBiomeIcons();
|
if (height < 50) placeBiomeIcons();
|
||||||
else placeReliefIcons();
|
else placeReliefIcons();
|
||||||
|
|
@ -42,45 +433,21 @@ const reliefIconsRenderer = (): void => {
|
||||||
const iconsDensity = biomesData.iconsDensity[biome] / 100;
|
const iconsDensity = biomesData.iconsDensity[biome] / 100;
|
||||||
const radius = 2 / iconsDensity / density;
|
const radius = 2 / iconsDensity / density;
|
||||||
if (Math.random() > iconsDensity * 10) return;
|
if (Math.random() > iconsDensity * 10) return;
|
||||||
|
for (const [cx, cy] of window.poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
|
||||||
for (const [cx, cy] of window.poissonDiscSampler(
|
|
||||||
minX,
|
|
||||||
minY,
|
|
||||||
maxX,
|
|
||||||
maxY,
|
|
||||||
radius,
|
|
||||||
)) {
|
|
||||||
if (!polygonContains(polygon, [cx, cy])) continue;
|
if (!polygonContains(polygon, [cx, cy])) continue;
|
||||||
let h = (4 + Math.random()) * size;
|
let h = (4 + Math.random()) * size;
|
||||||
const icon = getBiomeIcon(i, biomesData.icons[biome]);
|
const icon = getBiomeIcon(i, biomesData.icons[biome]);
|
||||||
if (icon === "#relief-grass-1") h *= 1.2;
|
if (icon === "#relief-grass-1") h *= 1.2;
|
||||||
relief.push({
|
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
|
||||||
i: icon,
|
|
||||||
x: rn(cx - h, 2),
|
|
||||||
y: rn(cy - h, 2),
|
|
||||||
s: rn(h * 2, 2),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function placeReliefIcons(): void {
|
function placeReliefIcons(): void {
|
||||||
const radius = 2 / density;
|
const radius = 2 / density;
|
||||||
const [icon, h] = getReliefIcon(i, height);
|
const [icon, h] = getReliefIcon(i, height);
|
||||||
|
for (const [cx, cy] of window.poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
|
||||||
for (const [cx, cy] of window.poissonDiscSampler(
|
|
||||||
minX,
|
|
||||||
minY,
|
|
||||||
maxX,
|
|
||||||
maxY,
|
|
||||||
radius,
|
|
||||||
)) {
|
|
||||||
if (!polygonContains(polygon, [cx, cy])) continue;
|
if (!polygonContains(polygon, [cx, cy])) continue;
|
||||||
relief.push({
|
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
|
||||||
i: icon,
|
|
||||||
x: rn(cx - h, 2),
|
|
||||||
y: rn(cy - h, 2),
|
|
||||||
s: rn(h * 2, 2),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,19 +459,22 @@ const reliefIconsRenderer = (): void => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort relief icons by y+size
|
|
||||||
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
|
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
|
||||||
|
reliefIconData = relief;
|
||||||
const reliefHTML: string[] = [];
|
|
||||||
for (const r of relief) {
|
|
||||||
reliefHTML.push(
|
|
||||||
`<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
terrain.html(reliefHTML.join(""));
|
|
||||||
|
|
||||||
TIME && console.timeEnd("drawRelief");
|
TIME && console.timeEnd("drawRelief");
|
||||||
|
|
||||||
|
if (reliefIconData.length) {
|
||||||
|
if (ensureRenderer()) {
|
||||||
|
preloadTextures().then(() => {
|
||||||
|
buildScene();
|
||||||
|
renderFrame();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
WARN && console.warn("Relief: WebGL renderer failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getBiomeIcon(cellIndex: number, b: string[]): string {
|
function getBiomeIcon(cellIndex: number, b: string[]): string {
|
||||||
let type = b[Math.floor(Math.random() * b.length)];
|
let type = b[Math.floor(Math.random() * b.length)];
|
||||||
const temp = grid.cells.temp[pack.cells.g[cellIndex]];
|
const temp = grid.cells.temp[pack.cells.g[cellIndex]];
|
||||||
|
|
@ -130,35 +500,37 @@ const reliefIconsRenderer = (): void => {
|
||||||
return rand(1, 3);
|
return rand(1, 3);
|
||||||
case "deadTree":
|
case "deadTree":
|
||||||
return rand(1, 2);
|
return rand(1, 2);
|
||||||
|
case "vulcan":
|
||||||
|
return rand(1, 3);
|
||||||
|
case "deciduous":
|
||||||
|
return rand(2, 3);
|
||||||
default:
|
default:
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOldIcon(type: string): string {
|
function getOldIcon(type: string): string {
|
||||||
switch (type) {
|
const map: Record<string, string> = {
|
||||||
case "mountSnow":
|
mountSnow: "mount",
|
||||||
return "mount";
|
vulcan: "mount",
|
||||||
case "vulcan":
|
coniferSnow: "conifer",
|
||||||
return "mount";
|
cactus: "dune",
|
||||||
case "coniferSnow":
|
deadTree: "dune"
|
||||||
return "conifer";
|
};
|
||||||
case "cactus":
|
return map[type] ?? type;
|
||||||
return "dune";
|
|
||||||
case "deadTree":
|
|
||||||
return "dune";
|
|
||||||
default:
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIcon(type: string): string {
|
function getIcon(type: string): string {
|
||||||
const set = terrain.attr("set") || "simple";
|
const set = terrain.attr("set") || "simple";
|
||||||
if (set === "simple") return `#relief-${getOldIcon(type)}-1`;
|
|
||||||
if (set === "colored") return `#relief-${type}-${getVariant(type)}`;
|
if (set === "colored") return `#relief-${type}-${getVariant(type)}`;
|
||||||
if (set === "gray") return `#relief-${type}-${getVariant(type)}-bw`;
|
if (set === "gray") return `#relief-${type}-${getVariant(type)}-bw`;
|
||||||
return `#relief-${getOldIcon(type)}-1`; // simple
|
return `#relief-${getOldIcon(type)}-1`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.renderReliefIcons = renderFrame;
|
||||||
window.drawReliefIcons = reliefIconsRenderer;
|
window.drawReliefIcons = reliefIconsRenderer;
|
||||||
|
window.enterReliefSvgEditMode = enterSvgEditMode;
|
||||||
|
window.exitReliefSvgEditMode = exitSvgEditMode;
|
||||||
|
window.prepareReliefForSave = prepareReliefForSave;
|
||||||
|
window.restoreReliefAfterSave = restoreReliefAfterSave;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Selection } from "d3";
|
import type {Selection} from "d3";
|
||||||
import type { NameBase } from "../modules/names-generator";
|
import type {NameBase} from "../modules/names-generator";
|
||||||
import type { PackedGraph } from "./PackedGraph";
|
import type {PackedGraph} from "./PackedGraph";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var seed: string;
|
var seed: string;
|
||||||
|
|
@ -11,7 +11,7 @@ declare global {
|
||||||
var TIME: boolean;
|
var TIME: boolean;
|
||||||
var WARN: boolean;
|
var WARN: boolean;
|
||||||
var ERROR: boolean;
|
var ERROR: boolean;
|
||||||
var DEBUG: { stateLabels?: boolean; [key: string]: boolean | undefined };
|
var DEBUG: {stateLabels?: boolean; [key: string]: boolean | undefined};
|
||||||
var options: any;
|
var options: any;
|
||||||
|
|
||||||
var heightmapTemplates: any;
|
var heightmapTemplates: any;
|
||||||
|
|
@ -66,9 +66,9 @@ declare global {
|
||||||
};
|
};
|
||||||
var notes: any[];
|
var notes: any[];
|
||||||
var style: {
|
var style: {
|
||||||
burgLabels: { [key: string]: { [key: string]: string } };
|
burgLabels: {[key: string]: {[key: string]: string}};
|
||||||
burgIcons: { [key: string]: { [key: string]: string } };
|
burgIcons: {[key: string]: {[key: string]: string}};
|
||||||
anchors: { [key: string]: { [key: string]: string } };
|
anchors: {[key: string]: {[key: string]: string}};
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -81,12 +81,14 @@ declare global {
|
||||||
message: string,
|
message: string,
|
||||||
autoHide?: boolean,
|
autoHide?: boolean,
|
||||||
type?: "info" | "warn" | "error" | "success",
|
type?: "info" | "warn" | "error" | "success",
|
||||||
timeout?: number,
|
timeout?: number
|
||||||
) => void;
|
) => void;
|
||||||
var locked: (settingId: string) => boolean;
|
var locked: (settingId: string) => boolean;
|
||||||
var unlock: (settingId: string) => void;
|
var unlock: (settingId: string) => void;
|
||||||
var $: (selector: any) => any;
|
var $: (selector: any) => any;
|
||||||
var scale: number;
|
var scale: number;
|
||||||
|
var viewX: number;
|
||||||
|
var viewY: number;
|
||||||
var changeFont: () => void;
|
var changeFont: () => void;
|
||||||
var getFriendlyHeight: (coords: [number, number]) => string;
|
var getFriendlyHeight: (coords: [number, number]) => string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue