fix: no foright object

This commit is contained in:
Azgaar 2026-03-11 23:14:56 +01:00
parent fae0bd1085
commit 94da8fa0bd

View file

@ -4,13 +4,16 @@ import type { ReliefIcon } from "../modules/relief-generator";
import { generateRelief } from "../modules/relief-generator"; import { generateRelief } from "../modules/relief-generator";
import { byId } from "../utils"; import { byId } from "../utils";
let fo: SVGForeignObjectElement | null = null; let glCanvas: HTMLCanvasElement | null = null;
let renderer: THREE.WebGLRenderer | null = null; let renderer: THREE.WebGLRenderer | null = null;
let camera: THREE.OrthographicCamera | null = null; let camera: THREE.OrthographicCamera | null = null;
let scene: THREE.Scene | null = null; let scene: THREE.Scene | null = null;
const textureCache = new Map<string, THREE.Texture>(); // set name → THREE.Texture const textureCache = new Map<string, THREE.Texture>(); // set name → THREE.Texture
let lastBuiltIcons: ReliefIcon[] | null = null;
let lastBuiltSet: string | null = null;
function preloadTextures(): void { function preloadTextures(): void {
for (const set of Object.keys(RELIEF_SYMBOLS)) loadTexture(set); for (const set of Object.keys(RELIEF_SYMBOLS)) loadTexture(set);
} }
@ -18,6 +21,7 @@ function preloadTextures(): void {
function loadTexture(set: string): Promise<THREE.Texture | null> { function loadTexture(set: string): Promise<THREE.Texture | null> {
if (textureCache.has(set)) if (textureCache.has(set))
return Promise.resolve(textureCache.get(set) || null); return Promise.resolve(textureCache.get(set) || null);
return new Promise((resolve) => { return new Promise((resolve) => {
const loader = new THREE.TextureLoader(); const loader = new THREE.TextureLoader();
loader.load( loader.load(
@ -44,8 +48,7 @@ function loadTexture(set: string): Promise<THREE.Texture | null> {
} }
function ensureRenderer(): boolean { function ensureRenderer(): boolean {
const terrainEl = byId("terrain"); if (!byId("terrain")) return false;
if (!terrainEl) return false;
if (renderer) { if (renderer) {
if (renderer.getContext().isContextLost()) { if (renderer.getContext().isContextLost()) {
@ -55,41 +58,42 @@ function ensureRenderer(): boolean {
renderer = null; renderer = null;
camera = null; camera = null;
scene = null; scene = null;
glCanvas = null;
disposeTextureCache(); disposeTextureCache();
lastBuiltIcons = null;
lastBuiltSet = null;
} else { } else {
if (fo && !fo.isConnected) terrainEl.appendChild(fo); // Re-attach if the canvas was removed from the DOM externally.
if (glCanvas && !glCanvas.isConnected) {
const terrainSvg = byId("map-layer-terrain");
if (terrainSvg)
terrainSvg.parentElement!.insertBefore(glCanvas, terrainSvg);
else document.body.appendChild(glCanvas);
}
return true; return true;
} }
} }
// foreignObject hosts the WebGL canvas inside the SVG. glCanvas = document.createElement("canvas");
fo = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject"); glCanvas.id = "terrainCanvas";
fo.id = "terrainFo"; glCanvas.style.cssText =
fo.setAttribute("x", "0"); "display:block;pointer-events:none;position:absolute;top:0;left:0";
fo.setAttribute("y", "0"); const map = byId("map");
fo.setAttribute("width", String(graphWidth)); if (map) document.body.insertAdjacentElement("afterend", glCanvas);
fo.setAttribute("height", String(graphHeight));
// IMPORTANT: use document.createElement, not createElementNS.
const canvas = document.createElement("canvas");
canvas.id = "terrainGlCanvas";
fo.appendChild(canvas);
terrainEl.appendChild(fo);
try { try {
renderer = new THREE.WebGLRenderer({ renderer = new THREE.WebGLRenderer({
canvas, canvas: glCanvas,
alpha: true, alpha: true,
antialias: false, antialias: false,
preserveDrawingBuffer: true,
}); });
renderer.setClearColor(0x000000, 0); renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(window.devicePixelRatio || 1); renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setSize(graphWidth, graphHeight); renderer.setSize(graphWidth, graphHeight);
canvas.style.cssText =
"display:block;pointer-events:none;position:absolute;top:0;left:0;width:100%;height:100%;";
} catch (e) { } catch (e) {
console.error("Relief: WebGL init failed", e); console.error("Relief: WebGL init failed", e);
glCanvas.remove();
glCanvas = null;
return false; return false;
} }
@ -115,20 +119,23 @@ function resolveSprite(symbolHref: string): {
} }
// Build a BufferGeometry with all icon quads for one atlas set. // Build a BufferGeometry with all icon quads for one atlas set.
function buildSetMesh(icons: ReliefIcon[], set: string, texture: any): any { function buildSetMesh(
entries: Array<{ icon: ReliefIcon; tileIndex: number }>,
set: string,
texture: any,
): any {
const ids = RELIEF_SYMBOLS[set] ?? []; const ids = RELIEF_SYMBOLS[set] ?? [];
const n = ids.length || 1; const n = ids.length || 1;
const cols = Math.ceil(Math.sqrt(n)); const cols = Math.ceil(Math.sqrt(n));
const rows = Math.ceil(n / cols); const rows = Math.ceil(n / cols);
const positions = new Float32Array(icons.length * 4 * 3); const positions = new Float32Array(entries.length * 4 * 3);
const uvs = new Float32Array(icons.length * 4 * 2); const uvs = new Float32Array(entries.length * 4 * 2);
const indices = new Uint32Array(icons.length * 6); const indices = new Uint32Array(entries.length * 6);
let vi = 0, let vi = 0,
ii = 0; ii = 0;
for (const r of icons) { for (const { icon: r, tileIndex } of entries) {
const { tileIndex } = resolveSprite(r.href);
const col = tileIndex % cols; const col = tileIndex % cols;
const row = Math.floor(tileIndex / cols); const row = Math.floor(tileIndex / cols);
const u0 = col / cols, const u0 = col / cols,
@ -197,37 +204,35 @@ function buildScene(icons: ReliefIcon[]): void {
if (!scene) return; if (!scene) return;
disposeScene(); disposeScene();
const bySet = new Map<string, ReliefIcon[]>(); const bySet = new Map<
string,
Array<{ icon: ReliefIcon; tileIndex: number }>
>();
for (const r of icons) { for (const r of icons) {
const { set } = resolveSprite(r.href); const { set, tileIndex } = resolveSprite(r.href);
let arr = bySet.get(set); let arr = bySet.get(set);
if (!arr) { if (!arr) {
arr = []; arr = [];
bySet.set(set, arr); bySet.set(set, arr);
} }
arr.push(r); arr.push({ icon: r, tileIndex });
} }
for (const [set, setIcons] of bySet) { for (const [set, setEntries] of bySet) {
const texture = textureCache.get(set); const texture = textureCache.get(set);
if (!texture) continue; if (!texture) continue;
scene.add(buildSetMesh(setIcons, set, texture)); scene.add(buildSetMesh(setEntries, set, texture));
} }
} }
function renderFrame(): void { function renderFrame(): void {
if (!renderer || !camera || !scene || !fo) return; if (!renderer || !camera || !scene) return;
const x = -viewX / scale; const x = -viewX / scale;
const y = -viewY / scale; const y = -viewY / scale;
const w = graphWidth / scale; const w = graphWidth / scale;
const h = graphHeight / scale; const h = graphHeight / scale;
fo.setAttribute("x", String(x));
fo.setAttribute("y", String(y));
fo.setAttribute("width", String(w));
fo.setAttribute("height", String(h));
camera.left = x; camera.left = x;
camera.right = x + w; camera.right = x + w;
camera.top = y; camera.top = y;
@ -241,7 +246,11 @@ function drawWebGl(icons: ReliefIcon[], parentEl: HTMLElement): void {
if (ensureRenderer()) { if (ensureRenderer()) {
loadTexture(set).then(() => { loadTexture(set).then(() => {
buildScene(icons); if (icons !== lastBuiltIcons || set !== lastBuiltSet) {
buildScene(icons);
lastBuiltIcons = icons;
lastBuiltSet = set;
}
renderFrame(); renderFrame();
}); });
} else { } else {
@ -284,19 +293,28 @@ window.undrawRelief = () => {
renderer.dispose(); renderer.dispose();
renderer = null; renderer = null;
} }
if (fo) { if (glCanvas) {
if (fo.isConnected) fo.remove(); if (glCanvas.isConnected) glCanvas.remove();
fo = null; glCanvas = null;
} }
camera = null; camera = null;
scene = null; scene = null;
lastBuiltIcons = null;
lastBuiltSet = null;
} }
if (terrainEl) terrainEl.innerHTML = ""; if (terrainEl) terrainEl.innerHTML = "";
}; };
// re-render the current WebGL frame (called on pan/zoom) // re-render the current WebGL frame (called on pan/zoom); coalesced to one GPU draw per animation frame
window.rerenderReliefIcons = renderFrame; let rafId: number | null = null;
window.rerenderReliefIcons = () => {
if (rafId !== null) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
rafId = null;
renderFrame();
});
};
declare global { declare global {
var drawRelief: (type?: "svg" | "webGL") => void; var drawRelief: (type?: "svg" | "webGL") => void;