release: bump version to 0.49.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user