From 6bb1f3a456e8c22b9d7414631ccdfdd23b8680aa Mon Sep 17 00:00:00 2001 From: Nicek Date: Thu, 22 Oct 2026 12:03:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=9F=A2=E9=87=8F?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=A8=A1=E5=BC=8F=E4=B8=8B=E7=9A=84=E5=89=AA?= =?UTF-8?q?=E5=88=87=E5=91=BD=E4=BB=A4=E9=80=BB=E8=BE=91=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在矢量编辑模式中,剪切命令会处理选中的顶点并自动修复路径,而非直接删除整个图层。新增了专门的cut函数处理该逻辑,同时添加了vector_keys_tests测试文件,覆盖了不同场景下的剪切行为,包括Windows系统的Shift+Delete映射、选中顶点时的剪切、无选中项时的剪切以及非编辑模式下的剪切。 --- crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/shortcuts.rs | 17 +++++- crates/ui-egui/src/vector_keys_tests.rs | 70 +++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 crates/ui-egui/src/vector_keys_tests.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e24bbf24..846bfa92 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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; diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index c86cfdf1..30267cc7 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -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). diff --git a/crates/ui-egui/src/vector_keys_tests.rs b/crates/ui-egui/src/vector_keys_tests.rs new file mode 100644 index 00000000..eb99742a --- /dev/null +++ b/crates/ui-egui/src/vector_keys_tests.rs @@ -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"); +}