mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-04-04 06:27:24 +02:00
fix: no foright object
This commit is contained in:
parent
fae0bd1085
commit
94da8fa0bd
1 changed files with 62 additions and 44 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue