diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index daa453ec..e76e4d3b 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -1047,6 +1047,63 @@ fn occurrence_panel(app: &mut SolveApp, ctx: &egui::Context) { } } +/// Dimensions of finished sketches whose dimensions are shown (Show Dimensions), drawn over the +/// viewport like the sketch being edited draws its own (`crate::dim_view`), but not editable. +pub fn dims_overlay(app: &SolveApp, ctx: &egui::Context) { + use solvecraft_engine::geom::Vec2; + use solvecraft_engine::sketch::{DimFrame, chain_centres, default_text, dim_frame, dim_layout}; + let Some(rect) = app.viewport.rect else { return }; + if app.ui.shown_dims.is_empty() { + return; + } + let tk = Tokens::get(); + let proj = crate::viewport::projection(app, rect); + let painter = ctx.layer_painter(egui::LayerId::background()).with_clip_rect(rect); + let st = app.session.world_state(); + for id in app.ui.shown_dims.iter().filter(|id| app.session.active_sketch != Some(**id) && sketch_visible(app, **id)) { + let Some(ss) = st.sketch(*id) else { continue }; + let sk = &ss.sketch; + let to = |p: Vec2| proj.to_screen(ss.plane.to_world(p)); + // Unplaced dimensions sit away from the middle of the sketch. + let centres = chain_centres(sk); + let n = centres.len().max(1) as f64; + let middle = centres.iter().fold(Vec2::ZERO, |a, p| a + *p) * (1.0 / n); + for c in &sk.constraints { + let Some(frame) = dim_frame(sk, &c.kind) else { continue }; + let anchor = match frame { + DimFrame::Linear { p0, p1, .. } => (p0 + p1) * 0.5, + DimFrame::Radial { center, .. } | DimFrame::ArcLength { center, .. } => center, + DimFrame::Angular { vertex, .. } => vertex, + }; + let (Some(a), Some(b)) = (to(anchor), to(anchor + Vec2::X)) else { continue }; + let px = 1.0 / f64::from(a.distance(b)).max(1e-9); + if !px.is_finite() { + continue; + } + let expr = c.param.as_ref().and_then(|p| app.session.doc.param(p)).map(|p| p.expr.as_str()); + let col = if c.driven { tk.dimension_driven } else { tk.dimension }; + let galley = painter.layout_no_wrap(crate::dim_view::label(&c.kind, expr, c.driven), FontId::proportional(11.5), col); + let size = galley.size() + vec2(6.0, 2.0); + let half = Vec2::new(f64::from(size.x) * 0.5 * px, f64::from(size.y) * 0.5 * px); + let lay = dim_layout(&frame, c.text.or_else(|| default_text(&frame, middle, px)), px, half); + for line in &lay.lines { + painter.add(egui::Shape::line(line.iter().filter_map(|q| to(*q)).collect(), Stroke::new(1.0, col))); + } + for (tip, dir) in &lay.arrows { + if let (Some(t), Some(back)) = (to(*tip), to(*tip - *dir * px)) { + let d = (t - back).normalized(); + let n = vec2(-d.y, d.x); + painter.add(egui::Shape::convex_polygon(vec![t, t - d * 9.0 + n * 3.0, t - d * 9.0 - n * 3.0], col, Stroke::NONE)); + } + } + let Some(tc) = to(lay.text) else { continue }; + let r = Rect::from_center_size(tc, size); + painter.rect_filled(r, 2.0, tk.viewport_top.gamma_multiply(0.85)); + painter.galley(r.min + vec2(3.0, 1.0), galley, col); + } + } +} + /// Where the browser's panels open: the viewport's top left corner. fn panel_pos(app: &SolveApp) -> Pos2 { app.viewport.rect.map(|r| r.left_top() + vec2(16.0, 16.0)).unwrap_or(pos2(270.0, 140.0)) diff --git a/crates/ui-egui/src/context_menu.rs b/crates/ui-egui/src/context_menu.rs index e660f6bd..4d5a1fe6 100644 --- a/crates/ui-egui/src/context_menu.rs +++ b/crates/ui-egui/src/context_menu.rs @@ -359,11 +359,13 @@ fn body_items(app: &SolveApp, bodies: &[String], browser: bool) -> Vec { fn sketch_items(app: &SolveApp, id: u64) -> Vec { let visible = crate::browser::sketch_visible(app, id); let profiles = !app.ui.hidden_profiles.contains(&id); + let dims = app.ui.shown_dims.contains(&id); let editing = app.session.active_sketch == Some(id); vec![ act("ui.editSketch", "Edit Sketch", "sketch").with(json!({ "sketch": id })).on(!editing), act("ui.redefineSketch", "Redefine Sketch Plane", "plane").with(json!({ "sketch": id })), act(if visible { "ui.hideSketch" } else { "ui.showSketch" }, if visible { "Hide" } else { "Show" }, "eye").with(json!({ "sketch": id })), + act("ui.sketchDims", if dims { "Hide Dimensions" } else { "Show Dimensions" }, "dimension").with(json!({ "sketch": id })), act("ui.sketchProfile", if profiles { "Hide Profile" } else { "Show Profile" }, "").with(json!({ "sketch": id })), act("ui.rename", "Rename", "").with(json!({ "feature": id })), cmd(app, "FusionDeleteCommand", "Delete").key("Del").with(json!({ "features": [id.to_string()] })), @@ -603,6 +605,15 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) { crate::browser::set_sketch_visible(app, id, item.id == "ui.showSketch"); } } + "ui.sketchDims" => { + if let Some(id) = id_of(&p, "sketch") { + if app.ui.shown_dims.contains(&id) { + app.ui.shown_dims.retain(|x| *x != id); + } else { + app.ui.shown_dims.push(id); + } + } + } "ui.sketchProfile" => { if let Some(id) = id_of(&p, "sketch") { if app.ui.hidden_profiles.contains(&id) { @@ -895,6 +906,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { app.menu.pending = None; if target == Target::Viewport { open(app, at) } else { open_for(app, at, target) } } + crate::browser::dims_overlay(app, ctx); track_hold(app, ctx); show_rename(app, ctx); show_props(app, ctx); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 5a7c306b..7881092f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -74,6 +74,8 @@ pub struct UiState { pub shown_sketches: Vec, /// Sketches whose closed profiles are not shaded. pub hidden_profiles: Vec, + /// Finished sketches whose dimensions are shown. + pub shown_dims: Vec, /// Bodies locked in the browser (no move or delete). pub locked_bodies: Vec, } @@ -98,6 +100,7 @@ impl Default for UiState { hidden_sketches: Vec::new(), shown_sketches: Vec::new(), hidden_profiles: Vec::new(), + shown_dims: Vec::new(), locked_bodies: Vec::new(), } } diff --git a/crates/ui-egui/src/menu_tests.rs b/crates/ui-egui/src/menu_tests.rs index beb9a494..e34da427 100644 --- a/crates/ui-egui/src/menu_tests.rs +++ b/crates/ui-egui/src/menu_tests.rs @@ -106,6 +106,9 @@ fn sketch_menu_hides_and_moves_to_another_plane() { let vis = crate::browser::sketch_visible(&app, id); context_menu::run_item(&mut app, &find(&items, if vis { "Hide" } else { "Show" }), pos2(0.0, 0.0)); assert_eq!(crate::browser::sketch_visible(&app, id), !vis); + context_menu::run_item(&mut app, &find(&items, "Show Dimensions"), pos2(0.0, 0.0)); + assert_eq!(app.ui.shown_dims, vec![id]); + assert!(find(&context_menu::items(&app, &t), "Hide Dimensions").enabled); context_menu::run_item(&mut app, &find(&items, "Edit Sketch"), pos2(0.0, 0.0)); assert_eq!(app.session.active_sketch, Some(id)); app.finish_sketch();