mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 12:56:47 -06:00
feat: 实现矢量编辑模式下的剪切命令逻辑并添加测试
在矢量编辑模式中,剪切命令会处理选中的顶点并自动修复路径,而非直接删除整个图层。新增了专门的cut函数处理该逻辑,同时添加了vector_keys_tests测试文件,覆盖了不同场景下的剪切行为,包括Windows系统的Shift+Delete映射、选中顶点时的剪切、无选中项时的剪切以及非编辑模式下的剪切。
This commit is contained in:
@@ -219,6 +219,8 @@ mod variables_rename_tests;
|
||||
#[cfg(test)]
|
||||
mod variants_discovery_tests;
|
||||
#[cfg(test)]
|
||||
mod vector_keys_tests;
|
||||
#[cfg(test)]
|
||||
mod vector_panel_tests;
|
||||
#[cfg(test)]
|
||||
mod vector_tail_ui_tests;
|
||||
|
||||
@@ -125,7 +125,7 @@ pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, swallowed: Swallowe
|
||||
for ev in &events {
|
||||
match ev {
|
||||
Event::Copy => crate::menus::invoke(app, "edit.copy"),
|
||||
Event::Cut => crate::menus::invoke(app, "edit.cut"),
|
||||
Event::Cut => cut(app),
|
||||
Event::Paste(text) if !swallowed.drop_text => crate::menus::paste(app, Some(text), None),
|
||||
Event::Key { key, physical_key, pressed: true, modifiers, repeat }
|
||||
if key_event(app, *key, *physical_key, *modifiers, *repeat, now, ctx) =>
|
||||
@@ -137,6 +137,21 @@ pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, swallowed: Swallowe
|
||||
}
|
||||
}
|
||||
|
||||
/// Cut: usually the layers. Windows sends Shift+Delete (remove the points and heal the path)
|
||||
/// as a cut command and never as a key, so in vector edit mode the selection is points: they are
|
||||
/// removed, a heal joining the path through them.
|
||||
fn cut(app: &mut FigApp) {
|
||||
let Some(ve) = app.session.ds().ok().and_then(|d| d.vector_edit.clone()) else {
|
||||
crate::menus::invoke(app, "edit.cut");
|
||||
return;
|
||||
};
|
||||
if !ve.vertices.is_empty() {
|
||||
app.run("vector.deleteHeal", json!({}));
|
||||
} else if !ve.segments.is_empty() {
|
||||
app.run("vector.edit.delete", json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
/// The command Cmd/Ctrl+V runs with `m` held too: `edit.paste`, `edit.pasteOver` (Shift),
|
||||
/// `edit.pasteProperties` (Alt), or `None` for a chord nothing is bound to. The keymap decides,
|
||||
/// whether the key press arrived (browser) or only a paste event and the release (desktop).
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//! Keys in vector edit mode that the window's key handling twists: Windows sends Shift+Delete
|
||||
//! (delete the points and heal the path) as a cut command and no key at all, so the canvas must
|
||||
//! heal the selected points rather than cut the layer.
|
||||
|
||||
use egui::{Event, Modifiers};
|
||||
use figcraft_doc::NodeId;
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
|
||||
fn id(v: serde_json::Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn net(u: &Ui, path: NodeId) -> figcraft_doc::VectorNetwork {
|
||||
let doc = u.app.session.doc().unwrap();
|
||||
figcraft_engine::vectoredit::network(doc, path).unwrap().clone()
|
||||
}
|
||||
|
||||
/// A three-point open path A-B-C in edit mode, B selected.
|
||||
fn abc(u: &mut Ui) -> NodeId {
|
||||
let path = id(u.app.session.execute("vector.create", &json!({"points": [[100, 100], [200, 100], [300, 100]], "closed": false})).unwrap());
|
||||
u.app.session.execute("vector.edit.begin", &json!({"id": path})).unwrap();
|
||||
u.app.session.execute("vector.edit.select", &json!({"vertices": [1]})).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_delete_in_vector_edit_mode_heals_the_point_instead_of_cutting_the_layer() {
|
||||
let mut u = Ui::new(Session::new());
|
||||
let path = abc(&mut u);
|
||||
// Windows: egui-winit turns Shift+Delete into a cut command, with no key event.
|
||||
u.frame_with(vec![Event::Cut], Modifiers::SHIFT);
|
||||
assert!(u.app.session.doc().unwrap().node(path).is_some(), "the layer stays");
|
||||
let n = net(&u, path);
|
||||
assert_eq!(n.vertices.len(), 2, "the point is gone: {n:?}");
|
||||
assert_eq!(n.segments.len(), 1, "A and C are joined");
|
||||
let (a, b) = (n.vertices.first().map(|v| v.x).unwrap_or_default(), n.vertices.get(1).map(|v| v.x).unwrap_or_default());
|
||||
assert!(b - a == 200.0 && n.vertices.iter().all(|v| v.y == 0.0), "it runs straight from A to C: {n:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cut_command_in_edit_mode_takes_the_selected_points_not_the_layer() {
|
||||
let mut u = Ui::new(Session::new());
|
||||
let path = abc(&mut u);
|
||||
u.frame_with(vec![Event::Cut], Modifiers::COMMAND);
|
||||
assert!(u.app.session.doc().unwrap().node(path).is_some(), "Ctrl+X on points keeps the layer");
|
||||
assert_eq!(net(&u, path).vertices.len(), 2, "it takes the selected point");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cut_command_in_edit_mode_with_nothing_selected_keeps_the_layer() {
|
||||
let mut u = Ui::new(Session::new());
|
||||
let path = abc(&mut u);
|
||||
u.app.session.execute("vector.edit.select", &json!({"vertices": []})).unwrap();
|
||||
u.frame_with(vec![Event::Cut], Modifiers::SHIFT);
|
||||
assert!(u.app.session.doc().unwrap().node(path).is_some(), "nothing selected: nothing goes");
|
||||
assert_eq!(net(&u, path).vertices.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cut_command_outside_edit_mode_cuts_the_layer() {
|
||||
let mut s = Session::new();
|
||||
let path = id(s.execute("vector.create", &json!({"points": [[100, 100], [200, 100], [300, 100]], "closed": false})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": [path]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.frame_with(vec![Event::Cut], Modifiers::SHIFT);
|
||||
assert!(u.app.session.doc().unwrap().node(path).is_none(), "outside edit mode the selected layer is cut");
|
||||
}
|
||||
Reference in New Issue
Block a user