Section Analysis in the browser, with an eye to turn it off (#822) (#947)

A section stayed on with nothing in the browser to switch it off; only the
viewport's context menu had Remove Section. While there is one, the browser
now has an Analysis folder listing it. Its eye (and the folder's) hides the
section and shows it again on the same plane; a double-click or Edit Section
Analysis opens the dialog, and Delete removes it. Browser rows with an eye
publish an `eye:<label>` handle for automation.

Fixes #822

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-13 22:18:06 -04:00
1 parent abddd67d22
commit 15b5b51bf5
8 files changed
+187 -3

No files matched your search

+80
View File
@@ -62,6 +62,9 @@ pub struct TreeState {
pub timeline_anchor: Option<String>,
/// The Edit Canvas / Calibrate panel.
pub canvas_panel: Option<CanvasPanel>,
/// 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<String>,
/// 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<Item> {
]
}
/// 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<Item> {
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<solidcraft_engin
let own = app.session.pending_moves.get(&occurrence).copied().unwrap_or(o.transform);
Some(solidcraft_engine::doc::apply_point(&solidcraft_engine::doc::mat_mul(&parent, &own), solidcraft_engine::geom::Vec3::ZERO))
}
#[cfg(test)]
#[path = "section_browser_tests.rs"]
mod section_browser_tests;
+11 -2
View File
@@ -68,6 +68,8 @@ pub enum Target {
Canvas {
id: u64,
},
/// The Section Analysis in the browser's Analysis folder.
Section,
/// A component's Bodies, Sketches or Construction folder.
Folder {
component: u64,
@@ -261,7 +263,8 @@ pub fn target_bodies(app: &SolveApp, target: &Target) -> Vec<String> {
| 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<Item> {
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.
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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": "エンティティ",
+2
View File
@@ -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": "요소",
@@ -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");
}
+3 -1
View File
@@ -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` |