release: bump version to 0.49.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
linkong
2026-05-08 17:42:27 +08:00
parent bb9183b8a4
commit e1984c7a35
86 changed files with 9165 additions and 412 deletions

View File

@@ -33,6 +33,10 @@ function buildTileUrl(z, x, y) {
.replace("{y}", String(y));
}
function buildTerrainTileKey(z, x, y) {
return `${z}/${x}/${y}`;
}
function getTerrainCanvas(size) {
if (typeof OffscreenCanvas !== "undefined") {
return new OffscreenCanvas(size, size);
@@ -44,8 +48,34 @@ function getTerrainCanvas(size) {
return canvas;
}
async function decodeTerrainBlob(blob, cacheKey) {
const bitmap = await createImageBitmap(blob);
const canvas = getTerrainCanvas(TERRAIN_CONFIG.tileSize);
const ctx = canvas.getContext("2d", { willReadFrequently: true });
ctx.drawImage(bitmap, 0, 0, TERRAIN_CONFIG.tileSize, TERRAIN_CONFIG.tileSize);
bitmap.close?.();
const { data, width, height } = ctx.getImageData(
0,
0,
TERRAIN_CONFIG.tileSize,
TERRAIN_CONFIG.tileSize,
);
const tileData = { data, width, height };
resolvedTileCache.set(cacheKey, tileData);
return tileData;
}
function base64ToBlob(base64, contentType = "image/png") {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return new Blob([bytes], { type: contentType });
}
async function decodeTerrainTile(z, x, y) {
const cacheKey = `${z}/${x}/${y}`;
const cacheKey = buildTerrainTileKey(z, x, y);
if (terrainTileCache.has(cacheKey)) {
return terrainTileCache.get(cacheKey);
}
@@ -56,27 +86,89 @@ async function decodeTerrainTile(z, x, y) {
throw new Error(`HTTP ${response.status} for terrain tile ${cacheKey}`);
}
const blob = await response.blob();
const bitmap = await createImageBitmap(blob);
const canvas = getTerrainCanvas(TERRAIN_CONFIG.tileSize);
const ctx = canvas.getContext("2d", { willReadFrequently: true });
ctx.drawImage(bitmap, 0, 0, TERRAIN_CONFIG.tileSize, TERRAIN_CONFIG.tileSize);
bitmap.close?.();
const { data, width, height } = ctx.getImageData(
0,
0,
TERRAIN_CONFIG.tileSize,
TERRAIN_CONFIG.tileSize,
);
const tileData = { data, width, height };
resolvedTileCache.set(cacheKey, tileData);
return tileData;
return decodeTerrainBlob(await response.blob(), cacheKey);
})();
terrainTileCache.set(cacheKey, tilePromise);
return tilePromise;
}
async function fetchTerrainTileBatch(keys) {
const uncachedKeys = keys.filter((key) => !terrainTileCache.has(key));
if (uncachedKeys.length === 0) {
return;
}
if (!TERRAIN_CONFIG.batchUrl || typeof atob !== "function") {
uncachedKeys.forEach((key) => {
const [z, x, y] = key.split("/").map(Number);
terrainTileCache.set(key, decodeTerrainTile(z, x, y));
});
return;
}
const batchPromise = (async () => {
const response = await fetch(TERRAIN_CONFIG.batchUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
tiles: uncachedKeys.map((key) => {
const [z, x, y] = key.split("/").map(Number);
return { z, x, y };
}),
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} for terrain tile batch`);
}
const payload = await response.json();
const tiles = Array.isArray(payload?.tiles) ? payload.tiles : [];
const returnedKeys = new Set();
await Promise.all(
tiles.map(async (tile) => {
const z = Number(tile?.z);
const x = Number(tile?.x);
const y = Number(tile?.y);
const data = typeof tile?.data === "string" ? tile.data : "";
if (!Number.isFinite(z) || !Number.isFinite(x) || !Number.isFinite(y) || !data) {
return;
}
const key = buildTerrainTileKey(z, x, y);
returnedKeys.add(key);
const blob = base64ToBlob(data, tile.content_type || "image/png");
terrainTileCache.set(key, decodeTerrainBlob(blob, key));
}),
);
uncachedKeys.forEach((key) => {
if (returnedKeys.has(key)) return;
const [z, x, y] = key.split("/").map(Number);
terrainTileCache.delete(key);
terrainTileCache.set(key, decodeTerrainTile(z, x, y));
});
})().catch((error) => {
console.warn("批量地形瓦片加载失败,回退到单瓦片请求:", error);
uncachedKeys.forEach((key) => {
terrainTileCache.delete(key);
const [z, x, y] = key.split("/").map(Number);
terrainTileCache.set(key, decodeTerrainTile(z, x, y));
});
});
uncachedKeys.forEach((key) => {
terrainTileCache.set(
key,
batchPromise.then(() => terrainTileCache.get(key)),
);
});
await batchPromise;
}
function decodeTerrariumHeight(tile, pixelX, pixelY) {
const safeX = THREE.MathUtils.clamp(pixelX, 0, tile.width - 1);
const safeY = THREE.MathUtils.clamp(pixelY, 0, tile.height - 1);
@@ -168,19 +260,39 @@ async function runWithConcurrency(items, limit, worker) {
async function fetchRequiredTiles(samples) {
const uniqueKeys = Array.from(
new Set(samples.map((sample) => `${TERRAIN_CONFIG.baseZoom}/${sample.tileX}/${sample.tileY}`)),
new Set(
samples.map((sample) =>
buildTerrainTileKey(TERRAIN_CONFIG.baseZoom, sample.tileX, sample.tileY),
),
),
);
const resolvedTiles = new Map();
const batchSize = Math.max(1, Number(TERRAIN_CONFIG.batchRequestSize) || 1);
const batchChunks = [];
for (let i = 0; i < uniqueKeys.length; i += batchSize) {
batchChunks.push(uniqueKeys.slice(i, i + batchSize));
}
await runWithConcurrency(
uniqueKeys,
batchChunks,
TERRAIN_CONFIG.maxConcurrentRequests,
async (key) => {
const [z, x, y] = key.split("/").map(Number);
resolvedTiles.set(key, await decodeTerrainTile(z, x, y));
async (keys) => {
await fetchTerrainTileBatch(keys);
},
);
await Promise.all(
uniqueKeys.map(async (key) => {
const tilePromise = terrainTileCache.get(key);
if (!tilePromise) {
const [z, x, y] = key.split("/").map(Number);
resolvedTiles.set(key, await decodeTerrainTile(z, x, y));
return;
}
resolvedTiles.set(key, await tilePromise);
}),
);
return resolvedTiles;
}