Skip to content

Commit 4bcc6de

Browse files
gajopclaude
andcommitted
Paste at the map centre when the cursor is over the panel
Paste places at the cursor's ground hit, so clicking the Paste toolbar icon (cursor over the panel) sent the paste to the world point behind the panel — off-screen, so it looked like nothing happened. Fall back to the centre of the map view whenever the cursor is over the panel; Ctrl+V with the cursor on the map still pastes under the cursor. Also split the grab-bag toolbar_actions e2e into two focused scenarios: import_action (drops a real image and asserts ImportDiffuseCommand — the one toolbar action nothing else covered) and clipboard_actions (now asserts the paste lands in the visible map centre, not off-screen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0c6dfee commit 4bcc6de

2 files changed

Lines changed: 56 additions & 28 deletions

File tree

native/src/sbc/panels/manager.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,12 +523,21 @@ impl PanelManager {
523523
self.modals.close_top(&self.interface, doc)
524524
}
525525

526-
/// Paste the clipboard at the cursor's ground hit.
526+
/// Paste the clipboard at the cursor's ground hit — or, when the cursor is
527+
/// over the panel (the toolbar Paste icon was clicked, not Ctrl+V over the
528+
/// map), at the centre of the map view, so the paste always lands somewhere
529+
/// visible instead of off-screen.
527530
fn run_paste(&mut self, models: &mut Models) {
528-
let Ok(mouse) = self.interface.input().get_mouse_state() else {
531+
const PANEL_WIDTH: f32 = 500.0;
532+
let Ok(geometry) = self.interface.display().get_view_geometry() else {
529533
return;
530534
};
531-
let Some(hit) = crate::sbc::states::trace_ground(&self.interface, mouse.x, mouse.y) else {
535+
let map_width = (geometry.viewSizeX as f32 - PANEL_WIDTH).max(1.0);
536+
let (x, y) = match self.interface.input().get_mouse_state() {
537+
Ok(mouse) if mouse.x < map_width => (mouse.x, mouse.y),
538+
_ => (map_width / 2.0, geometry.viewSizeY as f32 / 2.0),
539+
};
540+
let Some(hit) = crate::sbc::states::trace_ground(&self.interface, x, y) else {
532541
return;
533542
};
534543
let commands = actions::execute_paste(&self.interface, models, hit.x, hit.z);

tools/e2e/scenarios/shell.py

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
from __future__ import annotations
44

5+
import shutil
56
from typing import TYPE_CHECKING
67

8+
from paths import GAME_DIRNAME
79
from scenarios.geometry import (
10+
DIALOG,
811
EDITORS,
912
TAB_X,
1013
TAB_Y,
1114
TOOLBAR,
15+
dialog_point,
1216
editor_point,
1317
panel_left,
1418
panel_point,
@@ -82,54 +86,69 @@ def panel_tabs_are_choices(run_state: E2ERun) -> None:
8286

8387

8488
@scenario()
85-
def toolbar_actions(run_state: E2ERun) -> None:
86-
"""Every Rust shell-toolbar icon invokes its own action.
89+
def import_action(run_state: E2ERun) -> None:
90+
"""The Import toolbar icon picks an image and dispatches its command.
8791
88-
Project actions are opened and cancelled rather than accepted: the point is
89-
the action-to-dialog wiring, not creating files in an isolated e2e run.
90-
Clipboard actions use a selected tree and then compare the map, so a button
91-
that merely receives a click cannot pass.
92+
Import is the one toolbar action no other scenario drives (New/Load/Save
93+
As/Export are covered by gallery and map). Diffuse is the default type, so
94+
picking a file is the full path: icon -> dialog -> pick -> ImportDiffuseCommand.
9295
"""
9396
run_state.focus()
9497
left = panel_left(run_state)
9598

96-
# New Project has its own dialog. Load, Import, Save (without a project
97-
# path), Save As, and Export all use the file dialog, with distinct visible
98-
# headings/configuration. Capturing each gives the review a concise audit
99-
# trail of the icon row without mutating project state.
100-
run_state.click(*panel_point(left, TOOLBAR["new_project"]), delay=0.7)
101-
run_state.screenshot("new-project")
102-
run_state.key("Escape", delay=0.35)
99+
# Import browses springboard/projects/; drop an image there for it to pick.
100+
assert run_state.write_dir is not None
101+
projects = run_state.write_dir / "springboard" / "projects"
102+
projects.mkdir(parents=True, exist_ok=True)
103+
game_image = (
104+
run_state.write_dir
105+
/ "games"
106+
/ GAME_DIRNAME
107+
/ "LuaUI"
108+
/ "images"
109+
/ "scenedit"
110+
/ "area-add.png"
111+
)
112+
shutil.copyfile(game_image, projects / "import_test.png")
113+
114+
run_state.click(*panel_point(left, TOOLBAR["import"]), delay=0.7)
115+
run_state.screenshot("import-dialog")
116+
run_state.click(*dialog_point(run_state, DIALOG["asset_first_cell"]), delay=0.4)
117+
run_state.click(*dialog_point(run_state, DIALOG["file_ok_name"]), delay=0.6)
118+
run_state.assert_any_command("ImportDiffuseCommand")
103119

104-
for action in ("load", "import", "save", "save_as", "export"):
105-
before = run_state.screenshot(f"before-{action}")
106-
run_state.click(*panel_point(left, TOOLBAR[action]), delay=0.65)
107-
opened = run_state.screenshot(f"{action}-dialog")
108-
run_state.assert_screenshot_pixels(before, opened, min_changed=2_000)
109-
run_state.key("Escape", delay=0.35)
110120

111-
# Exact toolbar clicks, not their keyboard shortcuts. Paste needs the
112-
# cursor ground hit; move it to a distinct map point before clicking the
113-
# icon so the second tree's placement is observable.
121+
@scenario()
122+
def clipboard_actions(run_state: E2ERun) -> None:
123+
"""The Copy/Cut/Paste toolbar icons round-trip a selected object.
124+
125+
Exact toolbar clicks, not their keyboard shortcuts. Clicking the Paste icon
126+
leaves the cursor over the panel, so the paste must land at the map centre —
127+
in view — rather than off-screen behind the panel; the golden checks that
128+
centre region changed. Cut is then undone with Ctrl+Z. Each step asserts its
129+
grouped native command and a real change to the map.
130+
"""
131+
run_state.focus()
114132
left = _open(run_state, "features")
115133
_arm_tree(run_state, left)
116134
width, height = window_size(run_state)
117135
source = (width // 3 - 130, height // 2)
118-
target = (width // 3 + 220, height // 2 + 120)
119136
run_state.wheel(*source, clicks=8, up=True)
120137
run_state.click(*source, delay=0.8)
121138
run_state.key("Escape", delay=0.35)
122139
run_state.click(*source, delay=0.5)
123140

124141
run_state.click(*panel_point(left, TOOLBAR["copy"]), delay=0.4)
125142
before_paste = run_state.screenshot("copied")
126-
run_state.move(*target, delay=0.25)
127143
mark = len(run_state.commands())
128144
run_state.click(*panel_point(left, TOOLBAR["paste"]), delay=0.8)
129145
pasted = run_state.screenshot("pasted")
130146
if not any(entry["data"].get("className") == "CompoundCommand" for entry in run_state.commands()[mark:]):
131147
raise AssertionError("toolbar Paste did not dispatch a grouped native command")
132-
run_state.assert_screenshot_pixels(before_paste, pasted, min_changed=400)
148+
# The paste must appear near the centre of the map view (left of the panel),
149+
# not at the off-screen cursor over the Paste icon.
150+
centre = (left // 2 - 220, height // 2 - 220, 440, 440)
151+
run_state.assert_region_pixels(before_paste, pasted, centre, min_changed=200)
133152

134153
mark = len(run_state.commands())
135154
run_state.click(*panel_point(left, TOOLBAR["cut"]), delay=0.8)

0 commit comments

Comments
 (0)