Fix layered collision selection

This commit is contained in:
2026-06-10 01:30:06 +08:00
parent 8050ab2c5e
commit 8daf5d27aa
3 changed files with 50 additions and 19 deletions
+2 -2
View File
@@ -94,7 +94,7 @@ http://127.0.0.1:8787/tools/map_editor/
- Canvas 可视化 world、camera、player spawn、collision、rect、tile_rect、battle zone、spawn。
- 对象列表选择和画布点击选择。
- 网格吸附拖动对象。
- 通过“编辑目标”过滤画布命中目标,默认“智能选择”优先选中 `tile_rect` 图形,避免被重叠 `collision` 抢选
- 通过“编辑目标”过滤画布命中目标,默认“智能选择”`collision` 当作上层覆盖;重叠位置第一次点选碰撞块,再点一次切到下层 `tile_rect` 图形。
- 矩形对象支持右下角手柄缩放。
- 属性面板编辑选中对象的数值或文本。
- 新增/删除 tileset、layer、collision、tile_rect、battle zone、spawn。
@@ -137,7 +137,7 @@ assets/stage/stage_01/props/<图片文件名>
2. 已完成:能看到地面、平台、battle zone、spawn point。
3. 已完成:能新增/移动/删除一个 `collision`
4. 已完成:能新增/移动/删除一个 `tile_rect`
5. 已完成:`tile_rect``collision` 重叠时,默认智能选择可直接选中并移动铺砖区。
5. 已完成:`tile_rect``collision` 重叠时,默认智能选择选中并移动碰撞块,再次点击同一区域可切到铺砖区。
6. 已完成:能导入图形,生成 `rect_sprite` 和同尺寸 `collision`
7. 已完成:导入图形支持移动、缩放和同步调整绑定碰撞块。
8. 已完成:能撤回最近编辑操作。
+4 -4
View File
@@ -24,7 +24,7 @@ http://127.0.0.1:8787/tools/map_editor/
- 可视化世界范围、摄像机范围、玩家出生点、碰撞块、场景矩形、铺砖区、战斗区和敌人出生点。
- 从画布或对象列表选择对象。
- 在画布上拖动对象,支持网格吸附。
- 通过“编辑目标”过滤画布选择对象,默认“智能选择”会优先选中铺砖区图形,避免被重叠碰撞块挡住
- 通过“编辑目标”过滤画布选择对象,默认“智能选择”把碰撞块当作上层覆盖;同位置对象第一次点选碰撞块,再点一次切到下层铺砖区图形。
- 选中矩形对象后,拖动右下角蓝色手柄可以调整宽高。
- 通过“导入图形”选择图片,自动生成一个 `rect_sprite` 图形对象和一个同尺寸 `collision`
- 导入的图形会在浏览器内临时预览,拖动或缩放图形时,同步移动或缩放绑定的碰撞块。
@@ -43,8 +43,8 @@ http://127.0.0.1:8787/tools/map_editor/
## 调整碰撞块
1. 在左侧“编辑目标”里选择“碰撞块”。
2. 在画布上点击红色碰撞框,或从对象列表选择对应碰撞块。
1. 在左侧“编辑目标”里选择“智能选择”或“碰撞块”。
2. 在画布上点击红色碰撞框,或从对象列表选择对应碰撞块;智能选择下碰撞块会优先命中
3. 拖动碰撞块本体可以移动位置。
4. 拖动右下角蓝色手柄可以调整宽高。
5. 也可以在右侧属性面板直接修改 `X``Y``W``H`
@@ -52,7 +52,7 @@ http://127.0.0.1:8787/tools/map_editor/
## 调整铺砖区
1. 在左侧“编辑目标”里选择“智能选择”或“铺砖区”。
2. 在画布上点击铺砖区,或从对象列表选择对应铺砖区
2. 如果铺砖区和碰撞块重叠,智能选择下先点到碰撞块,再在同一区域点一次会切到铺砖区;也可以直接切到“铺砖区”
3. 拖动铺砖区本体可以移动位置。
4. 拖动右下角蓝色手柄可以调整列数和行数。
5. 如果要编辑同位置碰撞体,再把“编辑目标”切到“碰撞块”。
+44 -13
View File
@@ -731,21 +731,22 @@ function getSelectedObject() {
function onCanvasMouseDown(event) {
const world = screenToWorld(event.offsetX, event.offsetY);
const selected = getSelectedObject();
const object = hitTest(world.x, world.y);
const stack = hitStackAt(world.x, world.y);
const object = stack[0] || null;
if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) {
state.dragging = { resize: true, last: snapPoint(world) };
setCanvasCursor("nwse-resize");
return;
}
if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object)) {
state.dragging = { last: snapPoint(world) };
if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object, stack)) {
state.dragging = dragStateFor(selected, stack, world, true);
setCanvasCursor("move");
return;
}
if (object) {
state.selected = object.key;
state.dragging = { last: snapPoint(world) };
state.dragging = dragStateFor(object, stack, world, selected && sameSelection(selected.key, object.key));
setCanvasCursor("move");
refresh();
} else {
@@ -775,6 +776,7 @@ function onCanvasMouseMove(event) {
pushUndo(state.dragging.resize ? "调整对象尺寸" : "移动对象");
state.dragging.undoPushed = true;
}
state.dragging.moved = true;
if (state.dragging.resize && object.resize) {
object.resize(dx, dy);
} else if (object.move) {
@@ -786,7 +788,11 @@ function onCanvasMouseMove(event) {
}
function onCanvasMouseUp() {
const dragging = state.dragging;
state.dragging = null;
if (dragging && dragging.cycleOnClick && !dragging.moved && dragging.cycleKeys && dragging.cycleKeys.length > 1) {
cycleSelection(dragging.cycleKeys, dragging.cycleFrom);
}
setCanvasCursor("crosshair");
}
@@ -802,19 +808,23 @@ function onCanvasWheel(event) {
}
function hitTest(x, y) {
const objects = hitCandidates();
for (const object of objects) {
return hitStackAt(x, y)[0] || null;
}
function hitStackAt(x, y) {
const stack = [];
for (const object of hitCandidates()) {
const bounds = object.bounds && object.bounds();
if (!bounds) continue;
if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) return object;
if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) stack.push(object);
}
return null;
return stack;
}
function hitCandidates() {
const objects = collectObjects().filter(isHitFilterEnabled);
if (state.hitFilter !== "all") return objects.reverse();
const priority = { tile: 0, rect: 1, spawn: 2, collision: 3, zone: 4, player: 5, camera: 6 };
const priority = { collision: 0, rect: 1, tile: 2, spawn: 3, zone: 4, player: 5, camera: 6 };
return objects
.map((object, index) => ({ object, index }))
.sort((a, b) => {
@@ -832,9 +842,29 @@ function isInsideBounds(object, x, y) {
return x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height;
}
function canDragSelectedInPlace(selected, topHit) {
function canDragSelectedInPlace(selected, topHit, stack) {
if (state.hitFilter !== "all") return true;
return topHit && sameSelection(selected.key, topHit.key);
return Boolean(topHit) && stack.some((object) => sameSelection(object.key, selected.key));
}
function dragStateFor(object, stack, world, cycleOnClick) {
return {
last: snapPoint(world),
cycleKeys: stack.map((item) => item.key),
cycleFrom: object.key,
cycleOnClick,
moved: false,
};
}
function cycleSelection(cycleKeys, currentKey) {
const index = cycleKeys.findIndex((key) => sameSelection(key, currentKey));
if (index < 0) return;
const nextKey = cycleKeys[(index + 1) % cycleKeys.length];
if (!sameSelection(nextKey, state.selected)) {
state.selected = nextKey;
refresh();
}
}
function isHitFilterEnabled(object) {
@@ -846,12 +876,13 @@ function isHitFilterEnabled(object) {
function updateCanvasCursor(event) {
const world = screenToWorld(event.offsetX, event.offsetY);
const selected = getSelectedObject();
const object = hitTest(world.x, world.y);
const stack = hitStackAt(world.x, world.y);
const object = stack[0] || null;
if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) {
setCanvasCursor("nwse-resize");
return;
}
if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object)) {
if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object, stack)) {
setCanvasCursor("move");
return;
}