From aa2ace81af76076d4dee3e2035fe5a24df98f5e0 Mon Sep 17 00:00:00 2001 From: JIJINBEI <87472238+jijinbei@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:24:10 +0900 Subject: [PATCH] Right-clicking a construction plane in the browser opens its menu (#1130) Construction plane rows get a menu: Edit Feature, Delete, Rename, Show/Hide, Find in Timeline, and Group Selected for several planes (#1036). --- crates/ui-egui/src/browser.rs | 3 ++- crates/ui-egui/src/context_menu.rs | 22 +++++++++++++++ crates/ui-egui/src/i18n_menu_tests.rs | 1 + crates/ui-egui/src/menu_tests.rs | 39 +++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index 7e6ff5d0..c561da6f 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -216,6 +216,7 @@ pub fn selection_group(app: &SolveApp, target: &Target) -> Option { (b.component, "bodies") } Target::Sketch { id } => (app.session.doc.feature(*id)?.component, "sketches"), + Target::Plane { id } => (app.session.doc.feature(*id)?.component, "construction"), _ => return None, }; let sel = selected_keys(app, comp, folder); @@ -1287,7 +1288,7 @@ fn entries(app: &SolveApp, comp: u64, folder: &str) -> Vec { color: None, locked: false, sel: Some(Sel::Plane { name: f.name.clone() }), - target: None, + target: Some(Target::Plane { id: f.id }), bodies: Vec::new(), }) .collect(), diff --git a/crates/ui-egui/src/context_menu.rs b/crates/ui-egui/src/context_menu.rs index 28a5d7c1..d8211c0b 100644 --- a/crates/ui-egui/src/context_menu.rs +++ b/crates/ui-egui/src/context_menu.rs @@ -33,6 +33,10 @@ pub enum Target { Sketch { id: u64, }, + /// A construction plane (its feature id). + Plane { + id: u64, + }, Component { id: u64, }, @@ -254,6 +258,7 @@ pub fn target_bodies(app: &SolveApp, target: &Target) -> Vec { Target::Occurrence { path } => app.session.occurrence_bodies(path, true).unwrap_or_default(), Target::Group { id } => crate::browser::group_bodies(app, *id), Target::Sketch { .. } + | Target::Plane { .. } | Target::Folder { .. } | Target::Canvas { .. } | Target::Origin @@ -311,6 +316,7 @@ pub fn items(app: &SolveApp, target: &Target) -> Vec { Target::Viewport => viewport_items(app), Target::Body { name } => body_items(app, std::slice::from_ref(name), true), Target::Sketch { id } => sketch_items(app, *id), + Target::Plane { id } => plane_items(app, *id), Target::Component { id } => component_items(app, *id), Target::Occurrence { path } => occurrence_items(app, path), Target::Group { id } => crate::browser::group_items(app, *id), @@ -524,6 +530,22 @@ fn sketch_items(app: &SolveApp, id: u64) -> Vec { ] } +/// A construction plane's row in the browser: edit, delete, rename, show or hide. +fn plane_items(app: &SolveApp, id: u64) -> Vec { + let Some(f) = app.session.doc.feature(id) else { return Vec::new() }; + let visible = !app.session.visibility.hidden_origin.contains(&f.name); + vec![ + act("ui.editFeature", "Edit Feature", "plane").with(json!({ "feature": id })), + Item::sep(), + act("ui.delete", "Delete", "delete").key("Del").with(json!({ "items": [{"type": "feature", "id": id}] })), + act("ui.rename", "Rename", "").with(json!({ "feature": id })), + Item::sep(), + act("ui.originPart", "Show/Hide", "eye").key("V").with(json!({ "keys": [f.name], "show": !visible })), + Item::sep(), + act("ui.findTimeline", "Find in Timeline", "").with(json!({ "feature": id })), + ] +} + /// An assembly joint: edit, drive, suppress, delete. fn joint_items(app: &SolveApp, id: u64) -> Vec { let Some(j) = app.session.doc.assembly.joints.iter().find(|j| j.id == id) else { return Vec::new() }; diff --git a/crates/ui-egui/src/i18n_menu_tests.rs b/crates/ui-egui/src/i18n_menu_tests.rs index 2f292610..0b486dcf 100644 --- a/crates/ui-egui/src/i18n_menu_tests.rs +++ b/crates/ui-egui/src/i18n_menu_tests.rs @@ -49,6 +49,7 @@ fn take(app: &SolveApp, out: &mut BTreeSet) { } for id in &features { take_items(out, &context_menu::items(app, &Target::Sketch { id: *id })); + take_items(out, &context_menu::items(app, &Target::Plane { id: *id })); } for id in &components { take_items(out, &context_menu::items(app, &Target::Component { id: *id })); diff --git a/crates/ui-egui/src/menu_tests.rs b/crates/ui-egui/src/menu_tests.rs index 95b01dc4..f9449df4 100644 --- a/crates/ui-egui/src/menu_tests.rs +++ b/crates/ui-egui/src/menu_tests.rs @@ -667,3 +667,42 @@ fn the_workspace_menu_lists_design_and_greys_the_rest() { assert!(!find(&items, "Render").enabled && find(&items, "Render").shortcut == "coming later"); assert!(!items.iter().any(|i| i.label.contains("Generative"))); } + +/// A right-click on a construction plane's browser row opens its menu, like a body's or a +/// sketch's row, and the menu's items act on that plane. +#[test] +fn construction_plane_row_opens_its_menu() { + let mut h = crate::scenario::Harness::new(); + h.app.run("solid.box", json!({"length": 40, "width": 40, "height": 10, "body_name": "Block"})).unwrap(); + h.app.run("construct.plane.offset", json!({"base": "XY", "offset": 25, "name": "Cut"})).unwrap(); + let id = h.app.session.doc.find_feature("Cut").map(|f| f.id).unwrap(); + h.app.run("select.clear", json!({})).unwrap(); + h.frames(3); + let at = crate::scenario::handle_at("row:Cut").expect("the plane has a browser row"); + h.call("ui.click", json!({"x": at.x, "y": at.y, "button": "right"})); + h.frames(2); + let menu = h.call("ui.menu", json!({})); + let menu = &menu["result"]; + assert_eq!(menu["open"], true, "{menu}"); + assert_eq!(menu["target"], json!({"type": "plane", "id": id}), "{menu}"); + let labels: Vec<&str> = menu["items"].as_array().unwrap().iter().filter_map(|i| i["label"].as_str()).filter(|l| !l.is_empty()).collect(); + assert_eq!(labels, ["Edit Feature", "Delete", "Rename", "Show/Hide", "Find in Timeline"]); + assert_eq!(h.app.session.selection, vec![solidcraft_engine::Sel::Plane { name: "Cut".into() }], "the right-click picks the plane"); + context_menu::close(&mut h.app); + + let t = Target::Plane { id }; + act(&mut h.app, &t, "Show/Hide"); + assert!(h.app.session.visibility.hidden_origin.contains(&"Cut".to_string())); + act(&mut h.app, &t, "Show/Hide"); + assert!(!h.app.session.visibility.hidden_origin.contains(&"Cut".to_string())); + act(&mut h.app, &t, "Rename"); + assert!(context_menu::finish_rename(&mut h.app, Some("Section"), true)); + assert!(h.app.session.doc.find_feature("Section").is_some_and(|f| f.id == id)); + act(&mut h.app, &t, "Find in Timeline"); + assert_eq!(h.app.session.selection, vec![solidcraft_engine::Sel::Feature { id }]); + act(&mut h.app, &t, "Edit Feature"); + assert!(h.app.dialog.is_some(), "Edit Feature opens the plane's dialog"); + h.app.dialog = None; + act(&mut h.app, &t, "Delete"); + assert!(h.app.session.doc.feature(id).is_none(), "nothing depends on the plane: it goes at once"); +}