diff --git a/apps/solidcraft-cli/src/main.rs b/apps/solidcraft-cli/src/main.rs index 8a93eac0..80cfce92 100644 --- a/apps/solidcraft-cli/src/main.rs +++ b/apps/solidcraft-cli/src/main.rs @@ -4,7 +4,7 @@ //! solidcraft-cli run [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--json-out FILE] [--allow-errors] //! solidcraft-cli eval [--json-out FILE] (alias: inspect) measurements as JSON //! solidcraft-cli script [--out script.json] the design's history as a command script -//! solidcraft-cli bench-open [--parse-only] loading timings as JSON lines +//! solidcraft-cli bench-open [--parse-only] [--display LEVEL [--edit NAME EXPR]] loading timings as JSON lines //! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed //! solidcraft-cli snapshot --out shot.png [--width W] [--height H] [--view iso|front|top|…] //! solidcraft-cli exec [json-params] run one command on an empty design @@ -45,7 +45,7 @@ const USAGE: &str = "usage: as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark) solidcraft-cli script [--out script.json] (the design's history as a command script that eval and run replay) - solidcraft-cli bench-open [--parse-only] + solidcraft-cli bench-open [--parse-only] [--display LEVEL [--edit NAME EXPR]] solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json] solidcraft-cli snapshot --out shot.png [--width W] [--height H] [--view iso|front|back|top|bottom|left|right] solidcraft-cli render --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS] @@ -500,5 +500,37 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> { st.warm_meshes(|| true); let triangles: usize = st.bodies.iter().map(|b| b.mesh().triangles.len()).sum(); println!("{}", json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "bodies": st.bodies.len(), "triangles": triangles})); + // `--display LEVEL`: the display plans the viewport draws at that display quality, made as + // the GPU viewport makes them; `--edit NAME EXPR` then changes a parameter and times the + // evaluation and the plans of the bodies it changed. + let flag = |name: &str| args.iter().position(|a| a == name); + let Some(level) = flag("--display").and_then(|i| args.get(i + 1)) else { return Ok(()) }; + let q = solidcraft_engine::doc::display_quality::DisplayQuality::parse(level).ok_or("--display: coarse, normal, fine or ultra")?; + let plans = |st: &solidcraft_engine::doc::ModelState| { + let t = std::time::Instant::now(); + solidcraft_engine::kernel::for_each_on_threads(&st.bodies, 16, |b| { + let _ = b.plan_mesh_at(q); + }); + let triangles: usize = st.bodies.iter().filter_map(|b| b.plan_mesh_at(q)).map(|m| m.triangles.len()).sum(); + (t.elapsed().as_secs_f64() * 1000.0, triangles) + }; + let (ms, triangles) = plans(&st); + println!("{}", json!({"step": "display", "quality": q.id(), "ms": ms, "triangles": triangles})); + if let Some(i) = flag("--edit") { + let (Some(name), Some(expr)) = (args.get(i + 1), args.get(i + 2)) else { return Err("--edit NAME EXPR".into()) }; + let mut s = s; + let t = std::time::Instant::now(); + s.execute("parameters.change", &json!({"name": name, "expression": expr})).map_err(|e| e.to_string())?; + let eval_ms = t.elapsed().as_secs_f64() * 1000.0; + let st = s.world_state(); + let t = std::time::Instant::now(); + st.warm_meshes(|| true); + let mesh_ms = t.elapsed().as_secs_f64() * 1000.0; + let (ms, triangles) = plans(&st); + println!( + "{}", + json!({"step": "edit", "quality": q.id(), "evaluate_ms": eval_ms, "mesh_ms": mesh_ms, "display_ms": ms, "triangles": triangles}) + ); + } Ok(()) } diff --git a/assets/locales/de.json b/assets/locales/de.json index b136916c..e3263f9d 100644 --- a/assets/locales/de.json +++ b/assets/locales/de.json @@ -49,6 +49,12 @@ "ui.animation": "Animation", "ui.animation_speed": "Animationsgeschwindigkeit", "ui.anti_aliasing": "Kantenglättung", + "ui.display_quality": "Darstellungsqualität", + "ui.display_quality_coarse": "Grob", + "ui.display_quality_normal": "Normal", + "ui.display_quality_fine": "Fein", + "ui.display_quality_ultra": "Ultra", + "settings.display_quality_note": "Feinere Stufen zeigen gekrümmte Flächen und Kanten glatter und brauchen länger zum Vernetzen. Nur die Ansicht ändert sich, nie das Design oder Exporte.", "ui.apply": "Anwenden", "ui.arc": "Kreisbogen", "ui.assemble": "Zusammenbau", diff --git a/assets/locales/en.json b/assets/locales/en.json index 71c95196..6fec997c 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -327,6 +327,12 @@ "ui.animation": "Animation", "ui.animation_speed": "Animation speed", "ui.anti_aliasing": "Anti-aliasing", + "ui.display_quality": "Display quality", + "ui.display_quality_coarse": "Coarse", + "ui.display_quality_normal": "Normal", + "ui.display_quality_fine": "Fine", + "ui.display_quality_ultra": "Ultra", + "settings.display_quality_note": "Finer levels draw curved faces and edges smoother and take longer to mesh. Only the viewport changes, never the design or exports.", "ui.apply": "Apply", "ui.arc": "Arc", "ui.assemble": "Assemble", diff --git a/assets/locales/ja.json b/assets/locales/ja.json index a47fec71..74b6efda 100644 --- a/assets/locales/ja.json +++ b/assets/locales/ja.json @@ -327,6 +327,12 @@ "ui.animation": "アニメーション", "ui.animation_speed": "アニメーション速度", "ui.anti_aliasing": "アンチエイリアス", + "ui.display_quality": "表示品質", + "ui.display_quality_coarse": "粗い", + "ui.display_quality_normal": "標準", + "ui.display_quality_fine": "細かい", + "ui.display_quality_ultra": "最高", + "settings.display_quality_note": "細かいほど曲面とエッジが滑らかに表示されますが、メッシュの生成に時間がかかります。変わるのはビューポートだけで、設計やエクスポートは変わりません。", "ui.apply": "適用", "ui.arc": "円弧", "ui.assemble": "アセンブリ", diff --git a/assets/locales/ko.json b/assets/locales/ko.json index a703fcc6..33f28d53 100644 --- a/assets/locales/ko.json +++ b/assets/locales/ko.json @@ -49,6 +49,12 @@ "ui.animation": "애니메이션", "ui.animation_speed": "애니메이션 속도", "ui.anti_aliasing": "안티앨리어싱", + "ui.display_quality": "표시 품질", + "ui.display_quality_coarse": "거침", + "ui.display_quality_normal": "보통", + "ui.display_quality_fine": "세밀", + "ui.display_quality_ultra": "최고", + "settings.display_quality_note": "세밀할수록 곡면과 모서리가 더 매끄럽게 표시되지만 메시 생성 시간이 늘어납니다. 뷰포트만 바뀌며 설계나 내보내기는 바뀌지 않습니다.", "ui.apply": "적용", "ui.arc": "호", "ui.assemble": "조립", diff --git a/assets/locales/zh.json b/assets/locales/zh.json index d02b80aa..088ae73f 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -326,6 +326,12 @@ "ui.animation": "动画", "ui.animation_speed": "动画速度", "ui.anti_aliasing": "抗锯齿", + "ui.display_quality": "显示质量", + "ui.display_quality_coarse": "粗糙", + "ui.display_quality_normal": "标准", + "ui.display_quality_fine": "精细", + "ui.display_quality_ultra": "极高", + "settings.display_quality_note": "级别越精细,曲面和边显示越平滑,但网格生成耗时更长。只影响视口,不影响设计或导出。", "ui.apply": "应用", "ui.arc": "圆弧", "ui.assemble": "装配", diff --git a/crates/doc/src/display_quality.rs b/crates/doc/src/display_quality.rs new file mode 100644 index 00000000..d6795b86 --- /dev/null +++ b/crates/doc/src/display_quality.rs @@ -0,0 +1,143 @@ +//! How finely the viewport tessellates curved faces and edges (Preferences ▸ Graphics ▸ Display +//! quality). A viewing preference, not design data: it only picks the tolerances of the display +//! plan ([`crate::ModelBody::display_plan`]) the viewport draws. The reference mesh the model +//! reads (picks, holes, measuring) and exports keep their own tolerances. + +use std::sync::atomic::{AtomicU8, Ordering}; + +/// A display quality level. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum DisplayQuality { + Coarse, + #[default] + Normal, + Fine, + Ultra, +} + +impl DisplayQuality { + pub const ALL: [DisplayQuality; 4] = [DisplayQuality::Coarse, DisplayQuality::Normal, DisplayQuality::Fine, DisplayQuality::Ultra]; + + /// The id kept in the settings file. + pub fn id(self) -> &'static str { + match self { + DisplayQuality::Coarse => "coarse", + DisplayQuality::Normal => "normal", + DisplayQuality::Fine => "fine", + DisplayQuality::Ultra => "ultra", + } + } + + pub fn parse(id: &str) -> Option { + Self::ALL.into_iter().find(|q| q.id() == id) + } + + /// Position in [`DisplayQuality::ALL`] (indexes the per-level display caches). + pub fn index(self) -> usize { + self as usize + } + + /// Chord tolerance as a fraction of the body's size: the largest gap allowed between a + /// facet and its surface. + pub fn chord_fraction(self) -> f64 { + match self { + DisplayQuality::Coarse => 3e-3, + DisplayQuality::Normal => 1e-3, + DisplayQuality::Fine => 3e-4, + DisplayQuality::Ultra => 1e-4, + } + } + + /// Smallest chord tolerance (mm), so a tiny body does not mesh down to nothing. + pub fn min_chord(self) -> f64 { + match self { + DisplayQuality::Coarse => 3e-3, + DisplayQuality::Normal => 1e-3, + DisplayQuality::Fine => 3e-4, + DisplayQuality::Ultra => 1e-4, + } + } + + /// Most a facet of a curved face or edge may turn (radians), so a small radius on a large + /// body still looks round. + pub fn angle(self) -> f64 { + let deg: f64 = match self { + DisplayQuality::Coarse => 30.0, + DisplayQuality::Normal => 20.0, + DisplayQuality::Fine => 12.0, + DisplayQuality::Ultra => 7.0, + }; + deg.to_radians() + } + + /// The chord tolerance (mm) for a body of this size. + pub fn chord(self, size: f64) -> f64 { + let size = if size.is_finite() && size > 0.0 { size } else { 0.0 }; + (size * self.chord_fraction()).max(self.min_chord()) + } +} + +static CURRENT: AtomicU8 = AtomicU8::new(1); + +/// The level the viewport draws at (process-wide; set from the preferences). +pub fn current() -> DisplayQuality { + DisplayQuality::ALL.get(CURRENT.load(Ordering::Relaxed) as usize).copied().unwrap_or_default() +} + +/// Set the level the viewport draws at. Bodies keep a display plan per level, so switching back +/// is instant and nothing is re-evaluated. +pub fn set_current(q: DisplayQuality) { + CURRENT.store(q.index() as u8, Ordering::Relaxed); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normal_is_the_application_tolerance_and_levels_get_finer() { + let n = DisplayQuality::Normal; + assert_eq!(n.chord(200.0), 0.2); + assert_eq!(n.chord(0.5), 1e-3); + assert!((n.angle() - solidcraft_kernel::DISPLAY_ANGLE).abs() < 1e-12); + for w in DisplayQuality::ALL.windows(2) { + assert!(w[1].chord(100.0) < w[0].chord(100.0) && w[1].angle() < w[0].angle()); + } + for q in DisplayQuality::ALL { + assert_eq!(DisplayQuality::parse(q.id()), Some(q)); + assert_eq!(DisplayQuality::ALL[q.index()], q); + } + } + + /// Each level keeps its own plan: finer levels draw a round body with more triangles and + /// closer to the surface, and the reference mesh the model reads never changes. + #[test] + fn each_level_has_its_own_plan_and_the_reference_mesh_stays() { + use solidcraft_geom::Vec3; + let body = solidcraft_kernel::cylinder(Vec3::ZERO, Vec3::Z, 20.0, 50.0).unwrap(); + let mb = crate::ModelBody::new("Body1".into(), body, 1); + let reference = mb.mesh(); + let mut last = 0; + for q in DisplayQuality::ALL { + let mesh = mb.plan_mesh_at(q).unwrap(); + let r = |v: Vec3| (v.x * v.x + v.y * v.y).sqrt(); + // The worst chord's sag on the side wall, from its triangle centres. + let sag = mesh + .triangles + .iter() + .map(|t| t.map(|i| mesh.positions[i as usize])) + .filter(|t| t.iter().all(|p| (r(*p) - 20.0).abs() < 1e-9)) + .map(|[a, b, c]| 20.0 - r((a + b + c) * (1.0 / 3.0))) + .fold(0.0, f64::max); + assert!(sag <= q.chord(mb.body.size()) * 1.01, "{q:?}: sag {sag}"); + assert!(mesh.triangles.len() > last, "{q:?}: {} triangles", mesh.triangles.len()); + last = mesh.triangles.len(); + assert!(std::sync::Arc::ptr_eq(&mesh, &mb.plan_mesh_at(q).unwrap()), "made once per level"); + } + assert!(std::sync::Arc::ptr_eq(&reference, &mb.mesh())); + assert_eq!(mb.display_plan_at(DisplayQuality::Normal).unwrap().vertex_count(), { + let plan = mb.body.display_plan_angular((mb.body.size() * 1e-3).max(1e-3), solidcraft_kernel::DISPLAY_ANGLE).unwrap(); + plan.vertex_count() + }); + } +} diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 63f5ce56..e66a7166 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -30,10 +30,10 @@ pub struct ModelBody { mesh: Arc>>, /// The display plan (none when the body has no plan: a mesh body, or one the planner /// refuses), shared like the mesh. A GPU viewport draws it. - plan: Arc>>>, + plan: Arc<[OnceLock>>; 4]>, /// The plan evaluated on the CPU (none without a plan), shared like the mesh: the triangles a /// GPU viewport draws, so a highlight over a face lies exactly on it. - plan_mesh: Arc>>>, + plan_mesh: Arc<[OnceLock>>; 4]>, /// Identity of this body's content for the evaluation cache: the key of the evaluation /// that produced it (kept while later features pass it through unchanged). key: u64, @@ -61,8 +61,8 @@ impl ModelBody { body, feature, mesh: Arc::new(OnceLock::new()), - plan: Arc::new(OnceLock::new()), - plan_mesh: Arc::new(OnceLock::new()), + plan: Arc::new(Default::default()), + plan_mesh: Arc::new(Default::default()), key: 0, names: Default::default(), made: Default::default(), @@ -88,11 +88,19 @@ impl ModelBody { .clone() } - /// The display plan of this body (the same tolerance and angle as [`ModelBody::mesh`]), made - /// once; `None` when the body has no plan (a mesh body, or one the planner refuses). + /// The display plan of this body at the viewport's display quality + /// ([`crate::display_quality::current`]; at Normal, the same tolerance and angle as + /// [`ModelBody::mesh`]), made once per level; `None` when the body has no plan (a mesh body, + /// or one the planner refuses). pub fn display_plan(&self) -> Option> { + self.display_plan_at(crate::display_quality::current()) + } + + /// The display plan at a given display quality, made once per level. + pub fn display_plan_at(&self, q: crate::display_quality::DisplayQuality) -> Option> { self.plan - .get_or_init(|| match self.body.display_plan_angular(self.display_tol(), kernel::DISPLAY_ANGLE) { + .get(q.index())? + .get_or_init(|| match self.body.display_plan_angular(q.chord(self.body.size()), q.angle()) { Ok(plan) => Some(Arc::new(plan)), Err(e) => { log::debug!("{}: no display plan ({e})", self.name); @@ -102,12 +110,20 @@ impl ModelBody { .clone() } - /// The display plan's mesh, evaluated on the CPU once: the same triangles a GPU viewport - /// evaluates and draws for this body. Highlights over a face drawn from the plan use it, since - /// the reference mesh ([`ModelBody::mesh`]) triangulates the face differently and the two - /// would fight at equal depth. `None` when the body has no plan. + /// The display plan's mesh, evaluated on the CPU once per level: the same triangles a GPU + /// viewport evaluates and draws for this body. Highlights over a face drawn from the plan use + /// it, since the reference mesh ([`ModelBody::mesh`]) triangulates the face differently and + /// the two would fight at equal depth. `None` when the body has no plan. pub fn plan_mesh(&self) -> Option> { - self.plan_mesh.get_or_init(|| self.display_plan().and_then(|plan| self.body.display_mesh_of(&plan).ok()).map(Arc::new)).clone() + self.plan_mesh_at(crate::display_quality::current()) + } + + /// The display plan's mesh at a given display quality. + pub fn plan_mesh_at(&self, q: crate::display_quality::DisplayQuality) -> Option> { + self.plan_mesh + .get(q.index())? + .get_or_init(|| self.display_plan_at(q).and_then(|plan| self.body.display_mesh_of(&plan).ok()).map(Arc::new)) + .clone() } /// The display mesh's chord tolerance: 1/1000 of the body's size. diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index e6fc0f69..1aa47845 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -16,6 +16,7 @@ mod clipboard; pub use clipboard::{clone_expression, remap_topology}; pub mod config; pub mod construct; +pub mod display_quality; mod document; mod eval; pub mod expr; diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 4c30b043..6a6bc5f1 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -248,6 +248,9 @@ pub struct Prefs { pub space_mouse_speed: f32, /// Anti-aliasing samples (applies on restart). pub msaa: u32, + /// How finely the viewport tessellates curved faces and edges: `coarse`, `normal`, `fine` or + /// `ultra` (#1287). Only the display plan the viewport draws; never the design or exports. + pub display_quality: String, /// Decimals shown for lengths and angles. pub length_decimals: u8, pub angle_decimals: u8, @@ -327,6 +330,7 @@ impl Default for Prefs { space_mouse_enabled: true, space_mouse_speed: 1.0, msaa: 4, + display_quality: "normal".into(), length_decimals: 2, angle_decimals: 1, theme: "dark".into(), @@ -467,6 +471,7 @@ fn defaults(app: &mut SolveApp, page: usize) { PAGE_GRAPHICS => { let msaa = p.msaa != d.msaa; p.msaa = d.msaa; + p.display_quality = d.display_quality; p.color_vision = d.color_vision; app.ui.visual_style = 0; app.ui.show_grid = true; @@ -484,6 +489,10 @@ fn defaults(app: &mut SolveApp, page: usize) { /// Settings that follow the preferences every frame: the theme when it follows the system, and /// the interface scale. pub fn apply(app: &mut SolveApp, ctx: &egui::Context) { + // The viewport's display quality (an unknown saved value draws at Normal). + solidcraft_engine::doc::display_quality::set_current( + solidcraft_engine::doc::display_quality::DisplayQuality::parse(&app.preferences.display_quality).unwrap_or_default(), + ); // The view's up axis follows the preference (set in the dialog, by ui.prefs or loaded). if app.prefs_window.applied_up.as_deref() != Some(app.preferences.up_axis.as_str()) { up_axis_changed(app); @@ -1094,6 +1103,21 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) { }); ui.end_row(); note(ui, app.tr("settings.msaa_applies_on_restart")); + ui.label(app.tr("ui.display_quality")); + { + use solidcraft_engine::doc::display_quality::DisplayQuality; + let label = |q: DisplayQuality| crate::i18n::tr(&format!("ui.display_quality_{}", q.id())).to_string(); + let current = DisplayQuality::parse(&app.preferences.display_quality).unwrap_or_default(); + egui::ComboBox::from_id_salt("pref_display_quality").selected_text(label(current)).show_ui(ui, |ui| { + for q in DisplayQuality::ALL { + if ui.selectable_label(q == current, label(q)).clicked() { + app.preferences.display_quality = q.id().to_string(); + } + } + }); + } + ui.end_row(); + note(ui, app.tr("settings.display_quality_note")); ui.label(crate::i18n::tr_label("Renderer")); ui.label(match (&app.viewport.gpu, &app.help.gpu) { (Some(_), Some(g)) => format!("GPU: {g}"), @@ -1305,6 +1329,9 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result Result u64 { app.ui.show_grid.hash(&mut h); app.ui.visual_style.hash(&mut h); app.ui.ground_shadow.hash(&mut h); + solidcraft_engine::doc::display_quality::current().hash(&mut h); // The section plane (its cut outlines are part of the model scene). section_plane(app).map(|(o, n)| [o.x, o.y, o.z, n.x, n.y, n.z].map(f64::to_bits)).hash(&mut h); app.cam.up.hash(&mut h); @@ -347,7 +348,7 @@ fn drawn_plan( } /// The mesh whose triangles the model scene shows for `b`'s faces: the plan's (evaluated on the -/// CPU, the same triangles the GPU draws) or the reference mesh. A face highlighted over the model +/// CPU, the same triangles the GPU draws) or [`drawn_mesh`]. A face highlighted over the model /// must use the same triangles: another triangulation of the same face fights it at equal depth /// and shimmers. fn shown_mesh( @@ -362,7 +363,16 @@ fn shown_mesh( .then(|| drawn_plan(app, b, true)) .flatten() .and_then(|_| b.plan_mesh()) - .unwrap_or_else(|| b.mesh()) + .unwrap_or_else(|| drawn_mesh(b)) +} + +/// The mesh the model scene builds `b` from on the CPU (its edges always, its faces when they +/// are not drawn from the plan on the GPU): the reference mesh at Normal display quality, else +/// the plan's mesh at the chosen quality (finer or coarser faces and edges alike), falling back +/// to the reference mesh when the body has no plan. +fn drawn_mesh(b: &solidcraft_engine::doc::ModelBody) -> std::sync::Arc { + use solidcraft_engine::doc::display_quality::{DisplayQuality, current}; + if current() == DisplayQuality::Normal { b.mesh() } else { b.plan_mesh().unwrap_or_else(|| b.mesh()) } } /// A model scene block kept by the viewport: its bytes and the meshes they were built from @@ -513,7 +523,7 @@ fn build_scene(app: &SolveApp) -> GpuScene { .collect(); sc.planned.push(crate::gpu::PlannedBody { plan, face_colors }); } - let body = BodyDraw { mesh: b.mesh(), col, faded, opacity, face_looks, on_gpu }; + let body = BodyDraw { mesh: drawn_mesh(b), col, faded, opacity, face_looks, on_gpu }; let key = body.key(shaded, edges, hidden_edges, edge_col, hidden_col); key.hash(&mut run_key); run.push(body); @@ -1665,7 +1675,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene { // A live preview stands in for the body: its old faces would fight with it. if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !app.preview.replaced.contains(body)) { face_tris(&mut sc, &shown(b), *index, c4(t.sel_face), false); - hidden_face_outline(&mut sc, &b.mesh(), *index, t.sel_edge); + hidden_face_outline(&mut sc, &drawn_mesh(b), *index, t.sel_edge); } } Sel::Body { name } => { @@ -1681,7 +1691,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene { Sel::Edge { body, index, point } => { if let Some(b) = st.body(body) && app.session.body_visible(body) - && let Some(e) = edge_of(&b.mesh(), *index, *point) + && let Some(e) = edge_of(&drawn_mesh(b), *index, *point) { edge_lines(&mut sc, e, t.sel_edge, t.sel_edge_rim, 3.0); } @@ -1745,7 +1755,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene { } let k = t.hover_face_lift; let c = body_rgb(app, &b.name); - let m = b.mesh(); + let m = drawn_mesh(b); let lit = shown(b); for &f in &faces { if !sel.iter().any(|x| { @@ -1780,7 +1790,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene { if let Some(b) = st.body(body) { let k = t.hover_face_lift; let c = body_rgb(app, body); - let m = b.mesh(); + let m = drawn_mesh(b); let patch = m.surface_patch(*index); let lit = shown(b); for &f in &patch { @@ -1797,7 +1807,8 @@ fn build_highlight(app: &SolveApp) -> GpuScene { } Some(Hit::Edge { body, index, mid }) => { if let Some(b) = st.body(body) { - let m = b.mesh(); + // Lit over the drawn edges (finer away from Normal display quality). + let m = drawn_mesh(b); for i in m.circular_rim(*index) { let selected = sel.iter().any(|x| matches!(x, Sel::Edge { body: b, index: j, .. } if b == body && *j == i)); if !selected && let Some(e) = if i == *index { edge_of(&m, i, *mid) } else { m.edges.get(i) } {