refactor: Update relief icon handling and streamline texture atlas integration

This commit is contained in:
Azgaar 2026-03-12 21:46:22 +01:00
parent b17b417d1b
commit 256f36015b
5 changed files with 46 additions and 58 deletions

View file

@ -1122,10 +1122,11 @@ export function resolveVersionConflicts(mapVersion) {
terrainEl.querySelectorAll("use").forEach(u => {
const href = u.getAttribute("href") || u.getAttribute("xlink:href") || "";
if (!href) return;
const icon = href.replace("#", "");
const x = +u.getAttribute("x");
const y = +u.getAttribute("y");
const s = +u.getAttribute("width");
relief.push({i: relief.length, href, x, y, s});
relief.push({i: relief.length, icon, x, y, s});
});
terrainEl.innerHTML = "";
pack.relief = relief;

View file

@ -1,11 +1,12 @@
function createAtlas(ids: string[]) {
function createAtlas(set: string, ids: string[]) {
const url = `images/relief/${set}.png`;
const n = ids.length || 1;
const cols = Math.ceil(Math.sqrt(n));
return { ids, cols, rows: Math.ceil(n / cols) };
return { url, ids, cols, rows: Math.ceil(n / cols) };
}
export const RELIEF_SYMBOLS = {
simple: createAtlas([
export const RELIEF_ATLASES = {
simple: createAtlas("simple", [
"relief-mount-1",
"relief-hill-1",
"relief-conifer-1",
@ -16,7 +17,7 @@ export const RELIEF_SYMBOLS = {
"relief-swamp-1",
"relief-dune-1",
]),
gray: createAtlas([
gray: createAtlas("gray", [
"relief-mount-2-bw",
"relief-mount-3-bw",
"relief-mount-4-bw",
@ -52,7 +53,7 @@ export const RELIEF_SYMBOLS = {
"relief-deciduous-2-bw",
"relief-deciduous-3-bw",
]),
colored: createAtlas([
colored: createAtlas("colored", [
"relief-mount-2",
"relief-mount-3",
"relief-mount-4",
@ -90,13 +91,6 @@ export const RELIEF_SYMBOLS = {
]),
};
export const RELIEF_ATLASES = Object.fromEntries(
Object.entries(RELIEF_SYMBOLS).map(([set, { cols, rows }]) => [
set,
{ url: `images/relief/${set}.png`, cols, rows },
]),
);
export const VARIANT_RANGES: Record<string, [number, number]> = {
mount: [2, 7],
mountSnow: [1, 6],

View file

@ -11,7 +11,7 @@ import {
export interface ReliefIcon {
i: number;
href: string; // e.g. "#relief-mount-1"
icon: string; // e.g. "relief-mount-1"
x: number;
y: number;
s: number; // size (width = height in map units)
@ -63,10 +63,10 @@ export function generateRelief(): ReliefIcon[] {
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;
if (icon === "relief-grass-1") h *= 1.2;
reliefIcons.push({
i: reliefIcons.length,
href: icon,
icon,
x: rn(cx - h, 2),
y: rn(cy - h, 2),
s: rn(h * 2, 2),
@ -85,7 +85,7 @@ export function generateRelief(): ReliefIcon[] {
if (!polygonContains(polygon, [cx, cy])) continue;
reliefIcons.push({
i: reliefIcons.length,
href: icon,
icon,
x: rn(cx - h, 2),
y: rn(cy - h, 2),
s: rn(h * 2, 2),
@ -125,9 +125,9 @@ function getVariant(type: string): number {
}
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`;
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;

View file

@ -14,16 +14,16 @@ import {
export interface AtlasConfig {
url: string;
ids: string[];
cols: number;
rows: number;
}
export interface AtlasQuad {
atlasId: string;
export interface AtlasItem {
icon: string;
x: number;
y: number;
s: number;
tileIndex: number;
}
export class TextureAtlasLayer {
@ -47,25 +47,30 @@ export class TextureAtlasLayer {
});
}
draw(quads: AtlasQuad[]) {
draw(items: AtlasItem[]) {
if (!this.group) return;
this.disposeGroup();
const byAtlas = new Map<string, AtlasQuad[]>();
for (const q of quads) {
let arr = byAtlas.get(q.atlasId);
if (!arr) {
arr = [];
byAtlas.set(q.atlasId, arr);
const byAtlas = new Map<string, { item: AtlasItem; tileIndex: number }[]>();
for (const item of items) {
for (const [atlasId, config] of Object.entries(this.atlases)) {
const tileIndex = config.ids.indexOf(item.icon);
if (tileIndex === -1) continue;
let arr = byAtlas.get(atlasId);
if (!arr) {
arr = [];
byAtlas.set(atlasId, arr);
}
arr.push({ item, tileIndex });
break;
}
arr.push(q);
}
for (const [atlasId, atlasQuads] of byAtlas) {
for (const [atlasId, entries] of byAtlas) {
const texture = this.textureCache.get(atlasId);
const config = this.atlases[atlasId];
if (!texture || !config) continue;
this.group.add(buildMesh(atlasQuads, config, texture));
this.group.add(buildMesh(entries, config, texture));
}
WebGLLayer.rerender();
}
@ -113,20 +118,20 @@ export class TextureAtlasLayer {
}
function buildMesh(
quads: AtlasQuad[],
entries: Array<{ item: AtlasItem; tileIndex: number }>,
atlas: AtlasConfig,
texture: Texture,
): Mesh {
const { cols, rows } = atlas;
const positions = new Float32Array(quads.length * 4 * 3);
const uvs = new Float32Array(quads.length * 4 * 2);
const indices = new Uint32Array(quads.length * 6);
const positions = new Float32Array(entries.length * 4 * 3);
const uvs = new Float32Array(entries.length * 4 * 2);
const indices = new Uint32Array(entries.length * 6);
let vi = 0,
ii = 0;
for (const q of quads) {
const col = q.tileIndex % cols;
const row = Math.floor(q.tileIndex / cols);
for (const { item: q, tileIndex } of entries) {
const col = tileIndex % cols;
const row = Math.floor(tileIndex / cols);
const u0 = col / cols,
u1 = (col + 1) / cols;
const v0 = row / rows,

View file

@ -1,16 +1,16 @@
import { RELIEF_ATLASES, RELIEF_SYMBOLS } from "../config/relief-config";
import { RELIEF_ATLASES } from "../config/relief-config";
import type { ReliefIcon } from "../modules/relief-generator";
import { generateRelief } from "../modules/relief-generator";
import { TextureAtlasLayer } from "../modules/texture-atlas-layer";
import { byId } from "../utils";
const terrainLayer = new TextureAtlasLayer("terrain", RELIEF_ATLASES);
const layer = new TextureAtlasLayer("terrain", RELIEF_ATLASES);
function drawSvg(icons: ReliefIcon[], parentEl: HTMLElement): void {
parentEl.innerHTML = icons
.map(
(r) =>
`<use href="${r.href}" data-id="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`,
`<use href="#${r.icon}" data-id="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`,
)
.join("");
}
@ -29,24 +29,12 @@ window.drawRelief = (
if (type === "svg") {
drawSvg(icons, parentEl);
} else {
terrainLayer.draw(resolveQuads(icons));
layer.draw(icons);
}
};
function resolveQuads(icons: ReliefIcon[]) {
return icons.map((r) => {
const id = r.href.startsWith("#") ? r.href.slice(1) : r.href;
for (const [set, { ids }] of Object.entries(RELIEF_SYMBOLS)) {
const tileIndex = ids.indexOf(id);
if (tileIndex !== -1)
return { atlasId: set, x: r.x, y: r.y, s: r.s, tileIndex };
}
throw new Error(`Relief: unknown symbol href "${r.href}"`);
});
}
window.undrawRelief = () => {
terrainLayer.clear();
layer.clear();
const terrainEl = byId("terrain");
if (terrainEl) terrainEl.innerHTML = "";
};