From 5665cc3d8c397e3ed6815818cb415a4db7c43c52 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:20:56 -0400 Subject: [PATCH] ui: the viewport draws the analysis setup's symbols, with toggles The setup's symbols (fieldcraft_render::glyphs) are built with the display data and drawn over the model in every style, at the drawn nodes (a results plot's deformed shape), muted over results; the clipping radius holds them. Theme tokens give their colours in light and dark. The browser's Loads & BCs and Interactions rows get eye toggles (ui.set show_loads, show_interactions), also in the view menu. The GPU viewport draws reference point names with the interface's font; the bottom left says which definitions are sampled. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --- ROADMAP.md | 8 ++--- crates/ui-egui/src/browser.rs | 22 ++++++++++-- crates/ui-egui/src/lib.rs | 66 +++++++++++++++++++++++++++++----- crates/ui-egui/src/results.rs | 2 +- crates/ui-egui/src/scene.rs | 22 +++++++++--- crates/ui-egui/src/theme.rs | 52 +++++++++++++++++++++++++++ crates/ui-egui/src/viewport.rs | 30 ++++++++++++++++ docs/control-protocol.md | 2 +- 8 files changed, 183 insertions(+), 21 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8103973..78cec25 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -52,7 +52,7 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s | M23 Wave propagation and ultrasound | Elastic, acoustic and coupled waves from the explicit solver's 1D bar today to guided and ultrasonic waves, by two methods. Finite differences first, for fast 2D (then 3D) ultrasound on regular grids: staggered-grid velocity–stress FDTD (Virieux) and the elastodynamic finite integration technique for solids, the rotated staggered grid for anisotropic media, a k-space pseudospectral scheme for weakly heterogeneous tissue and water, all on grids voxelised from the face-tagged geometry by the LBM's exact voxeliser, with heterogeneous, layered and fluid–solid media, CPML boundaries, threads and the GPU backend; staircased curved boundaries scatter spuriously, so the FD grid is checked against the finite elements on curved parts. Finite elements for curved geometry and coupling with structures: spectral elements (high-order Gauss–Lobatto–Legendre hexes with diagonal mass) on the hex meshes, and a mesh check in elements per wavelength with the dispersion error it implies; absorbing boundaries (convolutional PML, absorbing layers with increasing damping) and infinite media; tone-burst sources (Hann-windowed cycles), point, line and surface excitation, and piezoelectric transducers (M19); anisotropic media (composites, crystals) and viscoelastic, power-law-in-frequency attenuation; fluid–solid interfaces and fluid-loaded plates; ultrasound in water and tissue, nonlinear acoustics (Westervelt, KZK) for focused ultrasound. Guided waves: dispersion curves by semi-analytical finite elements (SAFE) for plates, pipes, rails and layered and anisotropic laminates — Lamb, Rayleigh, Love and SH modes — with phase and group velocities and mode shapes; mode-selective excitation. Output: A-scans and B-scans at sensors, wavefield snapshots, 2D FFT (frequency–wavenumber) for mode identification, and time–frequency diagrams of any signal — short-time Fourier spectrograms, continuous wavelet transforms (Morlet), reassigned spectrograms and smoothed pseudo Wigner–Ville — with the SAFE or Rayleigh–Lamb group-velocity curves overlaid as arrival times, and group velocities extracted from the ridges. GPU paths (M12's backend) for long runs. Verified by Lamb's problem (point load on a half-space), Rayleigh-wave speed, Rayleigh–Lamb dispersion, Love waves in a layer over a half-space, reflection and transmission at interfaces, and published guided-wave measurements | planned | | Continuous: verification and validation | Every capability checked against theory (closed forms), experiments (published measurements) and published benchmarks (NAFEMS, vendor benchmark and verification manuals such as Abaqus's), each case our own model in `tests/oracle/` with its cited reference and tolerance; `cargo xtask oracle` runs them and writes docs/oracle.md; once a case matches, our result is frozen as a fixture so fidelity cannot regress (docs/verification.md). Runner, fixtures and CI job: done; 26 theory cases (patch tests of every solid element, cantilevers, Lamé in 3D, plane strain and axisymmetric, Kirsch in 3D and plane stress, rotating disc and cylinder, beam frequencies, couplings, explicit waves, impact and J2 plasticity, CDT invariants), 9 published benchmarks (MacNeal–Harder solid tests, Scordelis–Lo with solids, NAFEMS LE1 in 3D and 2D, LE10, FV32) and 9 pending cases with citations (shells, thermal, static plasticity, Taylor bar, CFD); `mesh.block` mapped blocks and 2D patches build their meshes | started | | Continuous: performance | CPU first, deterministic: results never depend on the thread count (fixed partitions, sums in a fixed order; checked by hashing results on 1, 2 and 7 threads). Supernodal left-looking Cholesky (11–15× faster than the column solver on one thread), parallel element matrices, stresses and Lanczos products, explicit element updates and LBM cell blocks on rayon threads (done — `fieldcraft-parallel`, `threads` on the solve commands, `FIELDCRAFT_THREADS`); parallel forward/back substitution and explicit tet4 assembly; GPU paths later, within a stated tolerance of the CPU | started | -| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done; the Craft-family shell — title bar, workspaces and ribbon drop-downs, browser, context panel, command search, journal timeline, light and dark themes — native file pickers with background opening, drag and drop and recent files, samples and one-click Solve: done; MCP server over stdio, headless or bridged to the app, with read and write roots, docs/mcp.md: done; caps labels in Inter SemiBold; the README's analysis video recorded through MCP by `tools/demo-video`: done; support, load and coupling symbols in the viewport, a camera parameter for screenshots, a ribbon that follows the command being run: in progress), browser build, command schema as the public API | started | +| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done; the Craft-family shell — title bar, workspaces and ribbon drop-downs, browser, context panel, command search, journal timeline, light and dark themes — native file pickers with background opening, drag and drop and recent files, samples and one-click Solve: done; MCP server over stdio, headless or bridged to the app, with read and write roots, docs/mcp.md: done; caps labels in Inter SemiBold; the README's analysis video recorded through MCP by `tools/demo-video`: done; support, load, reference-point and coupling symbols in the viewport and headless renders, with their browser toggles: done; a camera parameter for screenshots, a ribbon that follows the command being run: in progress), browser build, command schema as the public API | started | ## Next @@ -61,9 +61,9 @@ In progress, in this order of landing: 1. Tet quality: sliver repair and Delaunay refinement to a size field, so curved and filleted parts (the filleted bracket) solve on tets (M2). 2. Octree all-hex for any part (M6), the project's main meshing goal. -3. Support, load, reference-point and coupling symbols in the viewport and headless renders, - and the analysis video regenerated with them, captions and an orbit (screenshot camera and the - ribbon following the command: done). +3. The analysis video regenerated with the setup's symbols (done: support, load, reference-point + and coupling symbols in the viewport and headless renders), captions and an orbit (screenshot + camera and the ribbon following the command: done). 4. A Scordelis–Lo refinement study (32×32×2, 64×64×4, hex20 full and reduced) separating discretisation error from the solid-versus-shell model difference (−0.33 % today). 5. The nonlinear implicit core (M15), the gate for contact (M7), elastomers and viscoelasticity diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index c45563b..3aa88f0 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -1,7 +1,8 @@ //! The browser (left): the model as a tree. Geometry and Mesh with eye toggles, then the model's //! definitions (sets, materials, sections, steps, loads and boundary conditions, interactions) -//! with counts, and the solved steps' results. Clicking a definition selects it (its properties -//! show on the right), a double click opens its edit form, and clicking a result step shows it. +//! with counts (Loads & BCs and Interactions with eye toggles for their viewport symbols), and +//! the solved steps' results. Clicking a definition selects it (its properties show on the +//! right), a double click opens its edit form, and clicking a result step shows it. //! //! Row drawing follows SolidCraft's `crates/ui-egui/src/browser.rs` (MIT OR Apache-2.0), see //! ATTRIBUTION.md. @@ -305,6 +306,15 @@ pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) { } } +/// The viewport setting a definition group's eye shows and hides: the setup's symbols. +fn eye_flag<'u>(state: &'u mut crate::UiState, group: &str) -> Option<&'u mut bool> { + match group { + "loads" => Some(&mut state.show_loads), + "interactions" => Some(&mut state.show_interactions), + _ => None, + } +} + fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { let t = Tokens::get(app.ui.dark); let open = |app: &ModelApp, key: &str| app.tree_open.contains(key); @@ -438,6 +448,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { for (key, label, kinds) in groups { let items: Vec<(Def, String)> = kinds.iter().flat_map(|k| k.names(&app.session.model).into_iter().map(move |n| (*k, n))).collect(); let icon = kinds.first().map(|k| k.icon()); + let eye = eye_flag(&mut app.ui, key).map(|v| *v); let r = draw_row( app, ui, @@ -445,6 +456,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { &Row { depth: 1, fold: Some(open(app, key)), + eye, icon, label, note: Some(format!("{}", items.len())), @@ -452,7 +464,11 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { ..Default::default() }, ); - if r.fold || r.clicked { + if r.eye + && let Some(v) = eye_flag(&mut app.ui, key) + { + *v = !*v; + } else if r.fold || r.clicked { acts.push(Act::Fold(key.into())); } if open(app, key) { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 26c5e12..fc85131 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -47,6 +47,7 @@ use std::sync::mpsc::{Receiver, Sender}; use fieldcraft_engine::Session; use fieldcraft_engine::catalogue::{self, Tab, Workspace}; +use fieldcraft_render::glyphs::Glyphs; use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Scene, StandardView}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -69,6 +70,10 @@ pub struct UiState { pub show_geometry: bool, /// Elements. pub show_mesh: bool, + /// Boundary condition and load symbols. + pub show_loads: bool, + /// Reference point, coupling and rigid wall symbols. + pub show_interactions: bool, pub perspective: bool, pub dark: bool, /// The browser (left). @@ -92,6 +97,8 @@ impl Default for UiState { show_nodes: false, show_geometry: true, show_mesh: true, + show_loads: true, + show_interactions: true, perspective: false, dark: true, show_tree: true, @@ -119,7 +126,13 @@ impl UiState { /// What the viewport shows. pub fn show(&self) -> scene::Show { - scene::Show { geometry: self.show_geometry, mesh: self.show_mesh, nodes: self.show_nodes } + scene::Show { + geometry: self.show_geometry, + mesh: self.show_mesh, + nodes: self.show_nodes, + loads: self.show_loads, + interactions: self.show_interactions, + } } } @@ -218,6 +231,8 @@ pub struct ModelApp { /// Bumped whenever the model may have changed. revision: u64, display: (Option, DisplayMesh), + /// The analysis setup's symbols, built with the display data. + glyphs: Glyphs, /// The results plot and what it was built for. plot: (Option, Option>), /// The node probed last (a click on a results plot). @@ -230,8 +245,8 @@ pub struct ModelApp { bodies: (Option>, Vec), /// The frame-independent part of the results plot (scale, contour) and what it was built for. prepared: (Option, results::Prepared), - /// The built scene and its key. - scene: (Option, Scene), + /// The built scene, its key and the box around its symbols (they reach beyond the model). + scene: (Option, Scene, Aabb3), /// The theme last applied to the context. styled: Option, /// Input injected by the control channel, fed to egui one pointer event per frame. @@ -295,13 +310,14 @@ impl ModelApp { gpu_info: None, revision: 0, display: (None, DisplayMesh::default()), + glyphs: Glyphs::default(), plot: (None, None), probe: None, reactions: None, results_shown: false, bodies: (None, Vec::new()), prepared: (None, results::Prepared::default()), - scene: (None, Scene::default()), + scene: (None, Scene::default(), Aabb3::EMPTY), styled: None, synthetic: Vec::new(), synthetic_mods: egui::Modifiers::default(), @@ -567,6 +583,7 @@ impl ModelApp { } let was_empty = self.display.1.bounds.is_empty(); self.display = (Some(self.revision), d); + self.glyphs = Glyphs::from_model(&self.session.model); if was_empty && !self.display.1.bounds.is_empty() { self.cam.fit(&self.display.1.bounds); } @@ -574,6 +591,11 @@ impl ModelApp { &self.display.1 } + /// Definitions whose symbols are a sample (`Clamp: 400 of 12000 nodes`). + pub fn glyph_notes(&self) -> &[String] { + &self.glyphs.notes + } + /// The key of the current scene: the model revision, every setting the scene depends on and /// the results plot shown. pub fn scene_key(&self) -> u64 { @@ -619,17 +641,20 @@ impl ModelApp { if self.scene.0 != Some(key) { let t = theme::Tokens::get(self.ui.dark); let (style, show) = (self.ui.style, self.ui.show()); - let s = match &self.plot.1 { + let mut s = match &self.plot.1 { Some(p) => results::scene(p, style, show, &t), None => scene::build(&self.display.1, style, show, &t), }; - self.scene = (Some(key), s); + // Symbols follow the drawn nodes: the deformed shape of a results plot. + let drawn = self.plot.1.as_ref().map_or(&self.display.1, |p| &p.display); + let symbols = scene::symbols(&mut s, &self.glyphs, |n| drawn.node_position(n), show, self.plot.1.is_some(), &t); + self.scene = (Some(key), s, symbols); } let mut bounds = self.display.1.bounds; if let Some(p) = &self.plot.1 { - bounds.add(p.display.bounds.min); - bounds.add(p.display.bounds.max); + bounds.union(p.display.bounds); } + bounds.union(self.scene.2); self.scene.1.radius = scene::radius(&bounds, &self.cam); (key, &self.scene.1) } @@ -993,6 +1018,29 @@ mod tests { results::tests::node_at(&app.session, xyz) } + #[test] + fn setup_symbols_show_with_their_toggles_and_fade_over_results() { + let mut app = ModelApp::new(Session::default()); + app.run_script(&fieldcraft_engine::sample::cantilever()).unwrap(); + let t = theme::Tokens::get(app.ui.dark); + let count = |app: &mut ModelApp, c: egui::Color32| app.scene().1.tris.iter().filter(|x| x.color == theme::rgb(c)).count(); + assert!(count(&mut app, t.glyph_constraint) > 0, "the clamp's cones"); + assert!(app.scene().1.lines.iter().any(|l| l.color == theme::rgb(t.glyph_load)), "the push's arrows"); + // The symbols reach beyond the model: the clipping radius holds them. + let radius = app.scene().1.radius; + app.ui.show_loads = false; + assert_eq!(count(&mut app, t.glyph_constraint), 0); + assert!(app.scene().1.radius <= radius); + app.ui.show_loads = true; + // Over a results plot they are muted. + app.run("solve.static", json!({})).unwrap(); + app.run("results.deformed", json!({"step": "Load"})).unwrap(); + assert_eq!(count(&mut app, t.glyph_constraint), 0); + let muted = t.glyph_colors().over_results(theme::rgb(t.viewport_bottom)); + assert!(app.scene().1.tris.iter().any(|x| x.color == muted.constraint)); + assert!(app.glyph_notes().is_empty()); + } + #[test] fn solving_shows_the_result_on_the_results_tab() { let mut app = ModelApp::new(Session::default()); @@ -1012,6 +1060,8 @@ mod tests { assert!(app.scene().1.tris.len() > faces); app.run("results.clear", json!({})).unwrap(); assert!(app.plot().is_none()); + // The model's faces again (without the setup's symbols). + app.ui.show_loads = false; assert_eq!(app.scene().1.tris.len(), faces); } diff --git a/crates/ui-egui/src/results.rs b/crates/ui-egui/src/results.rs index 3ad29ad..cb3e8e2 100644 --- a/crates/ui-egui/src/results.rs +++ b/crates/ui-egui/src/results.rs @@ -86,7 +86,7 @@ impl Plot { /// The node's position in the plot, if it is drawn. pub fn node_position(&self, label: u32) -> Option { - self.display.nodes.binary_search_by_key(&label, |n| n.label).ok().and_then(|i| self.display.nodes.get(i)).map(|n| n.p) + self.display.node_position(label) } } diff --git a/crates/ui-egui/src/scene.rs b/crates/ui-egui/src/scene.rs index 0a200be..edd5749 100644 --- a/crates/ui-egui/src/scene.rs +++ b/crates/ui-egui/src/scene.rs @@ -3,9 +3,11 @@ //! //! Geometry (the open design, imported surfaces) draws as faces with their boundaries while //! the model has no element faces; once it is meshed, as boundaries only, so the mesh is not -//! hidden behind coincident faces. +//! hidden behind coincident faces. The analysis setup's symbols ([`symbols`]) go over the model, +//! muted over a results plot. -use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Scene, SceneLine, ScenePoint, SceneTri}; +use fieldcraft_render::glyphs::{GlyphShow, Glyphs}; +use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Scene, SceneLine, ScenePoint, SceneTri, Vec3}; use serde::{Deserialize, Serialize}; use crate::theme::{Tokens, rgb}; @@ -48,11 +50,23 @@ pub struct Show { /// Elements. pub mesh: bool, pub nodes: bool, + /// Boundary conditions and loads. + pub loads: bool, + /// Reference points, couplings and rigid walls. + pub interactions: bool, } impl Show { - /// Geometry and mesh, without nodes. - pub const DEFAULT: Show = Show { geometry: true, mesh: true, nodes: false }; + /// Geometry, mesh and the analysis setup, without nodes. + pub const DEFAULT: Show = Show { geometry: true, mesh: true, nodes: false, loads: true, interactions: true }; +} + +/// Draw the analysis setup's symbols into `s` at the nodes' positions `pos`, muted over a +/// results plot; returns the box around them. +pub fn symbols(s: &mut Scene, glyphs: &Glyphs, pos: impl Fn(u32) -> Option, show: Show, results: bool, t: &Tokens) -> Aabb3 { + let colors = t.glyph_colors(); + let colors = if results { colors.over_results(rgb(t.viewport_bottom)) } else { colors }; + glyphs.draw(pos, GlyphShow { loads: show.loads, interactions: show.interactions }, &colors, s) } /// The scene for `display` drawn in `style`. diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 62bef66..bf3ec16 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -8,6 +8,7 @@ use egui::{Color32, Stroke}; use fieldcraft_model::ElementKind; use fieldcraft_render::Rgb; +use fieldcraft_render::glyphs::GlyphColors; #[derive(Clone, Copy, Debug, PartialEq)] pub struct Tokens { @@ -70,6 +71,15 @@ pub struct Tokens { pub contour: [Color32; 5], /// Faces with no result at a node. pub no_result: Color32, + // ---- analysis setup symbols ---- + /// Boundary conditions and rigid walls. + pub glyph_constraint: Color32, + /// Forces, moments, pressures, body loads and initial velocities. + pub glyph_load: Color32, + /// Reference points and couplings. + pub glyph_interaction: Color32, + /// Temperatures, heat fluxes, convection and heat sources. + pub glyph_thermal: Color32, } /// The contour colour scale: blue, cyan, green, yellow, red. @@ -129,6 +139,10 @@ impl Tokens { geometry_edge: Color32::from_rgb(40, 44, 52), contour: CONTOUR, no_result: Color32::from_rgb(0x9a, 0xa0, 0xaa), + glyph_constraint: Color32::from_rgb(0x00, 0x8a, 0xa8), + glyph_load: Color32::from_rgb(0xd0, 0x24, 0x24), + glyph_interaction: Color32::from_rgb(0x9a, 0x30, 0xc0), + glyph_thermal: Color32::from_rgb(0xe8, 0x4c, 0x00), } } @@ -175,6 +189,10 @@ impl Tokens { t.geometry = Color32::from_rgb(152, 156, 162); t.geometry_edge = Color32::from_rgb(16, 16, 18); t.no_result = Color32::from_rgb(0x70, 0x76, 0x80); + t.glyph_constraint = Color32::from_rgb(0x3c, 0xdc, 0xf0); + t.glyph_load = Color32::from_rgb(0xff, 0x5a, 0x4c); + t.glyph_interaction = Color32::from_rgb(0xd2, 0x82, 0xff); + t.glyph_thermal = Color32::from_rgb(0xff, 0x8c, 0x00); t } @@ -183,6 +201,17 @@ impl Tokens { self.contour.map(rgb) } + /// The colours of the analysis setup's symbols; their names in the overlay text colour. + pub fn glyph_colors(&self) -> GlyphColors { + GlyphColors { + constraint: rgb(self.glyph_constraint), + load: rgb(self.glyph_load), + interaction: rgb(self.glyph_interaction), + thermal: rgb(self.glyph_thermal), + label: rgb(self.overlay_text), + } + } + /// The colour elements of this kind are drawn in. pub fn element(&self, kind: ElementKind) -> Color32 { match kind { @@ -294,6 +323,29 @@ mod tests { run(&ctx); } + /// Setup symbols differ from every element colour and from each other, and stand out from + /// the viewport background. + #[test] + fn symbol_colours_stand_out() { + let luma = |c: Color32| i32::from(c.r()) * 3 + i32::from(c.g()) * 6 + i32::from(c.b()); + let far = |a: Color32, b: Color32| { + (i32::from(a.r()) - i32::from(b.r())).abs() + (i32::from(a.g()) - i32::from(b.g())).abs() + (i32::from(a.b()) - i32::from(b.b())).abs() + }; + for dark in [true, false] { + let t = Tokens::get(dark); + let symbols = [t.glyph_constraint, t.glyph_load, t.glyph_interaction, t.glyph_thermal]; + for (i, s) in symbols.iter().enumerate() { + assert!((luma(*s) - luma(t.viewport_bottom)).abs() > 400 || far(*s, t.viewport_bottom) > 250, "{s:?} dark={dark}"); + for k in ElementKind::ALL { + assert!(far(*s, t.element(k)) > 90, "{s:?} vs {k:?} dark={dark}"); + } + for o in symbols.iter().skip(i + 1) { + assert!(far(*s, *o) > 90, "{s:?} vs {o:?} dark={dark}"); + } + } + } + } + /// Every element kind has a colour, and edges stand out from the faces they lie on. #[test] fn element_colours_contrast_with_edges() { diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index c431efc..fe884d2 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -33,9 +33,11 @@ pub fn show(app: &mut ModelApp, ui: &mut egui::Ui) { gradient(&painter, rect, t.viewport_top, t.viewport_bottom); if app.viewport.gpu.is_some() { paint_gpu(app, ui, &painter, rect); + labels(app, &painter, &t); } else { paint_cpu(app, ui, &painter, rect); } + sampled_note(app, &painter, rect, &t); if app.display().bounds.is_empty() && app.busy().is_none() { start_screen(app, ui, rect, &t); } @@ -176,6 +178,32 @@ fn paint_cpu(app: &mut ModelApp, ui: &egui::Ui, painter: &egui::Painter, rect: R } } +/// The scene's labels (reference point names) beside their points, with the interface's font. +/// The CPU renderer draws them into its image. +fn labels(app: &mut ModelApp, painter: &egui::Painter, t: &Tokens) { + let labels: Vec<(Vec3, String)> = app.scene().1.labels.iter().map(|l| (l.p, l.text.clone())).collect(); + let offset = fieldcraft_render::LABEL_OFFSET as f32; + for (p, text) in labels { + if let Some(at) = to_screen(app, p) { + let galley = painter.layout_no_wrap(text, egui::FontId::proportional(12.0), t.overlay_text); + let r = Rect::from_min_size(at + vec2(offset, -offset - galley.size().y), galley.size()).expand2(vec2(4.0, 1.0)); + painter.rect_filled(r, 3.0, t.overlay); + painter.galley(r.min + vec2(4.0, 1.0), galley, t.overlay_text); + } + } +} + +/// Bottom left: the definitions whose symbols are a sample of their nodes or faces. +fn sampled_note(app: &ModelApp, painter: &egui::Painter, rect: Rect, t: &Tokens) { + let show = app.ui.show(); + let notes = &app.glyph_notes(); + if notes.is_empty() || !(show.loads || show.interactions) { + return; + } + let text = format!("Symbols sampled: {}", notes.join("; ")); + painter.text(rect.left_bottom() + vec2(10.0, -10.0), egui::Align2::LEFT_BOTTOM, text, egui::FontId::proportional(11.0), t.text_dim); +} + /// The orientation gizmo's arm length and tip radius (points). const AXIS_LENGTH: f32 = 38.0; const AXIS_RADIUS: f32 = 11.0; @@ -321,6 +349,8 @@ fn nav_bar(app: &mut ModelApp, ui: &mut egui::Ui, rect: Rect, t: &Tokens) { ui.checkbox(&mut app.ui.show_geometry, "Geometry"); ui.checkbox(&mut app.ui.show_mesh, "Mesh"); ui.checkbox(&mut app.ui.show_nodes, "Nodes"); + ui.checkbox(&mut app.ui.show_loads, "Loads & BCs"); + ui.checkbox(&mut app.ui.show_interactions, "Interactions"); ui.separator(); ui.checkbox(&mut app.ui.show_tree, "Browser"); ui.checkbox(&mut app.ui.show_timeline, "Timeline"); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index cddf6b2..c82963e 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -42,7 +42,7 @@ commands (files, undo, `model.info`) leave the ribbon where it is. | Method | Params | Notes | |---|---|---| | `ui.inspect` | — | view settings (`ui`), camera, viewport rect (points), renderer (`gpu`/`cpu`) and adapter, open command form, status bar, node and element counts, the results display (`results`: what the `results.*` commands reply: view, `stale`, contour range, modes), the probed node (`probe`), `busy` (the background job: a file opening, a solve), `prompt` (`{"unsaved": path}`, `"recover"`, `"samples"`), `recent` (recent files), `selected` (the browser's selected definition), `nav` (the left-drag mode), `layout` (each region of the window drawn last frame as `[x, y, w, h]` in points: `app_bar`, `ribbon`, `workspace_switcher`, `document_tab`, `app_bar:`, `browser`, `viewport`, `nav_bar`, `axis_view`, `context`, `command_line`, `timeline`, `toolbar_dropdown`, `palette`), version | -| `ui.set` | any view setting: `style` (`shaded_edges`, `shaded`, `wireframe`), `show_nodes`, `show_geometry`, `show_mesh`, `perspective`, `dark`, `show_tree` (the browser), `show_timeline`, `workspace` (`prepare`, `model`, `analysis`, `results`), `tab` (the toolbar tab: `geometry`, `mesh`, `properties`, `assembly`, `steps`, `loads`, `interactions`, `solve`, `results`, `tools`; a tab of another workspace brings that workspace), `palette` (the command search open), `menu` (the open menu: `file`, `help`, `workspace`, `view`, `more`, or a panel's drop-down as `/` such as `mesh/Tet`; `null` closes it) | an unknown setting or value is an error; returns the settings | +| `ui.set` | any view setting: `style` (`shaded_edges`, `shaded`, `wireframe`), `show_nodes`, `show_geometry`, `show_mesh`, `show_loads` (boundary condition and load symbols), `show_interactions` (reference points, couplings, rigid walls), `perspective`, `dark`, `show_tree` (the browser), `show_timeline`, `workspace` (`prepare`, `model`, `analysis`, `results`), `tab` (the toolbar tab: `geometry`, `mesh`, `properties`, `assembly`, `steps`, `loads`, `interactions`, `solve`, `results`, `tools`; a tab of another workspace brings that workspace), `palette` (the command search open), `menu` (the open menu: `file`, `help`, `workspace`, `view`, `more`, or a panel's drop-down as `/` such as `mesh/Tet`; `null` closes it) | an unknown setting or value is an error; returns the settings | | `ui.view` | `view`: `front`, `back`, `left`, `right`, `top`, `bottom`, `iso`, `home` (iso and fit), `fit` | returns the camera | | `ui.camera` | `yaw`, `pitch` (degrees), `distance?` (model units) | turns the camera to any orientation about its target: `yaw` about Z (0 looks along +Y, the front view; −45 is the iso view's, −90 the right view's), `pitch` the elevation above the XY plane; `distance` moves the eye (the zoom of an orthographic view). Angles must be finite and the distance above 0: the yaw is wrapped, the pitch clamped to ±90 and the distance to 0.001–10⁹; anything else is an error. A sequence of calls orbits smoothly. Returns the camera (`ui.inspect`'s, angles in radians) | | `ui.toolbar` | — | the ribbon: `workspace`, `workspaces`, `tab`, `tabs` (the workspace's), `menu` (the open menu), `panels` (each panel label's `menu` key and `rect`), and every catalogue entry (docs/features.md) with `id`, `label`, `tab`, `panel`, `command` (null while coming soon), `more` (its related commands), `coming_soon` (the milestone, or null), `fit` — `full` (a button), `collapsed` (its panel is one button), `more` (its panel is in the » menu) or `menu` (in its panel's drop-down) — and `rect` (screen points, last frame, on the shown tab; for `menu` entries the row while the drop-down is open) |