diff --git a/tools/map_editor/app.js b/tools/map_editor/app.js index 83425a9..9cd4069 100644 --- a/tools/map_editor/app.js +++ b/tools/map_editor/app.js @@ -9,6 +9,7 @@ const state = { imagePreviews: new Map(), imageLoading: new Set(), imageFailures: new Set(), + previewLoadToken: 0, history: [], dragging: null, }; @@ -143,26 +144,63 @@ function loadImage(url) { }); } -function preloadMapImages(map) { - for (const layer of map.layers) { - for (const rect of layer.rects) { - if (rect.texture) queueImagePreview(rect.texture); +async function preloadMapImages(map) { + const textures = uniqueMapTextures(map); + if (!textures.length) return; + + const token = state.previewLoadToken + 1; + state.previewLoadToken = token; + setStatus(`正在加载贴图预览 0/${textures.length}`); + + let loaded = 0; + let failed = 0; + await Promise.all(textures.map(async (texturePath) => { + const ok = await queueImagePreview(texturePath, { refreshAfterLoad: false }); + if (ok) loaded += 1; + else failed += 1; + if (state.previewLoadToken === token) { + setStatus(`正在加载贴图预览 ${loaded + failed}/${textures.length}`); } + })); + + if (state.previewLoadToken !== token) return; + refresh(); + if (failed) { + setStatus(`贴图预览完成:成功 ${loaded},失败 ${failed}`); + } else { + setStatus(`贴图预览完成:${loaded}/${textures.length}`); } } -async function queueImagePreview(texturePath) { +function uniqueMapTextures(map) { + const textures = []; + const seen = new Set(); + for (const layer of map.layers) { + for (const rect of layer.rects) { + if (!rect.texture || seen.has(rect.texture)) continue; + seen.add(rect.texture); + textures.push(rect.texture); + } + } + return textures; +} + +async function queueImagePreview(texturePath, options = {}) { if (!texturePath || - state.imagePreviews.has(texturePath) || - state.imageLoading.has(texturePath) || - state.imageFailures.has(texturePath)) { - return; + state.imagePreviews.has(texturePath)) { + return true; + } + if (state.imageLoading.has(texturePath)) { + return false; + } + if (state.imageFailures.has(texturePath)) { + return false; } const url = previewUrlForTexture(texturePath); if (!url) { state.imageFailures.add(texturePath); - return; + return false; } state.imageLoading.add(texturePath); @@ -170,10 +208,12 @@ async function queueImagePreview(texturePath) { const image = await loadImage(url); state.imagePreviews.set(texturePath, image); state.imageFailures.delete(texturePath); - refresh(); + if (options.refreshAfterLoad !== false) refresh(); + return true; } catch (error) { state.imageFailures.add(texturePath); setStatus(`贴图预览加载失败:${texturePath}`); + return false; } finally { state.imageLoading.delete(texturePath); } @@ -183,9 +223,14 @@ function previewUrlForTexture(texturePath) { const clean = String(texturePath || "").replace(/\\/g, "/").replace(/^\/+/, ""); if (!clean) return ""; if (/^(blob:|data:|https?:\/\/)/i.test(clean)) return clean; - if (clean.startsWith("assets/")) return `../../game/${clean}`; - if (clean.startsWith("game/assets/")) return `../../${clean}`; - return clean; + if (clean.startsWith("assets/")) return cacheBustedUrl(`${location.origin}/game/${clean}`); + if (clean.startsWith("game/assets/")) return cacheBustedUrl(`${location.origin}/${clean}`); + return cacheBustedUrl(clean); +} + +function cacheBustedUrl(url) { + const separator = url.includes("?") ? "&" : "?"; + return `${url}${separator}preview=${Date.now()}`; } function loadMapText(text, status) { @@ -194,10 +239,10 @@ function loadMapText(text, status) { state.selected = null; state.history = []; state.imageFailures.clear(); - preloadMapImages(state.map); fitWorldToCanvas(); setStatus(status); refresh(); + preloadMapImages(state.map); } catch (error) { setStatus(`解析失败:${error.message}`); }