mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-23 07:37:24 +01:00
Refactor relief rendering and generation logic
- Migrate relief icon rendering from SVG to WebGL for improved performance. - Introduce a new relief generator module to handle relief icon creation. - Update event listeners in relief editor to use a consistent `byId` method. - Synchronize relief data with the current SVG DOM when exiting edit mode. - Enhance relief icon management by integrating new utility functions for generating and resolving relief icons. - Clean up legacy code and improve overall structure for better maintainability.
This commit is contained in:
parent
cbed9af783
commit
bf22c5eaf6
11 changed files with 504 additions and 385 deletions
|
|
@ -459,7 +459,7 @@ function handleZoom(isScaleChanged, isPositionChanged) {
|
|||
fitScaleBar(scaleBar, svgWidth, svgHeight);
|
||||
}
|
||||
|
||||
if (layerIsOn("toggleRelief")) renderReliefIcons();
|
||||
if (layerIsOn("toggleRelief")) rerenderReliefIcons();
|
||||
|
||||
// zoom image converter overlay
|
||||
if (customization === 1) {
|
||||
|
|
|
|||
|
|
@ -323,7 +323,8 @@ function editBiomes() {
|
|||
}
|
||||
|
||||
function regenerateIcons() {
|
||||
drawReliefIcons();
|
||||
pack.relief = [];
|
||||
drawRelief();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ function drawLayers() {
|
|||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
if (layerIsOn("toggleCompass")) compass.style("display", "block");
|
||||
if (layerIsOn("toggleRivers")) drawRivers();
|
||||
if (layerIsOn("toggleRelief")) drawReliefIcons();
|
||||
if (layerIsOn("toggleRelief")) drawRelief();
|
||||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
if (layerIsOn("toggleCultures")) drawCultures();
|
||||
if (layerIsOn("toggleStates")) drawStates();
|
||||
|
|
@ -699,22 +699,12 @@ function toggleCompass(event) {
|
|||
function toggleRelief(event) {
|
||||
if (!layerIsOn("toggleRelief")) {
|
||||
turnButtonOn("toggleRelief");
|
||||
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();
|
||||
drawRelief();
|
||||
if (event && isCtrlClick(event)) editStyle("terrain");
|
||||
} else {
|
||||
if (event && isCtrlClick(event)) return editStyle("terrain");
|
||||
$("#terrain").fadeOut();
|
||||
turnButtonOff("toggleRelief");
|
||||
undrawRelief();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
function editReliefIcon() {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
|
||||
// Switch from WebGL to editable SVG <use> elements
|
||||
undrawRelief();
|
||||
drawRelief("svg");
|
||||
|
||||
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);
|
||||
|
||||
// When called from the Tools button there is no d3 click event; fall back to the first <use>.
|
||||
|
|
@ -31,30 +32,42 @@ function editReliefIcon() {
|
|||
modules.editReliefIcon = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("reliefIndividual").addEventListener("click", enterIndividualMode);
|
||||
document.getElementById("reliefBulkAdd").addEventListener("click", enterBulkAddMode);
|
||||
document.getElementById("reliefBulkRemove").addEventListener("click", enterBulkRemoveMode);
|
||||
byId("reliefIndividual").on("click", enterIndividualMode);
|
||||
byId("reliefBulkAdd").on("click", enterBulkAddMode);
|
||||
byId("reliefBulkRemove").on("click", enterBulkRemoveMode);
|
||||
|
||||
document.getElementById("reliefSize").addEventListener("input", changeIconSize);
|
||||
document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize);
|
||||
document.getElementById("reliefEditorSet").addEventListener("change", changeIconsSet);
|
||||
reliefIconsDiv.querySelectorAll("svg").forEach(el => el.addEventListener("click", changeIcon));
|
||||
byId("reliefSize").on("input", changeIconSize);
|
||||
byId("reliefSizeNumber").on("input", changeIconSize);
|
||||
byId("reliefEditorSet").on("change", changeIconsSet);
|
||||
reliefIconsDiv.querySelectorAll("svg").forEach(el => el.on("click", changeIcon));
|
||||
|
||||
document.getElementById("reliefEditStyle").addEventListener("click", () => editStyle("terrain"));
|
||||
document.getElementById("reliefCopy").addEventListener("click", copyIcon);
|
||||
document.getElementById("reliefMoveFront").addEventListener("click", () => elSelected.raise());
|
||||
document.getElementById("reliefMoveBack").addEventListener("click", () => elSelected.lower());
|
||||
document.getElementById("reliefRemove").addEventListener("click", removeIcon);
|
||||
byId("reliefEditStyle").on("click", () => editStyle("terrain"));
|
||||
byId("reliefCopy").on("click", copyIcon);
|
||||
byId("reliefMoveFront").on("click", () => elSelected.raise());
|
||||
byId("reliefMoveBack").on("click", () => elSelected.lower());
|
||||
byId("reliefRemove").on("click", removeIcon);
|
||||
|
||||
function dragReliefIcon() {
|
||||
const dx = +this.getAttribute("x") - d3.event.x;
|
||||
const dy = +this.getAttribute("y") - d3.event.y;
|
||||
|
||||
let newX;
|
||||
let newY;
|
||||
|
||||
d3.event.on("drag", function () {
|
||||
const x = d3.event.x,
|
||||
y = d3.event.y;
|
||||
this.setAttribute("x", dx + x);
|
||||
this.setAttribute("y", dy + y);
|
||||
newX = dx + d3.event.x;
|
||||
newY = dy + d3.event.y;
|
||||
this.setAttribute("x", newX);
|
||||
this.setAttribute("y", newY);
|
||||
});
|
||||
|
||||
d3.event.on("end", function () {
|
||||
const id = this.dataset.id;
|
||||
const icon = pack.relief.find(icon => icon.i === +id);
|
||||
if (icon) {
|
||||
icon.x = newX;
|
||||
icon.y = newY;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +308,23 @@ function editReliefIcon() {
|
|||
removeCircle();
|
||||
unselect();
|
||||
clearMainTip();
|
||||
// Read back edits and switch terrain to canvas rendering
|
||||
if (typeof exitReliefSvgEditMode === "function") exitReliefSvgEditMode();
|
||||
|
||||
// Sync pack.relief from the current SVG DOM (captures all edits)
|
||||
pack.relief = [];
|
||||
terrain.selectAll("use").each(function () {
|
||||
const href = this.getAttribute("href") || this.getAttribute("xlink:href") || "";
|
||||
if (!href) return;
|
||||
pack.relief.push({
|
||||
i: pack.relief.length,
|
||||
href,
|
||||
x: +this.getAttribute("x"),
|
||||
y: +this.getAttribute("y"),
|
||||
s: +this.getAttribute("width")
|
||||
});
|
||||
});
|
||||
|
||||
// Switch from SVG edit mode back to WebGL rendering
|
||||
undrawRelief();
|
||||
drawRelief();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -729,19 +729,22 @@ styleHeightmapCurve.on("change", e => {
|
|||
|
||||
styleReliefSet.on("change", e => {
|
||||
terrain.attr("set", e.target.value);
|
||||
drawReliefIcons();
|
||||
pack.relief = [];
|
||||
drawRelief();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
});
|
||||
|
||||
styleReliefSize.on("change", e => {
|
||||
terrain.attr("size", e.target.value);
|
||||
drawReliefIcons();
|
||||
pack.relief = [];
|
||||
drawRelief();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
});
|
||||
|
||||
styleReliefDensity.on("change", e => {
|
||||
terrain.attr("density", e.target.value);
|
||||
drawReliefIcons();
|
||||
pack.relief = [];
|
||||
drawRelief();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function processFeatureRegeneration(event, button) {
|
|||
$("#labels").fadeIn();
|
||||
drawStateLabels();
|
||||
} else if (button === "regenerateReliefIcons") {
|
||||
drawReliefIcons();
|
||||
generateReliefIcons();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
} else if (button === "regenerateRoutes") {
|
||||
regenerateRoutes();
|
||||
|
|
@ -327,8 +327,8 @@ function recreateStates() {
|
|||
const type = nomadic
|
||||
? "Nomadic"
|
||||
: pack.cultures[culture].type === "Nomadic"
|
||||
? "Generic"
|
||||
: pack.cultures[culture].type;
|
||||
? "Generic"
|
||||
: pack.cultures[culture].type;
|
||||
const expansionism = rn(Math.random() * byId("sizeVariety").value + 1, 1);
|
||||
|
||||
const cultureType = pack.cultures[culture].type;
|
||||
|
|
@ -898,8 +898,8 @@ function configMarkersGeneration() {
|
|||
<td><input class="type" value="${type}" /></td>
|
||||
<td style="position: relative">
|
||||
<img class="image" src="${isExternal ? icon : ""}" ${
|
||||
isExternal ? "" : "hidden"
|
||||
} style="width:1.2em; height:1.2em; vertical-align: middle;">
|
||||
isExternal ? "" : "hidden"
|
||||
} style="width:1.2em; height:1.2em; vertical-align: middle;">
|
||||
<span class="emoji" style="font-size:1.2em">${isExternal ? "" : icon}</span>
|
||||
<button class="changeIcon icon-pencil"></button>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
import "./voronoi";
|
||||
import "./heightmap-generator";
|
||||
import "./biomes";
|
||||
import "./burgs-generator";
|
||||
import "./cultures-generator";
|
||||
import "./emblem";
|
||||
import "./features";
|
||||
import "./fonts";
|
||||
import "./heightmap-generator";
|
||||
import "./ice";
|
||||
import "./lakes";
|
||||
import "./markers-generator";
|
||||
import "./military-generator";
|
||||
import "./names-generator";
|
||||
import "./ocean-layers";
|
||||
import "./lakes";
|
||||
import "./provinces-generator";
|
||||
import "./relief-generator";
|
||||
import "./religions-generator";
|
||||
import "./river-generator";
|
||||
import "./burgs-generator";
|
||||
import "./biomes";
|
||||
import "./cultures-generator";
|
||||
import "./routes-generator";
|
||||
import "./states-generator";
|
||||
import "./voronoi";
|
||||
import "./zones-generator";
|
||||
import "./religions-generator";
|
||||
import "./provinces-generator";
|
||||
import "./emblem";
|
||||
import "./ice";
|
||||
import "./military-generator";
|
||||
import "./markers-generator";
|
||||
import "./fonts";
|
||||
|
|
|
|||
257
src/modules/relief-generator.ts
Normal file
257
src/modules/relief-generator.ts
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
import { extent, polygonContains } from "d3";
|
||||
import {
|
||||
byId,
|
||||
getPackPolygon,
|
||||
minmax,
|
||||
poissonDiscSampler,
|
||||
rand,
|
||||
rn,
|
||||
} from "../utils";
|
||||
|
||||
export interface ReliefIcon {
|
||||
i: number;
|
||||
href: string; // e.g. "#relief-mount-1"
|
||||
x: number;
|
||||
y: number;
|
||||
s: number; // size (width = height in map units)
|
||||
}
|
||||
|
||||
export function generateRelief(): ReliefIcon[] {
|
||||
TIME && console.time("generateRelief");
|
||||
|
||||
const cells = pack.cells;
|
||||
const terrain = byId("terrain");
|
||||
if (!terrain) throw new Error("Terrain element not found");
|
||||
|
||||
const set = terrain.getAttribute("set") || "simple";
|
||||
const density = Number(terrain.getAttribute("density")) || 0.4;
|
||||
const size = 2 * (Number(terrain.getAttribute("size")) || 1);
|
||||
const mod = 0.2 * size;
|
||||
|
||||
const reliefIcons: ReliefIcon[] = [];
|
||||
|
||||
for (const i of cells.i) {
|
||||
const height = cells.h[i];
|
||||
if (height < 20 || cells.r[i]) continue;
|
||||
const biome = cells.biome[i];
|
||||
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue;
|
||||
|
||||
const polygon = getPackPolygon(i, pack);
|
||||
const [minX, maxX] = extent(polygon, (p) => p[0]);
|
||||
const [minY, maxY] = extent(polygon, (p) => p[1]);
|
||||
if (
|
||||
minX === undefined ||
|
||||
minY === undefined ||
|
||||
maxX === undefined ||
|
||||
maxY === undefined
|
||||
)
|
||||
continue;
|
||||
|
||||
if (height < 50) {
|
||||
const iconsDensity = biomesData.iconsDensity[biome] / 100;
|
||||
const radius = 2 / iconsDensity / density;
|
||||
if (Math.random() > iconsDensity * 10) continue; // skip very low density icons
|
||||
|
||||
for (const [cx, cy] of poissonDiscSampler(
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
radius,
|
||||
)) {
|
||||
if (!polygonContains(polygon, [cx, cy])) continue;
|
||||
let h = (4 + Math.random()) * size;
|
||||
const icon = getBiomeIcon(i, biome);
|
||||
if (icon === "#relief-grass-1") h *= 1.2;
|
||||
reliefIcons.push({
|
||||
i: reliefIcons.length,
|
||||
href: icon,
|
||||
x: rn(cx - h, 2),
|
||||
y: rn(cy - h, 2),
|
||||
s: rn(h * 2, 2),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const radius = 2 / density;
|
||||
const [icon, h] = getReliefIconForCell(i, height);
|
||||
for (const [cx, cy] of poissonDiscSampler(
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
radius,
|
||||
)) {
|
||||
if (!polygonContains(polygon, [cx, cy])) continue;
|
||||
reliefIcons.push({
|
||||
i: reliefIcons.length,
|
||||
href: icon,
|
||||
x: rn(cx - h, 2),
|
||||
y: rn(cy - h, 2),
|
||||
s: rn(h * 2, 2),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reliefIcons.sort((a, b) => a.y + a.s - (b.y + b.s));
|
||||
pack.relief = reliefIcons;
|
||||
|
||||
TIME && console.timeEnd("generateRelief");
|
||||
return reliefIcons;
|
||||
|
||||
function getReliefIconForCell(
|
||||
cellIndex: number,
|
||||
h: number,
|
||||
): [string, number] {
|
||||
const temp = grid.cells.temp[pack.cells.g[cellIndex]];
|
||||
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
|
||||
const iconSize = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
|
||||
return [getHref(type, set), iconSize];
|
||||
}
|
||||
|
||||
function getBiomeIcon(cellIndex: number, biome: number): string {
|
||||
const b = biomesData.icons[biome];
|
||||
let type = b[Math.floor(Math.random() * b.length)];
|
||||
const temp = grid.cells.temp[pack.cells.g[cellIndex]];
|
||||
if (type === "conifer" && temp < 0) type = "coniferSnow";
|
||||
return getHref(type, set);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Utilities ─────────────────────────────────────────────────────────
|
||||
export 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",
|
||||
],
|
||||
};
|
||||
|
||||
// map a symbol href to its atlas set and tile index
|
||||
export function resolveSprite(symbolHref: string): {
|
||||
set: string;
|
||||
tileIndex: number;
|
||||
} {
|
||||
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}"`);
|
||||
}
|
||||
|
||||
const VARIANT_RANGES: Record<string, [number, number]> = {
|
||||
mount: [2, 7],
|
||||
mountSnow: [1, 6],
|
||||
hill: [2, 5],
|
||||
conifer: [2, 2],
|
||||
coniferSnow: [1, 1],
|
||||
swamp: [2, 3],
|
||||
cactus: [1, 3],
|
||||
deadTree: [1, 2],
|
||||
vulcan: [1, 3],
|
||||
deciduous: [2, 3],
|
||||
};
|
||||
|
||||
const COLORED_TO_SIMPLE_MAP: Record<string, string> = {
|
||||
mountSnow: "mount",
|
||||
vulcan: "mount",
|
||||
coniferSnow: "conifer",
|
||||
cactus: "dune",
|
||||
deadTree: "dune",
|
||||
};
|
||||
|
||||
function getVariant(type: string): number {
|
||||
const range = VARIANT_RANGES[type];
|
||||
return range ? rand(...range) : 2;
|
||||
}
|
||||
|
||||
function getHref(type: string, set: string): string {
|
||||
if (set === "colored") return `#relief-${type}-${getVariant(type)}`;
|
||||
if (set === "gray") return `#relief-${type}-${getVariant(type)}-bw`;
|
||||
return `#relief-${COLORED_TO_SIMPLE_MAP[type] ?? type}-1`;
|
||||
}
|
||||
|
||||
window.generateReliefIcons = generateRelief;
|
||||
|
||||
declare global {
|
||||
var generateReliefIcons: () => ReliefIcon[];
|
||||
}
|
||||
|
|
@ -1,33 +1,13 @@
|
|||
import {extent, polygonContains} from "d3";
|
||||
import * as THREE from "three";
|
||||
import {minmax, rand, rn} from "../utils";
|
||||
import type { ReliefIcon } from "../modules/relief-generator";
|
||||
import {
|
||||
generateRelief,
|
||||
RELIEF_SYMBOLS,
|
||||
resolveSprite,
|
||||
} from "../modules/relief-generator";
|
||||
import { byId } from "../utils";
|
||||
|
||||
interface ReliefIcon {
|
||||
i: string; // e.g. "#relief-mount-1"
|
||||
x: number;
|
||||
y: number;
|
||||
s: number; // size (width = height in map units)
|
||||
}
|
||||
|
||||
declare global {
|
||||
var drawReliefIcons: () => void;
|
||||
var terrain: import("d3").Selection<SVGGElement, unknown, null, undefined>;
|
||||
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;
|
||||
// ── Module state ───────────────────────────────────────────────────────────────
|
||||
let fo: SVGForeignObjectElement | null = null;
|
||||
let renderer: any = null; // THREE.WebGLRenderer
|
||||
let camera: any = null; // THREE.OrthographicCamera
|
||||
|
|
@ -35,109 +15,15 @@ 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}"`);
|
||||
}
|
||||
// ── Texture ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function loadTexture(set: string): Promise<any> {
|
||||
if (textureCache.has(set)) return Promise.resolve(textureCache.get(set));
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
const loader = new THREE.TextureLoader();
|
||||
loader.load(
|
||||
`images/relief/${set}.png`,
|
||||
texture => {
|
||||
(texture) => {
|
||||
texture.flipY = false;
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.needsUpdate = true;
|
||||
|
|
@ -150,17 +36,22 @@ function loadTexture(set: string): Promise<any> {
|
|||
},
|
||||
undefined,
|
||||
() => {
|
||||
console.warn(`Relief: atlas not found for "${set}". Run: npm run generate-atlases`);
|
||||
ERROR && console.error(`Relief: failed to load atlas for "${set}"`);
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ── WebGL bootstrap ────────────────────────────────────────────────────────────
|
||||
|
||||
function ensureRenderer(): boolean {
|
||||
const terrainEl = byId("terrain");
|
||||
if (!terrainEl) return false;
|
||||
|
||||
if (renderer) {
|
||||
// Recover from WebGL context loss (can happen when canvas is detached from DOM)
|
||||
if (renderer.getContext().isContextLost()) {
|
||||
// Recover from WebGL context loss
|
||||
renderer.forceContextRestore();
|
||||
renderer.dispose();
|
||||
renderer = null;
|
||||
|
|
@ -169,14 +60,16 @@ function ensureRenderer(): boolean {
|
|||
disposeTextureCache();
|
||||
// fall through to recreate
|
||||
} else {
|
||||
if (fo && !fo.isConnected) terrain.node()!.appendChild(fo);
|
||||
if (fo && !fo.isConnected) terrainEl.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 = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"foreignObject",
|
||||
) as unknown as SVGForeignObjectElement;
|
||||
fo.id = "terrainFo";
|
||||
fo.setAttribute("x", "0");
|
||||
fo.setAttribute("y", "0");
|
||||
|
|
@ -184,40 +77,41 @@ function ensureRenderer(): boolean {
|
|||
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);
|
||||
fo.appendChild(canvas);
|
||||
terrainEl.appendChild(fo);
|
||||
|
||||
try {
|
||||
renderer = new THREE.WebGLRenderer({canvas, alpha: true, antialias: false, preserveDrawingBuffer: true});
|
||||
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%;";
|
||||
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 in SVG coordinate space: top=0, bottom=H puts map y=0 at screen-top.
|
||||
camera = new THREE.OrthographicCamera(0, graphWidth, 0, graphHeight, -1, 1);
|
||||
scene = new THREE.Scene();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── Scene / geometry ─────────────────────────────────────────────────────────
|
||||
// ── 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.
|
||||
* Build a BufferGeometry with all icon quads for one atlas set.
|
||||
* Geometry is painter's-order sorted so depth is correct without depth testing.
|
||||
*
|
||||
* UV layout (texture.flipY = false means v=0 is top of image):
|
||||
* UV layout (texture.flipY = false — v=0 is top of image):
|
||||
* u = col/cols … (col+1)/cols
|
||||
* v = row/rows … (row+1)/rows
|
||||
*/
|
||||
|
|
@ -234,7 +128,7 @@ function buildSetMesh(icons: ReliefIcon[], set: string, texture: any): any {
|
|||
let vi = 0,
|
||||
ii = 0;
|
||||
for (const r of icons) {
|
||||
const {tileIndex} = resolveSprite(r.i);
|
||||
const { tileIndex } = resolveSprite(r.href);
|
||||
const col = tileIndex % cols;
|
||||
const row = Math.floor(tileIndex / cols);
|
||||
const u0 = col / cols,
|
||||
|
|
@ -245,26 +139,19 @@ function buildSetMesh(icons: ReliefIcon[], set: string, texture: any): any {
|
|||
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;
|
||||
}
|
||||
|
|
@ -279,7 +166,7 @@ function buildSetMesh(icons: ReliefIcon[], set: string, texture: any): any {
|
|||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
depthWrite: false,
|
||||
});
|
||||
|
||||
return new THREE.Mesh(geo, mat);
|
||||
|
|
@ -296,7 +183,6 @@ function disposeScene(): void {
|
|||
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();
|
||||
|
|
@ -304,14 +190,13 @@ function disposeScene(): void {
|
|||
}
|
||||
}
|
||||
|
||||
function buildScene(): void {
|
||||
function buildScene(icons: ReliefIcon[]): 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);
|
||||
for (const r of icons) {
|
||||
const { set } = resolveSprite(r.href);
|
||||
let arr = bySet.get(set);
|
||||
if (!arr) {
|
||||
arr = [];
|
||||
|
|
@ -320,10 +205,10 @@ function buildScene(): void {
|
|||
arr.push(r);
|
||||
}
|
||||
|
||||
for (const [set, icons] of bySet) {
|
||||
for (const [set, setIcons] of bySet) {
|
||||
const texture = textureCache.get(set);
|
||||
if (!texture) continue;
|
||||
scene.add(buildSetMesh(icons, set, texture));
|
||||
scene.add(buildSetMesh(setIcons, set, texture));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -335,197 +220,145 @@ function renderFrame(): void {
|
|||
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("")
|
||||
);
|
||||
}
|
||||
// ── Private draw / clear ───────────────────────────────────────────────────────
|
||||
|
||||
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")!
|
||||
function drawWebGl(icons: ReliefIcon[]): void {
|
||||
const terrainEl = byId("terrain");
|
||||
if (!terrainEl) return;
|
||||
if (!icons.length) return;
|
||||
|
||||
terrainEl.innerHTML = "";
|
||||
terrainEl.dataset.mode = "webGL";
|
||||
const set = terrainEl.getAttribute("set") || "simple";
|
||||
|
||||
if (ensureRenderer()) {
|
||||
loadTexture(set).then(() => {
|
||||
buildScene(icons);
|
||||
renderFrame();
|
||||
});
|
||||
});
|
||||
terrain.selectAll("use").remove();
|
||||
svgEditMode = false;
|
||||
loadTexture(terrain.attr("set")).then(() => {
|
||||
buildScene();
|
||||
renderFrame();
|
||||
});
|
||||
} else {
|
||||
WARN && console.warn("Relief: WebGL renderer failed");
|
||||
}
|
||||
}
|
||||
|
||||
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 drawSvg(icons: ReliefIcon[]): void {
|
||||
const terrainEl = byId("terrain");
|
||||
if (!terrainEl) return;
|
||||
terrainEl.innerHTML = "";
|
||||
|
||||
const html = icons.map(
|
||||
(r) =>
|
||||
`<use href="${r.href}" data-id="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`,
|
||||
);
|
||||
terrainEl.innerHTML = html.join("");
|
||||
terrainEl.dataset.mode = "svg";
|
||||
}
|
||||
|
||||
function restoreReliefAfterSave(): void {
|
||||
if (!svgEditMode) terrain.selectAll("use").remove();
|
||||
}
|
||||
window.drawRelief = (type: "svg" | "webGL" = "webGL") => {
|
||||
const icons = pack.relief?.length ? pack.relief : generateRelief();
|
||||
if (type === "svg") drawSvg(icons);
|
||||
else drawWebGl(icons);
|
||||
};
|
||||
|
||||
const reliefIconsRenderer = (): void => {
|
||||
TIME && console.time("drawRelief");
|
||||
window.undrawRelief = () => {
|
||||
const terrainEl = byId("terrain");
|
||||
const mode = terrainEl?.dataset.mode || "webGL";
|
||||
if (mode === "webGL") {
|
||||
disposeScene();
|
||||
disposeTextureCache();
|
||||
if (renderer) {
|
||||
renderer.dispose();
|
||||
renderer = null;
|
||||
}
|
||||
if (fo) {
|
||||
if (fo.isConnected) fo.remove();
|
||||
fo = null;
|
||||
}
|
||||
camera = null;
|
||||
scene = null;
|
||||
}
|
||||
|
||||
terrain.selectAll("*").remove();
|
||||
disposeTextureCache();
|
||||
disposeScene();
|
||||
reliefIconData = [];
|
||||
svgEditMode = false;
|
||||
if (terrainEl) terrainEl.innerHTML = "";
|
||||
};
|
||||
|
||||
const cells = pack.cells;
|
||||
const density = Number(terrain.attr("density")) || 0.4;
|
||||
const size = 2 * (Number(terrain.attr("size")) || 1);
|
||||
const mod = 0.2 * size;
|
||||
// re-render the current WebGL frame (called on pan/zoom)
|
||||
window.rerenderReliefIcons = renderFrame;
|
||||
|
||||
// Migrate legacy saves: read <use> elements from the terrain SVG into pack.relief, remove them from the DOM, then render via WebGL.
|
||||
window.migrateReliefFromSvg = () => {
|
||||
const terrainEl = byId("terrain");
|
||||
if (!terrainEl) return;
|
||||
const relief: ReliefIcon[] = [];
|
||||
|
||||
for (const i of cells.i) {
|
||||
const height = cells.h[i];
|
||||
if (height < 20 || cells.r[i]) continue;
|
||||
const biome = cells.biome[i];
|
||||
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue;
|
||||
terrainEl.querySelectorAll<SVGUseElement>("use").forEach((u) => {
|
||||
const href = u.getAttribute("href") || u.getAttribute("xlink:href") || "";
|
||||
if (!href) return;
|
||||
relief.push({
|
||||
i: relief.length,
|
||||
href,
|
||||
x: +u.getAttribute("x")!,
|
||||
y: +u.getAttribute("y")!,
|
||||
s: +u.getAttribute("width")!,
|
||||
});
|
||||
});
|
||||
terrainEl.innerHTML = "";
|
||||
pack.relief = relief;
|
||||
drawWebGl(relief);
|
||||
};
|
||||
|
||||
const polygon = getPackPolygon(i);
|
||||
const [minX, maxX] = extent(polygon, p => p[0]) as [number, number];
|
||||
const [minY, maxY] = extent(polygon, p => p[1]) as [number, number];
|
||||
let _reliefSvgInjectedForSave = false;
|
||||
|
||||
if (height < 50) placeBiomeIcons();
|
||||
else placeReliefIcons();
|
||||
|
||||
function placeBiomeIcons(): void {
|
||||
const iconsDensity = biomesData.iconsDensity[biome] / 100;
|
||||
const radius = 2 / iconsDensity / density;
|
||||
if (Math.random() > iconsDensity * 10) return;
|
||||
for (const [cx, cy] of window.poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
|
||||
if (!polygonContains(polygon, [cx, cy])) continue;
|
||||
let h = (4 + Math.random()) * size;
|
||||
const icon = getBiomeIcon(i, biomesData.icons[biome]);
|
||||
if (icon === "#relief-grass-1") h *= 1.2;
|
||||
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
|
||||
}
|
||||
}
|
||||
|
||||
function placeReliefIcons(): void {
|
||||
const radius = 2 / density;
|
||||
const [icon, h] = getReliefIcon(i, height);
|
||||
for (const [cx, cy] of window.poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
|
||||
if (!polygonContains(polygon, [cx, cy])) continue;
|
||||
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
|
||||
}
|
||||
}
|
||||
|
||||
function getReliefIcon(cellIndex: number, h: number): [string, number] {
|
||||
const temp = grid.cells.temp[pack.cells.g[cellIndex]];
|
||||
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
|
||||
const iconSize = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
|
||||
return [getIcon(type), iconSize];
|
||||
}
|
||||
}
|
||||
|
||||
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
|
||||
reliefIconData = relief;
|
||||
|
||||
TIME && console.timeEnd("drawRelief");
|
||||
|
||||
if (reliefIconData.length) {
|
||||
if (ensureRenderer()) {
|
||||
loadTexture(terrain.attr("set")).then(() => {
|
||||
buildScene();
|
||||
renderFrame();
|
||||
});
|
||||
} else {
|
||||
WARN && console.warn("Relief: WebGL renderer failed");
|
||||
}
|
||||
}
|
||||
|
||||
function getBiomeIcon(cellIndex: number, b: string[]): string {
|
||||
let type = b[Math.floor(Math.random() * b.length)];
|
||||
const temp = grid.cells.temp[pack.cells.g[cellIndex]];
|
||||
if (type === "conifer" && temp < 0) type = "coniferSnow";
|
||||
return getIcon(type);
|
||||
}
|
||||
|
||||
function getVariant(type: string): number {
|
||||
switch (type) {
|
||||
case "mount":
|
||||
return rand(2, 7);
|
||||
case "mountSnow":
|
||||
return rand(1, 6);
|
||||
case "hill":
|
||||
return rand(2, 5);
|
||||
case "conifer":
|
||||
return 2;
|
||||
case "coniferSnow":
|
||||
return 1;
|
||||
case "swamp":
|
||||
return rand(2, 3);
|
||||
case "cactus":
|
||||
return rand(1, 3);
|
||||
case "deadTree":
|
||||
return rand(1, 2);
|
||||
case "vulcan":
|
||||
return rand(1, 3);
|
||||
case "deciduous":
|
||||
return rand(2, 3);
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
function getOldIcon(type: string): string {
|
||||
const map: Record<string, string> = {
|
||||
mountSnow: "mount",
|
||||
vulcan: "mount",
|
||||
coniferSnow: "conifer",
|
||||
cactus: "dune",
|
||||
deadTree: "dune"
|
||||
};
|
||||
return map[type] ?? type;
|
||||
}
|
||||
|
||||
function getIcon(type: string): string {
|
||||
const set = terrain.attr("set") || "simple";
|
||||
if (set === "colored") return `#relief-${type}-${getVariant(type)}`;
|
||||
if (set === "gray") return `#relief-${type}-${getVariant(type)}-bw`;
|
||||
return `#relief-${getOldIcon(type)}-1`;
|
||||
/**
|
||||
* Before SVG serialization: ensure <use> elements are in the terrain group.
|
||||
* In WebGL mode, temporarily injects them from pack.relief.
|
||||
* In SVG edit mode, elements are already live in the DOM.
|
||||
*/
|
||||
window.prepareReliefForSave = () => {
|
||||
const terrainEl = byId("terrain");
|
||||
if (!terrainEl) return;
|
||||
if (terrainEl.querySelectorAll("use").length > 0) {
|
||||
_reliefSvgInjectedForSave = false;
|
||||
} else {
|
||||
terrainEl.insertAdjacentHTML(
|
||||
"afterbegin",
|
||||
(pack.relief || [])
|
||||
.map(
|
||||
(r) =>
|
||||
`<use href="${r.href}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`,
|
||||
)
|
||||
.join(""),
|
||||
);
|
||||
_reliefSvgInjectedForSave = true;
|
||||
}
|
||||
};
|
||||
|
||||
window.renderReliefIcons = renderFrame;
|
||||
window.drawReliefIcons = reliefIconsRenderer;
|
||||
window.enterReliefSvgEditMode = enterSvgEditMode;
|
||||
window.exitReliefSvgEditMode = exitSvgEditMode;
|
||||
window.prepareReliefForSave = prepareReliefForSave;
|
||||
window.restoreReliefAfterSave = restoreReliefAfterSave;
|
||||
/** Remove temporarily injected <use> elements after serialization. */
|
||||
window.restoreReliefAfterSave = () => {
|
||||
if (_reliefSvgInjectedForSave) {
|
||||
for (const el of byId("terrain")?.querySelectorAll("use") ?? [])
|
||||
el.remove();
|
||||
_reliefSvgInjectedForSave = false;
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
var drawRelief: (type?: "svg" | "webGL") => void;
|
||||
var undrawRelief: () => void;
|
||||
var rerenderReliefIcons: () => void;
|
||||
var migrateReliefFromSvg: () => void;
|
||||
var prepareReliefForSave: () => void;
|
||||
var restoreReliefAfterSave: () => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Burg } from "../modules/burgs-generator";
|
|||
import type { Culture } from "../modules/cultures-generator";
|
||||
import type { PackedGraphFeature } from "../modules/features";
|
||||
import type { Province } from "../modules/provinces-generator";
|
||||
import type { ReliefIcon } from "../modules/relief-generator";
|
||||
import type { River } from "../modules/river-generator";
|
||||
import type { Route } from "../modules/routes-generator";
|
||||
import type { State } from "../modules/states-generator";
|
||||
|
|
@ -53,6 +54,7 @@ export interface PackedGraph {
|
|||
p: [number, number][]; // vertex points
|
||||
};
|
||||
rivers: River[];
|
||||
relief: ReliefIcon[];
|
||||
features: PackedGraphFeature[];
|
||||
burgs: Burg[];
|
||||
states: State[];
|
||||
|
|
|
|||
|
|
@ -424,7 +424,10 @@ export const findAllCellsInRadius = (
|
|||
* @param {number} i - The index of the packed cell
|
||||
* @returns {Array} - An array of polygon points for the specified cell
|
||||
*/
|
||||
export const getPackPolygon = (cellIndex: number, packedGraph: any) => {
|
||||
export const getPackPolygon = (
|
||||
cellIndex: number,
|
||||
packedGraph: any,
|
||||
): [number, number][] => {
|
||||
return packedGraph.cells.v[cellIndex].map(
|
||||
(v: number) => packedGraph.vertices.p[v],
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue