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 <noreply@anthropic.com>
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),
_ => {
+15
View File
@@ -23,3 +23,18 @@ What changed:
With 49 bodies the frame was already 2.8 ms (hover) before these changes. The remaining frame
time with the Browser shown is mostly the Browser listing every body row. GPU ID-buffer picking
was not needed at this size.
## Browser
Same model and method (400 bodies, Bodies folder open, 60 hover moves over the viewport):
| | median | p90 |
|---|---|---|
| Browser shown, before | 1.95 ms | 2.71 ms |
| Browser shown, after | 0.72 ms | 1.19 ms |
| Browser hidden | 0.30 ms | 0.72 ms |
What changed: rows scrolled out of view only reserve their height. They register no widgets
and paint nothing (no text layout, icons or eye/fold buttons), so the cost follows the rows on
screen rather than the rows in the tree. Scrolling, Find in Browser and drag and drop still see
every row's place.