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:
1 parent
8ea38bc518
commit
c8da3b78f3
5 files changed
+101
-14
No files matched your search
@@ -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 })),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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),
|
||||
_ => {
|
||||
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user