Browser: rows out of view take only their space (400 bodies: 1.95 ms to 0.72 ms per frame, docs/perf.md); V shows/hides what is selected (bodies, sketches, planes and axes, components, canvases), Show/Hide with V in every menu that has it

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 08:56:49 +00:00
1 parent 8ea38bc518
commit c8da3b78f3
5 files changed
+101 -14

No files matched your search

+10 -6
View File
@@ -110,9 +110,7 @@ pub fn group_items(app: &SolveApp, id: u64) -> Vec<Item> {
let bodies = group_bodies(app, id);
let hidden = !bodies.is_empty() && bodies.iter().all(|b| app.ui.hidden_bodies.contains(b));
v.push(Item::sep());
v.push(
Item::action(if hidden { "ui.show" } else { "ui.hide" }, if hidden { "Show" } else { "Hide" }, "eye").with(json!({ "bodies": bodies })),
);
v.push(Item::action(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": bodies })));
v.push(Item::action("ui.isolate", "Isolate", "").with(json!({ "bodies": bodies })));
}
v
@@ -137,7 +135,7 @@ pub fn folder_items(app: &SolveApp, component: u64, folder: &str) -> Vec<Item> {
v.push(Item::action("ui.groupSelected", "Group Selected", "folder").with(json!({ "component": component, "folder": folder, "items": sel })));
}
v.push(Item::sep());
v.push(Item::action("ui.folderVisible", "Show/Hide", "eye").with(json!({ "component": component, "folder": folder })));
v.push(Item::action("ui.folderVisible", "Show/Hide", "eye").key("V").with(json!({ "component": component, "folder": folder })));
v.push(Item::action("ui.showAll", "Show All", "eye"));
v
}
@@ -147,7 +145,7 @@ pub fn origin_items(app: &SolveApp) -> Vec<Item> {
let planes = ["XY", "XZ", "YZ"].iter().all(|p| !app.ui.hidden_origin.iter().any(|h| h == p));
let axes = ["X", "Y", "Z"].iter().all(|p| !app.ui.hidden_origin.iter().any(|h| h == p));
vec![
Item::action("ui.origin", "Show/Hide", "eye"),
Item::action("ui.origin", "Show/Hide", "eye").key("V"),
Item::action("ui.originAll", "Show All", "eye"),
Item::action("ui.originPart", if planes { "Hide Planes" } else { "Show Planes" }, "plane")
.with(json!({ "keys": ["XY", "XZ", "YZ"], "show": !planes })),
@@ -252,6 +250,12 @@ struct RowResp {
fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
let t = Tokens::get();
// Rows scrolled out of view only take their space: no widgets, no painting.
let r = Rect::from_min_size(ui.cursor().min, vec2(ui.available_width(), ROW_H));
if !ui.is_rect_visible(r) {
ui.allocate_space(r.size());
return RowResp { rect: Some(r), ..Default::default() };
}
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW_H), Sense::click_and_drag());
let mut out = RowResp { rect: Some(r), hovered: resp.hovered(), ..Default::default() };
if row.selected {
@@ -678,7 +682,7 @@ pub fn canvas_items(app: &SolveApp, id: u64) -> Vec<Item> {
Item::action("ui.delete", "Delete", "delete").with(json!({ "canvases": [id] })),
Item::action("ui.rename", "Rename", "").with(json!({ "canvas": id })),
Item::sep(),
Item::action("ui.canvasVisible", if visible { "Hide" } else { "Show" }, "eye").with(json!({ "canvas": id, "visible": !visible })),
Item::action("ui.canvasVisible", "Show/Hide", "eye").key("V").with(json!({ "canvas": id, "visible": !visible })),
]
}
+51 -6
View File
@@ -89,7 +89,7 @@ impl Item {
self.enabled &= enabled;
self
}
fn key(mut self, k: &str) -> Item {
pub fn key(mut self, k: &str) -> Item {
self.shortcut = k.into();
self
}
@@ -319,7 +319,7 @@ fn viewport_items(app: &SolveApp) -> Vec<Item> {
v.push(act("ui.properties", "Properties", "measure").with(json!({ "bodies": [face] })));
v.push(Item::sep());
v.push(act("ui.delete", "Delete", "delete").key("Del"));
v.push(act(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").with(json!({ "bodies": [face] })));
v.push(act(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": [face] })));
v.push(Item::sep());
v.push(act("ui.findBrowser", "Find in Browser", "").with(json!({ "body": face })));
v.push(act("ui.findTimeline", "Find in Timeline", "").with(json!({ "body": face })));
@@ -333,7 +333,7 @@ fn viewport_items(app: &SolveApp) -> Vec<Item> {
v.push(cmd(app, "FusionChamferCommand", "Chamfer"));
v.push(Item::sep());
v.push(act("ui.delete", "Delete", "delete").key("Del"));
v.push(act(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").with(json!({ "bodies": [body] })));
v.push(act(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": [body] })));
v.push(Item::sep());
v.push(act("ui.findBrowser", "Find in Browser", "").with(json!({ "body": body })));
v.push(Item::sep());
@@ -396,7 +396,7 @@ fn body_items(app: &SolveApp, bodies: &[String], browser: bool) -> Vec<Item> {
cmd(app, "SoftDeleteCommand", "Remove").with(json!({ "bodies": names })).on(!locked),
act("ui.rename", "Rename", "").with(json!({ "body": bodies.first() })).on(one),
Item::sep(),
act(if all_hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").with(json!({ "bodies": names })),
act(if all_hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": names })),
act("ui.lock", if locked { "Unlock" } else { "Lock" }, "").with(json!({ "bodies": names })),
act("ui.isolate", "Isolate", "").with(json!({ "bodies": names })),
];
@@ -430,7 +430,7 @@ fn sketch_items(app: &SolveApp, id: u64) -> Vec<Item> {
act("ui.lookAt", "Look At", "").with(json!({ "sketch": id })),
act("ui.sketchProfile", if profiles { "Hide Profile" } else { "Show Profile" }, "").with(json!({ "sketch": id })),
act("ui.sketchDims", if dims { "Hide Dimension" } else { "Show Dimension" }, "dimension").with(json!({ "sketch": id })),
act(if visible { "ui.hideSketch" } else { "ui.showSketch" }, "Show/Hide", "eye").with(json!({ "sketch": id })),
act(if visible { "ui.hideSketch" } else { "ui.showSketch" }, "Show/Hide", "eye").key("V").with(json!({ "sketch": id })),
Item::sep(),
act("ui.findTimeline", "Find in Timeline", "").with(json!({ "feature": id })),
]
@@ -479,7 +479,7 @@ fn component_items(app: &SolveApp, id: u64) -> Vec<Item> {
}
v.push(act("ui.rename", "Rename", "").with(json!({ "component": id })));
v.push(Item::sep());
v.push(act(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").with(json!({ "bodies": names })).on(!bodies.is_empty()));
v.push(act(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": names })).on(!bodies.is_empty()));
v.push(act("ui.showAll", "Show All Bodies", "eye"));
v.push(act("ui.isolate", "Isolate", "").with(json!({ "bodies": names })).on(!bodies.is_empty() && id != 0));
v.push(act("ui.newGroup", "New Group", "folder").with(json!({ "component": id })));
@@ -807,6 +807,51 @@ pub fn delete(app: &mut SolveApp) {
crate::delete::delete_selection(app);
}
/// Show/Hide (the V key): what is selected in the viewport or picked in the browser flips its
/// visibility (bodies, faces' and edges' bodies, sketches, construction and origin planes and
/// axes, components' bodies, canvases). When some are shown and some hidden, all are hidden.
pub fn toggle_visibility(app: &mut SolveApp) {
use solvecraft_engine::doc::FeatureKind;
let mut bodies: Vec<String> = touched_bodies(app);
for c in app.tree.picked_components.clone() {
bodies.extend(component_bodies(app, c));
}
let sketches: Vec<u64> = app
.session
.selection
.iter()
.filter_map(|s| if let Sel::Feature { id } = s { Some(*id) } else { None })
.filter(|id| app.session.doc.feature(*id).is_some_and(|f| matches!(f.kind, FeatureKind::Sketch { .. })))
.collect();
let origin: Vec<String> = app
.session
.selection
.iter()
.filter_map(|s| match s {
Sel::Plane { name } | Sel::Axis { name } => Some(name.clone()),
_ => None,
})
.collect();
let canvases = app.tree.picked_canvases.clone();
let any_visible = bodies.iter().any(|b| !app.ui.hidden_bodies.contains(b))
|| sketches.iter().any(|s| crate::browser::sketch_visible(app, *s))
|| origin.iter().any(|o| !app.ui.hidden_origin.contains(o))
|| app.session.doc.canvases.iter().any(|c| canvases.contains(&c.id) && c.visible);
let hide = any_visible;
app.ui.hidden_bodies.retain(|b| !bodies.contains(b));
app.ui.hidden_origin.retain(|o| !origin.contains(o));
if hide {
app.ui.hidden_bodies.extend(bodies);
app.ui.hidden_origin.extend(origin);
}
for s in sketches {
crate::browser::set_sketch_visible(app, s, !hide);
}
for c in canvases {
let _ = app.run("canvas.edit", json!({ "canvas": c, "visible": !hide }));
}
}
/// Add the edges that continue the selected edges smoothly to the selection.
fn tangent_chain(app: &mut SolveApp) {
let st = app.session.model.state();
+24 -2
View File
@@ -183,7 +183,7 @@ fn folder_and_group_menus_make_rename_and_drop_groups() {
act(&mut app, &t, "Rename");
assert!(context_menu::finish_rename(&mut app, Some("Parts"), true));
assert_eq!(app.session.doc.browser_groups[0].name, "Parts");
act(&mut app, &t, "Hide");
act(&mut app, &t, "Show/Hide");
assert_eq!(app.ui.hidden_bodies.len(), 2);
act(&mut app, &t, "Ungroup");
assert!(app.session.doc.browser_groups.is_empty());
@@ -263,7 +263,7 @@ fn canvas_menu_hides_renames_calibrates_and_deletes() {
let data = solvecraft_engine::doc::canvas::base64_encode(&png);
let id = app.run("FusionAddCanvasCommand", json!({"data": data, "plane": "XY", "width": 80})).unwrap()["canvas"].as_u64().unwrap();
let t = Target::Canvas { id };
act(&mut app, &t, "Hide");
act(&mut app, &t, "Show/Hide");
assert!(!app.session.doc.canvases[0].visible);
act(&mut app, &t, "Rename");
assert!(context_menu::finish_rename(&mut app, Some("Photo"), true));
@@ -358,3 +358,25 @@ fn menus_follow_fusion_order() {
context_menu::run_item(&mut app, &find(&empty, "Pan"), pos2(0.0, 0.0));
assert!(matches!(app.viewport.nav, Some(crate::viewport::NavMode::Pan)));
}
#[test]
fn v_toggles_what_is_selected() {
let mut app = sample_app();
let b = body(&app);
let sk = app.session.doc.features.iter().find(|f| f.name == "Base").unwrap().id;
app.run("select.set", json!({"items": [{"type": "body", "name": b}, {"type": "plane", "name": "XY"}, {"type": "feature", "id": sk}]})).unwrap();
crate::browser::set_sketch_visible(&mut app, sk, true);
context_menu::toggle_visibility(&mut app);
assert!(app.ui.hidden_bodies.contains(&b) && app.ui.hidden_origin.iter().any(|h| h == "XY"));
assert!(!crate::browser::sketch_visible(&app, sk));
context_menu::toggle_visibility(&mut app);
assert!(app.ui.hidden_bodies.is_empty() && app.ui.hidden_origin.is_empty());
assert!(crate::browser::sketch_visible(&app, sk));
// Components picked in the browser hide their bodies.
app.run("FusionCreateComponentsFromBodiesCommand", json!({"bodies": [b.clone()]})).unwrap();
app.run("select.clear", json!({})).unwrap();
app.tree.picked_components = vec![app.session.doc.components[0].id];
context_menu::toggle_visibility(&mut app);
assert_eq!(app.ui.hidden_bodies, vec![b]);
assert_eq!(find(&context_menu::items(&app, &Target::Body { name: body(&app) }), "Show/Hide").shortcut, "V");
}
+1
View File
@@ -434,6 +434,7 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
}
}
Key::S => app.ui.palette_open = true,
Key::V => crate::context_menu::toggle_visibility(app),
Key::F6 => app.animate_view("fit"),
Key::Delete | Key::Backspace => crate::viewport::delete_selection(app),
_ => {