diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index 175703cf..c2544ef0 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -62,6 +62,9 @@ pub struct TreeState { pub timeline_anchor: Option, /// The Edit Canvas / Calibrate panel. pub canvas_panel: Option, + /// A Section Analysis hidden with its eye in the Analysis folder: its plane (origin, + /// normal), so the eye can bring it back (#822). + pub hidden_section: Option<(solidcraft_engine::geom::Vec3, solidcraft_engine::geom::Vec3)>, /// What waits for the Capture Position question: a command id, or [`SAVE`]. pub capture_prompt: Option, /// The last row clicked (for Shift ranges). @@ -354,6 +357,7 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp { ui.painter().line_segment([er.left_bottom() + vec2(2.0, -2.0), er.right_top() + vec2(-2.0, 2.0)], Stroke::new(1.4, t.text_dim)); } out.eye = resp.clicked() && click_on == 2; + crate::scenario::publish_handle(&format!("eye:{}", row.label), er.center()); } x += 18.0; // Activation radio (components). @@ -680,6 +684,7 @@ fn component_rows(app: &mut SolveApp, ui: &mut egui::Ui, id: u64, depth: usize, named_view_rows(app, ui, d, acts); origin_rows(app, ui, d); canvas_rows(app, ui, d, acts); + analysis_rows(app, ui, d, acts); } let _ = t; for folder in ["bodies", "sketches", "construction"] { @@ -952,6 +957,77 @@ pub fn view_items(app: &SolveApp, name: &str) -> Vec { ] } +/// The Analysis folder: the Section Analysis, while there is one (shown or hidden), with an eye +/// to hide and show it, a double-click to edit it and a menu to delete it (#822). +fn analysis_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut Actions) { + if app.session.section.is_some() { + // A section made (or remade) while an older one was hidden replaces it. + app.tree.hidden_section = None; + } + let visible = app.session.section.is_some(); + if !visible && app.tree.hidden_section.is_none() { + return; + } + let key = "analysis"; + let open = is_open(app, key, true); + let r = draw_row( + ui, + ui.id().with(key), + &Row { depth, fold: Some(open), eye: Some(visible), icon: "folder", label: "Analysis", ..Default::default() }, + ); + if r.fold || r.clicked { + toggle(app, key, true); + } + if r.eye { + set_section_visible(app, !visible); + } + if !open { + return; + } + let r = draw_row( + ui, + ui.id().with("section_analysis"), + &Row { depth: depth + 1, eye: Some(visible), icon: "section", label: "Section Analysis", dim: !visible, ..Default::default() }, + ); + if r.eye { + set_section_visible(app, !visible); + } + if r.double { + app.start("inspect.section"); + } + if let Some(p) = r.secondary { + acts.menu = Some((p, Target::Section)); + } +} + +/// Hide the Section Analysis (keeping its plane to show it again) or show the hidden one. +pub fn set_section_visible(app: &mut SolveApp, visible: bool) { + if visible { + if let Some((origin, normal)) = app.tree.hidden_section { + let plane = json!({"origin": [origin.x, origin.y, origin.z], "normal": [normal.x, normal.y, normal.z]}); + if app.run("inspect.section", json!({ "plane": plane })).is_ok() { + app.tree.hidden_section = None; + } + } + } else if let Some(section) = app.session.section + && app.run("inspect.section", json!({"clear": true})).is_ok() + { + app.tree.hidden_section = Some(section); + } +} + +/// The menu of the Section Analysis row. +pub fn section_items(app: &SolveApp) -> Vec { + let visible = app.session.section.is_some(); + vec![ + Item::action("ui.editSection", "Edit Section Analysis", "section"), + Item::sep(), + Item::action("ui.unsection", "Delete", "delete"), + Item::sep(), + Item::action("ui.sectionVisible", "Show/Hide", "eye").key("V").with(json!({ "visible": !visible })), + ] +} + /// The Canvases folder (reference images) with an eye per canvas. fn canvas_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut Actions) { let canvases: Vec<(u64, String, bool)> = app.session.doc.canvases.iter().map(|c| (c.id, c.name.clone(), c.visible)).collect(); @@ -1883,3 +1959,7 @@ fn occurrence_origin(app: &SolveApp, occurrence: u64) -> Option Vec { | Target::Toolbar | Target::Workspace | Target::NamedViews - | Target::NamedView { .. } => Vec::new(), + | Target::NamedView { .. } + | Target::Section => Vec::new(), } } @@ -315,6 +318,7 @@ pub fn items(app: &SolveApp, target: &Target) -> Vec { Target::Canvas { id } => crate::browser::canvas_items(app, *id), Target::Origin => crate::browser::origin_items(app), Target::NamedViews => vec![act("ui.newView", "New Named View", "perspective")], + Target::Section => crate::browser::section_items(app), Target::Joint { id } => joint_items(app, *id), Target::ToolbarCommand { id } => crate::toolbar_custom::command_items(app, id), Target::Toolbar => crate::toolbar_custom::toolbar_items(app), @@ -858,7 +862,12 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) { "ui.tangentChain" => tangent_chain(app), "ui.origin" => app.ui.show_origin = !app.ui.show_origin, "ui.clear" => drop(app.run("select.clear", json!({}))), - "ui.unsection" => drop(app.run("inspect.section", json!({"clear": true}))), + "ui.unsection" => { + app.tree.hidden_section = None; + drop(app.run("inspect.section", json!({"clear": true}))); + } + "ui.editSection" => app.start("inspect.section"), + "ui.sectionVisible" => crate::browser::set_section_visible(app, p.get("visible").and_then(Value::as_bool).unwrap_or(true)), "ui.fit" => app.animate_view("fit"), "ui.home" => app.animate_view("home"), // One command for the whole selection: all fixed unless all already are. diff --git a/crates/ui-egui/src/locales/de.json b/crates/ui-egui/src/locales/de.json index 9309d630..fcb5770f 100644 --- a/crates/ui-egui/src/locales/de.json +++ b/crates/ui-egui/src/locales/de.json @@ -27,6 +27,7 @@ "ui.agent_cursor": "Agent-Cursor", "ui.agent_cursor_speed": "Agent-Cursor-Geschwindigkeit", "ui.all": "Alle", + "ui.analysis": "Analyse", "ui.angle": "Winkel", "ui.angle_decimals": "Winkel-Dezimalstellen", "ui.animate_view_changes": "Ansichtsänderungen animieren", @@ -100,6 +101,7 @@ "ui.drawing": "Zeichnung", "ui.drill_point": "Bohrpunkt", "ui.edit": "Bearbeiten", + "ui.edit_section_analysis": "Querschnittsanalyse bearbeiten", "ui.ellipse": "Ellipse", "ui.end_radius": "Endradius", "ui.entities": "Elemente", diff --git a/crates/ui-egui/src/locales/en.json b/crates/ui-egui/src/locales/en.json index f20420d1..d7ff60a9 100644 --- a/crates/ui-egui/src/locales/en.json +++ b/crates/ui-egui/src/locales/en.json @@ -285,6 +285,7 @@ "ui.agent_cursor": "Agent cursor", "ui.agent_cursor_speed": "Agent cursor speed", "ui.all": "All", + "ui.analysis": "Analysis", "ui.angle": "Angle", "ui.angle_decimals": "Angle decimals", "ui.animate_view_changes": "Animate view changes", @@ -362,6 +363,7 @@ "ui.drawing": "Drawing", "ui.drill_point": "Drill Point", "ui.edit": "Edit", + "ui.edit_section_analysis": "Edit Section Analysis", "ui.ellipse": "Ellipse", "ui.end_radius": "End Radius", "ui.entities": "Entities", diff --git a/crates/ui-egui/src/locales/ja.json b/crates/ui-egui/src/locales/ja.json index 67ffc51e..4e4938ec 100644 --- a/crates/ui-egui/src/locales/ja.json +++ b/crates/ui-egui/src/locales/ja.json @@ -285,6 +285,7 @@ "ui.agent_cursor": "エージェントカーソル", "ui.agent_cursor_speed": "エージェントカーソル速度", "ui.all": "すべて", + "ui.analysis": "解析", "ui.angle": "角度", "ui.angle_decimals": "角度の小数点以下の桁数", "ui.animate_view_changes": "視点変更をアニメーション表示", @@ -362,6 +363,7 @@ "ui.drawing": "図面", "ui.drill_point": "ドリル先端", "ui.edit": "編集", + "ui.edit_section_analysis": "断面解析を編集", "ui.ellipse": "楕円", "ui.end_radius": "終了半径", "ui.entities": "エンティティ", diff --git a/crates/ui-egui/src/locales/ko.json b/crates/ui-egui/src/locales/ko.json index 4ae90448..21326205 100644 --- a/crates/ui-egui/src/locales/ko.json +++ b/crates/ui-egui/src/locales/ko.json @@ -27,6 +27,7 @@ "ui.agent_cursor": "에이전트 커서", "ui.agent_cursor_speed": "에이전트 커서 속도", "ui.all": "모두", + "ui.analysis": "분석", "ui.angle": "각도", "ui.angle_decimals": "각도 소수점 자릿수", "ui.animate_view_changes": "뷰 변경 애니메이션", @@ -100,6 +101,7 @@ "ui.drawing": "도면", "ui.drill_point": "드릴 선단", "ui.edit": "편집", + "ui.edit_section_analysis": "단면 분석 편집", "ui.ellipse": "타원", "ui.end_radius": "끝 반지름", "ui.entities": "요소", diff --git a/crates/ui-egui/src/section_browser_tests.rs b/crates/ui-egui/src/section_browser_tests.rs new file mode 100644 index 00000000..ca84efac --- /dev/null +++ b/crates/ui-egui/src/section_browser_tests.rs @@ -0,0 +1,85 @@ +//! Section Analysis in the browser (#822): an Analysis folder lists it with an eye to hide and +//! show it, and its menu edits or deletes it. + +use egui::pos2; +use serde_json::json; + +use crate::context_menu::{self, Target}; +use crate::scenario::Harness; + +fn harness() -> Harness { + let mut h = Harness::new(); + h.app.home.open = false; + h.app.ui.show_browser = true; + h.frames(3); + h +} + +fn click(h: &mut Harness, handle: &str) { + assert!(crate::scenario::handle_at(handle).is_some(), "{handle} is not drawn: {:?}", crate::scenario::handles("")); + h.step(&json!({"click": {"handle": handle}})).unwrap(); + h.frames(3); +} + +fn menu(h: &mut Harness, label: &str) { + let items = context_menu::items(&h.app, &Target::Section); + let item = items.iter().find(|i| i.label == label).unwrap_or_else(|| panic!("no {label} in {items:?}")).clone(); + context_menu::run_item(&mut h.app, &item, pos2(0.0, 0.0)); + h.frames(3); +} + +#[test] +fn no_analysis_folder_without_a_section() { + let h = harness(); + assert!(crate::scenario::handle_at("row:Analysis").is_none()); + assert!(crate::scenario::handle_at("row:Section Analysis").is_none()); + drop(h); +} + +#[test] +fn the_eye_hides_and_shows_the_section_and_delete_removes_it() { + let mut h = harness(); + h.app.run("inspect.section", json!({"plane": "XZ", "offset": 15})).unwrap(); + h.frames(3); + let section = h.app.session.section.expect("a section"); + assert!(crate::scenario::handle_at("row:Section Analysis").is_some(), "the section is listed under Analysis"); + + click(&mut h, "eye:Section Analysis"); + assert_eq!(h.app.session.section, None, "the eye turns the section off"); + assert!(crate::scenario::handle_at("row:Section Analysis").is_some(), "a hidden section stays listed"); + + click(&mut h, "eye:Section Analysis"); + let back = h.app.session.section.expect("the eye turns it back on"); + assert!((back.0 - section.0).len() < 1e-9 && (back.1 - section.1).len() < 1e-9, "the same plane: {back:?} vs {section:?}"); + + // The folder's eye does the same. + click(&mut h, "eye:Analysis"); + assert_eq!(h.app.session.section, None); + menu(&mut h, "Show/Hide"); + assert!(h.app.session.section.is_some(), "Show/Hide in the menu brings it back"); + + menu(&mut h, "Delete"); + assert_eq!(h.app.session.section, None); + assert!(h.app.tree.hidden_section.is_none()); + assert!(crate::scenario::handle_at("row:Section Analysis").is_none(), "a deleted section leaves the browser"); +} + +#[test] +fn deleting_a_hidden_section_forgets_it() { + let mut h = harness(); + h.app.run("inspect.section", json!({"plane": "XY", "offset": 2})).unwrap(); + h.frames(3); + click(&mut h, "eye:Section Analysis"); + menu(&mut h, "Delete"); + assert!(h.app.session.section.is_none() && h.app.tree.hidden_section.is_none()); + assert!(crate::scenario::handle_at("row:Analysis").is_none()); +} + +#[test] +fn edit_opens_the_section_dialog() { + let mut h = harness(); + h.app.run("inspect.section", json!({"plane": "YZ"})).unwrap(); + h.frames(3); + menu(&mut h, "Edit Section Analysis"); + assert!(h.app.dialog.is_some(), "the Section Analysis dialog opens"); +} diff --git a/docs/i18n_catalog_ja.md b/docs/i18n_catalog_ja.md index 12edf234..2a7c24d0 100644 --- a/docs/i18n_catalog_ja.md +++ b/docs/i18n_catalog_ja.md @@ -1,4 +1,4 @@ -# SolidCraft 英語・日本語 ローカライズ対照リスト (全 844 件) +# SolidCraft 英語・日本語 ローカライズ対照リスト (全 846 件) | 英語 (English) | 日本語 (Japanese) | キー名 (Key) | | :--- | :--- | :--- | @@ -50,6 +50,7 @@ | All Sketch Points | すべてのスケッチ点 | `All Sketch Points` | | Allowed expressions, separated by semicolons | 使用可能な式(セミコロンで区切る) | `Allowed expressions, separated by semicolons` | | Also deletes | 同時に削除される要素: | `Also deletes` | +| Analysis | 解析 | `ui.analysis` | | Angle | 角度 | `ui.angle` | | Angle 2 | 角度 2 | `Angle 2` | | Angle decimals | 角度の小数点以下の桁数 | `ui.angle_decimals` | @@ -290,6 +291,7 @@ | Edit Joint | ジョイントを編集 | `cmd.joint_edit` | | Edit Physical Material | 物理マテリアルを編集 | `cmd.material_edit` | | Edit Profile Sketch | プロファイルスケッチを編集 | `Edit Profile Sketch` | +| Edit Section Analysis | 断面解析を編集 | `ui.edit_section_analysis` | | Edit Sketch | スケッチを編集 | `cmd.sketch_edit` | | Edit Text | テキストを編集 | `cmd.sketch_edit_text` | | Ellipse | 楕円 | `ui.ellipse` |