Fix layered collision selection
This commit is contained in:
@@ -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. 已完成:能撤回最近编辑操作。
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user