Improve map editor image previews

This commit is contained in:
2026-06-10 10:17:25 +08:00
parent 7d0f115fcb
commit bc300f4ff9
4 changed files with 105 additions and 7 deletions
+84 -4
View File
@@ -7,6 +7,8 @@ const state = {
gridSize: 10,
hitFilter: "all",
imagePreviews: new Map(),
imageLoading: new Set(),
imageFailures: new Set(),
history: [],
dragging: null,
};
@@ -121,6 +123,7 @@ async function importGraphicAsset(event) {
const image = await loadImage(previewUrl);
const texturePath = defaultGraphicTexturePath(file.name);
state.imagePreviews.set(texturePath, image);
state.imageFailures.delete(texturePath);
pushUndo("导入图形");
addGraphicSprite(texturePath, image.naturalWidth || image.width || 64, image.naturalHeight || image.height || 64);
setStatus(`已导入图形 ${file.name},并自动创建同尺寸碰撞块。请把素材放到 ${texturePath}`);
@@ -140,11 +143,58 @@ 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 queueImagePreview(texturePath) {
if (!texturePath ||
state.imagePreviews.has(texturePath) ||
state.imageLoading.has(texturePath) ||
state.imageFailures.has(texturePath)) {
return;
}
const url = previewUrlForTexture(texturePath);
if (!url) {
state.imageFailures.add(texturePath);
return;
}
state.imageLoading.add(texturePath);
try {
const image = await loadImage(url);
state.imagePreviews.set(texturePath, image);
state.imageFailures.delete(texturePath);
refresh();
} catch (error) {
state.imageFailures.add(texturePath);
setStatus(`贴图预览加载失败:${texturePath}`);
} finally {
state.imageLoading.delete(texturePath);
}
}
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;
}
function loadMapText(text, status) {
try {
state.map = parseMap(text);
state.selected = null;
state.history = [];
state.imageFailures.clear();
preloadMapImages(state.map);
fitWorldToCanvas();
setStatus(status);
refresh();
@@ -489,6 +539,7 @@ function drawLayerRect(ctx, rect) {
ctx.strokeRect(rect.bounds.x, rect.bounds.y, rect.bounds.width, rect.bounds.height);
return;
}
if (rect.texture) queueImagePreview(rect.texture);
drawFilledRect(ctx, rect.bounds, rect.color, 0.55, "#7d8794");
}
@@ -1108,7 +1159,7 @@ function rectFields(rect) {
}
function layerRectFields(item) {
return [
const fields = [
numberField("X", () => item.bounds.x, (v) => {
const next = Number.isFinite(v) ? v : item.bounds.x;
moveLinkedCollision(item, next - item.bounds.x, 0);
@@ -1129,11 +1180,27 @@ function layerRectFields(item) {
}),
textField("贴图路径", () => item.texture || "", (v) => {
const next = String(v || "").trim();
if (next) item.texture = next;
else delete item.texture;
if (next) {
item.texture = next;
state.imageFailures.delete(next);
queueImagePreview(next);
} else {
delete item.texture;
}
}),
...colorFields(item.color, true),
];
if (item.texture && item.source) {
fields.push(
numberField("源 X", () => sourceRectFor(item).x, (v) => { sourceRectFor(item).x = Math.max(0, v); }),
numberField("源 Y", () => sourceRectFor(item).y, (v) => { sourceRectFor(item).y = Math.max(0, v); }),
numberField("源 W", () => sourceRectFor(item).width, (v) => { sourceRectFor(item).width = Math.max(1, v); }),
numberField("源 H", () => sourceRectFor(item).height, (v) => { sourceRectFor(item).height = Math.max(1, v); })
);
}
fields.push(...colorFields(item.color, true));
return fields;
}
function pointFields(point) {
@@ -1161,6 +1228,19 @@ function textField(label, get, set, options = null) {
return { label, type: "text", options, get, set };
}
function sourceRectFor(item) {
if (!item.source) {
const image = item.texture ? state.imagePreviews.get(item.texture) : null;
item.source = {
x: 0,
y: 0,
width: image ? (image.naturalWidth || image.width || Math.max(1, item.bounds.width)) : Math.max(1, item.bounds.width),
height: image ? (image.naturalHeight || image.height || Math.max(1, item.bounds.height)) : Math.max(1, item.bounds.height),
};
}
return item.source;
}
function ensureLevelVisualLayer() {
if (!state.map.layers.some((layer) => layer.role === "LevelVisual")) {
state.map.layers.push({ id: "level_visual", role: "LevelVisual", parallax: 1, rects: [], tileRects: [] });