forked from 1b-agent/fieldcraft-upstream
Setup symbols: wireframe glyphs per held degree of freedom, a constant size on screen
Boundary condition, load and interaction symbols are wireframes (thin lines, no shaded cones), so they never hide the mesh or a contour plot: - a crossed wire triangle per constrained translation, tip on the node, coming from outside the part where the set's faces say which side that is; a held rotation adds a smaller triangle behind it (the double mark); fixed shows all three axes, a symmetry plane only its normal; - forces as open-headed arrows ending on the node, as long as four support units at one place and 2.5 over a set, so loads and supports stay in proportion (#78); moments as an axis line and a right-handed curved arrow (#79); contact and tie surfaces as small squares on their faces; - a constant size on screen at any zoom (#80): the symbol unit is the size setting (View > Symbol size, `ui.set {symbol_size}`, 14 pt by default) times the model length a pixel spans, rounded to a quarter of a doubling; - spread evenly on a grid a few symbol units across, so their density follows the screen, never more than 400 per definition; - a layer of their own on the desktop (GPU and CPU): zooming rebuilds and uploads the symbols only, never the mesh's scene; framing leaves them out; - colour per kind from the theme's tokens (a new contact colour), and the definition selected in the browser drawn in the selection colour, wider; - depth-tested against the model with the line bias that keeps them on their own surface; a load at a reference point or a single node and a reference point's cross are drawn over the part (#78). Headless pictures (MCP screenshot, `ui.render`) frame the camera first and draw the symbols at the default size for it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
1fcc7acf84
commit
52c12e2ad7
13 files changed
+1221
-436
No files matched your search
@@ -27,4 +27,5 @@ thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
png = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
+92
-21
@@ -8,11 +8,11 @@
|
||||
//! deformed shape (automatic or chosen scale) with its contour bands. Animation is not drawn: a
|
||||
//! picture shows the full deformation (a mode at its positive shape). The analysis setup's
|
||||
//! symbols ([`fieldcraft_render::glyphs`]) are drawn over the model as the viewport draws them,
|
||||
//! muted over a results plot.
|
||||
//! muted over a results plot, at their default size on screen once the camera is known.
|
||||
|
||||
use fieldcraft_model::ElementKind;
|
||||
use fieldcraft_render::contour::{Bands, band_color, split_triangle};
|
||||
use fieldcraft_render::glyphs::{GlyphColors, GlyphShow, Glyphs};
|
||||
use fieldcraft_render::glyphs::{self, GlyphColors, GlyphShow, GlyphView, Glyphs};
|
||||
use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Orientation, Rgb, Scene, SceneLine, SceneTri, Vec3};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -73,6 +73,8 @@ impl Palette {
|
||||
thermal: Rgb(0xe8, 0x4c, 0x00),
|
||||
label: Rgb(0x26, 0x2a, 0x32),
|
||||
datum: Rgb(0x1e, 0x8c, 0x6e),
|
||||
contact: Rgb(0x6a, 0x8a, 0x00),
|
||||
highlight: Rgb(0xd6, 0x00, 0x7f),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -153,18 +155,39 @@ fn model_scene(d: &DisplayMesh, p: &Palette) -> Scene {
|
||||
s
|
||||
}
|
||||
|
||||
/// The scene of the session: the model, or the results view's plot, and what it shows.
|
||||
pub fn scene(s: &Session, p: &Palette) -> (Scene, Aabb3, Shown) {
|
||||
/// The model part of a picture: the model or the results view's plot, the shape the symbols sit
|
||||
/// on (the deformed one of a plot), the box around it and what it shows.
|
||||
struct Picture {
|
||||
scene: Scene,
|
||||
drawn: DisplayMesh,
|
||||
bounds: Aabb3,
|
||||
shown: Shown,
|
||||
glyphs: Glyphs,
|
||||
colors: GlyphColors,
|
||||
}
|
||||
|
||||
impl Picture {
|
||||
/// The setup's symbols at symbol unit `unit` ([`glyphs::symbol_unit`]), in a scene of their
|
||||
/// own; notes the definitions drawn with a sample. Returns the box around them.
|
||||
fn symbols(&mut self, unit: f64, into: &mut Scene) -> Aabb3 {
|
||||
let drawn = &self.drawn;
|
||||
let view = GlyphView { show: GlyphShow::ALL, colors: &self.colors, unit, highlight: None };
|
||||
let d = self.glyphs.draw(|label| drawn.node_position(label), &view, into);
|
||||
self.shown.sampled = d.notes;
|
||||
d.bounds
|
||||
}
|
||||
}
|
||||
|
||||
fn picture(s: &Session, p: &Palette) -> Picture {
|
||||
let (base, failed) = display(s);
|
||||
let glyphs = Glyphs::from_model(&s.model);
|
||||
let mut shown = Shown { failed, sampled: glyphs.notes.clone(), ..Shown::default() };
|
||||
let mut shown = Shown { failed, ..Shown::default() };
|
||||
let plot = s.results_view.as_ref().and_then(|v| s.latest_result(&v.step).map(|(r, _)| (v, r)));
|
||||
let Some((view, result)) = plot else {
|
||||
let mut bounds = base.bounds;
|
||||
let mut scene = model_scene(&base, p);
|
||||
bounds.union(glyphs.draw(|label| base.node_position(label), GlyphShow::ALL, &p.glyphs, &mut scene));
|
||||
let mut bounds = base.bounds;
|
||||
bounds.union(fieldcraft_render::glyphs::datums(&s.model, &base.bounds, &p.glyphs, &mut scene));
|
||||
return (scene, bounds, shown);
|
||||
return Picture { scene, drawn: base, bounds, shown, glyphs, colors: p.glyphs };
|
||||
};
|
||||
shown.step = Some(view.step.clone());
|
||||
let pick = view.pick();
|
||||
@@ -206,11 +229,21 @@ pub fn scene(s: &Session, p: &Palette) -> (Scene, Aabb3, Shown) {
|
||||
});
|
||||
}
|
||||
}
|
||||
let symbols = glyphs.draw(|label| drawn.node_position(label), GlyphShow::ALL, &p.glyphs.over_results(p.background.1), &mut scene);
|
||||
let mut bounds = base.bounds;
|
||||
bounds.union(drawn.bounds);
|
||||
bounds.union(symbols);
|
||||
(scene, bounds, shown)
|
||||
Picture { scene, drawn, bounds, shown, glyphs, colors: p.glyphs.over_results(p.background.1) }
|
||||
}
|
||||
|
||||
/// The scene of the session (the model, or the results view's plot) with the setup's symbols
|
||||
/// at the model's own symbol unit, the box around it, and what it shows.
|
||||
pub fn scene(s: &Session, p: &Palette) -> (Scene, Aabb3, Shown) {
|
||||
let mut pic = picture(s, p);
|
||||
let mut symbols = Scene::default();
|
||||
let b = pic.symbols(pic.glyphs.model_unit(), &mut symbols);
|
||||
let mut scene = pic.scene;
|
||||
scene.append(symbols);
|
||||
pic.bounds.union(b);
|
||||
(scene, pic.bounds, pic.shown)
|
||||
}
|
||||
|
||||
/// A camera orientation from JSON, `{"yaw": …, "pitch": …, "distance"?: …}`: degrees, and the
|
||||
@@ -231,16 +264,28 @@ pub fn orientation(p: &Value) -> Result<Orientation> {
|
||||
}
|
||||
|
||||
/// A PNG of the session seen from `at`, `w` × `h` pixels: framing everything drawn, or from
|
||||
/// `at`'s distance when it has one.
|
||||
/// `at`'s distance when it has one. The symbols are drawn at their default size on screen
|
||||
/// ([`glyphs::DEFAULT_SIZE`] pixels) for that camera.
|
||||
pub fn render_png(s: &Session, at: &Orientation, w: usize, h: usize, p: &Palette) -> Result<(Vec<u8>, Shown)> {
|
||||
let (mut scene, bounds, shown) = scene(s, p);
|
||||
let mut pic = picture(s, p);
|
||||
let mut cam = Camera::default();
|
||||
let aspect = w as f64 / h.max(1) as f64;
|
||||
let unit = |cam: &Camera| cam.length_per_pixel(h as f64).and_then(|l| glyphs::symbol_unit(l, glyphs::DEFAULT_SIZE)).unwrap_or(f64::NAN);
|
||||
// Framed on the model, then again with its symbols at that camera's size, which reach a
|
||||
// little beyond it.
|
||||
cam.orient(&Orientation { distance: None, ..*at });
|
||||
cam.fit_in(&bounds, w as f64 / h.max(1) as f64, fieldcraft_render::FULL_VIEW);
|
||||
cam.fit_in(&pic.bounds, aspect, fieldcraft_render::FULL_VIEW);
|
||||
let mut symbols = Scene::default();
|
||||
let mut framed = pic.bounds;
|
||||
framed.union(pic.symbols(unit(&cam), &mut symbols));
|
||||
cam.fit_in(&framed, aspect, fieldcraft_render::FULL_VIEW);
|
||||
cam.orient(at);
|
||||
scene.radius = if bounds.is_empty() { 1.0 } else { (cam.target.dist(bounds.center()) + bounds.diagonal() * 0.5).max(1e-3) };
|
||||
let png = fieldcraft_render::render_png(&scene, &cam, w, h).map_err(|e| EngineError::Params(format!("render: {e}")))?;
|
||||
Ok((png, shown))
|
||||
symbols = Scene::default();
|
||||
let mut bounds = pic.bounds;
|
||||
bounds.union(pic.symbols(unit(&cam), &mut symbols));
|
||||
pic.scene.radius = if bounds.is_empty() { 1.0 } else { (cam.target.dist(bounds.center()) + bounds.diagonal() * 0.5).max(1e-3) };
|
||||
let png = fieldcraft_render::render_layers_png(&pic.scene, &[&symbols], &cam, w, h).map_err(|e| EngineError::Params(format!("render: {e}")))?;
|
||||
Ok((png, pic.shown))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -259,10 +304,14 @@ mod tests {
|
||||
s.run_script(&crate::sample::cantilever()).unwrap();
|
||||
let (model, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
||||
assert_eq!(shown, Shown::default());
|
||||
// The clamp's cones and the push's arrows are drawn in their colours.
|
||||
// The clamp's triangles and the push's arrows are drawn in their colours, as wireframes.
|
||||
let (scene, ..) = super::scene(&s, &Palette::LIGHT);
|
||||
assert!(scene.tris.iter().any(|t| t.color == Palette::LIGHT.glyphs.constraint));
|
||||
assert!(scene.lines.iter().any(|l| l.color == Palette::LIGHT.glyphs.constraint));
|
||||
assert!(scene.lines.iter().any(|l| l.color == Palette::LIGHT.glyphs.load));
|
||||
assert!(!scene.tris.iter().any(|t| t.color == Palette::LIGHT.glyphs.constraint));
|
||||
// And in the picture: pixels of the clamp's colour.
|
||||
let (png, _) = render_png(&s, &iso, 320, 240, &Palette::LIGHT).unwrap();
|
||||
assert!(count_color(&png, Palette::LIGHT.glyphs.constraint) > 20);
|
||||
s.execute("solve.static", &json!({})).unwrap();
|
||||
let (plot, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
||||
assert_ne!(model, plot);
|
||||
@@ -275,8 +324,30 @@ mod tests {
|
||||
// Over the plot the symbols are muted.
|
||||
let (scene, ..) = super::scene(&s, &Palette::LIGHT);
|
||||
let muted = Palette::LIGHT.glyphs.over_results(Palette::LIGHT.background.1);
|
||||
assert!(scene.tris.iter().any(|t| t.color == muted.constraint));
|
||||
assert!(!scene.tris.iter().any(|t| t.color == Palette::LIGHT.glyphs.constraint));
|
||||
assert!(scene.lines.iter().any(|l| l.color == muted.constraint));
|
||||
assert!(!scene.lines.iter().any(|l| l.color == Palette::LIGHT.glyphs.constraint));
|
||||
}
|
||||
|
||||
/// Pixels of colour `c` (within a few levels) in a PNG.
|
||||
fn count_color(png: &[u8], c: Rgb) -> usize {
|
||||
let d = png::Decoder::new(std::io::Cursor::new(png));
|
||||
let mut r = d.read_info().unwrap();
|
||||
let mut buf = vec![0; r.output_buffer_size().unwrap()];
|
||||
let info = r.next_frame(&mut buf).unwrap();
|
||||
let close = |a: u8, b: u8| a.abs_diff(b) <= 6;
|
||||
buf[..info.buffer_size()].chunks(4).filter(|p| close(p[0], c.0) && close(p[1], c.1) && close(p[2], c.2)).count()
|
||||
}
|
||||
|
||||
/// The symbols are as large on screen in a close-up as in a distant view.
|
||||
#[test]
|
||||
fn symbols_keep_their_size_in_pictures_at_any_distance() {
|
||||
let mut s = Session::default();
|
||||
s.run_script(&crate::sample::cantilever()).unwrap();
|
||||
let at = |d: f64| orientation(&json!({"yaw": -45, "pitch": 30, "distance": d})).unwrap();
|
||||
let clamp = |d: f64| count_color(&render_png(&s, &at(d), 400, 300, &Palette::LIGHT).unwrap().0, Palette::LIGHT.glyphs.constraint);
|
||||
let (near, far) = (clamp(120.0), clamp(1200.0));
|
||||
// Zoomed out ten times, the symbols are as large (a few overlap and thin out).
|
||||
assert!(far > 30 && far * 4 > near, "near {near}, far {far}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+847
-359
File diff suppressed because it is too large.
Load diff
@@ -67,6 +67,17 @@ pub struct Scene {
|
||||
pub radius: f64,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
/// Add `other`'s primitives (not its background or radius) to this scene.
|
||||
pub fn append(&mut self, other: Scene) {
|
||||
self.tris.extend(other.tris);
|
||||
self.lines.extend(other.lines);
|
||||
self.points.extend(other.points);
|
||||
self.labels.extend(other.labels);
|
||||
self.on_top.extend(other.on_top);
|
||||
}
|
||||
}
|
||||
|
||||
/// The lit colour of a surface with normal `n` seen along `view_dir` (toward the eye). The GPU
|
||||
/// shader computes the same.
|
||||
pub fn shade(n: Vec3, view_dir: Vec3, base: [f64; 3]) -> [f64; 3] {
|
||||
|
||||
@@ -164,6 +164,20 @@ pub enum Def {
|
||||
}
|
||||
|
||||
impl Def {
|
||||
/// The kind of setup symbols a definition of this kind draws, if any.
|
||||
pub fn symbols(self) -> Option<fieldcraft_render::glyphs::Definition> {
|
||||
use fieldcraft_render::glyphs::Definition;
|
||||
Some(match self {
|
||||
Def::BoundaryCondition => Definition::BoundaryCondition,
|
||||
Def::Load => Definition::Load,
|
||||
Def::ReferencePoint => Definition::ReferencePoint,
|
||||
Def::Coupling => Definition::Coupling,
|
||||
Def::Contact => Definition::Contact,
|
||||
Def::Step => Definition::Step,
|
||||
Def::Set | Def::Material | Def::Section | Def::FluidDomain => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The reference fields that name one of these.
|
||||
pub fn collection(self) -> Collection {
|
||||
match self {
|
||||
|
||||
+59
-19
@@ -1,9 +1,11 @@
|
||||
//! GPU viewport: lit triangles and screen-space wide lines with a depth buffer, drawn in an
|
||||
//! `egui_wgpu` paint callback. Geometry is uploaded when the scene changes; each frame only the
|
||||
//! camera uniform changes. Nodes are drawn as zero-length lines, which the line shader turns
|
||||
//! into squares. The selection highlight is a second, small scene (the overlay) drawn over the
|
||||
//! first: its faces pass the depth test over the faces they repeat, so a hover change uploads
|
||||
//! only the overlay.
|
||||
//! into squares. The setup's symbols are a second scene, uploaded on their own, so zooming
|
||||
//! (which resizes them) never uploads the mesh again. The selection highlight is a third, small
|
||||
//! scene (the overlay) drawn over the first: its faces pass the depth test over the faces they
|
||||
//! repeat, so a hover change uploads only the overlay. Every layer's `on_top` lines are drawn
|
||||
//! last, with no depth test.
|
||||
//!
|
||||
//! A large mesh does not fit one vertex buffer (wgpu's default `max_buffer_size`, which eframe
|
||||
//! requests, is 256 MiB): geometry is split into batches of at most [`MAX_BATCH_BYTES`] and the
|
||||
@@ -38,13 +40,15 @@ pub type GpuErrors = Arc<Mutex<Option<String>>>;
|
||||
pub struct GpuScene {
|
||||
pub tris: Vec<u8>,
|
||||
pub lines: Vec<u8>,
|
||||
/// Lines drawn over everything ([`Scene::on_top`]).
|
||||
pub top_lines: Vec<u8>,
|
||||
}
|
||||
|
||||
impl GpuScene {
|
||||
/// The scene's triangles, lines and points; widths and sizes scaled by `pixels_per_point`
|
||||
/// (the scene gives them in points).
|
||||
pub fn from_scene(scene: &Scene, pixels_per_point: f32) -> GpuScene {
|
||||
let mut g = GpuScene { tris: Vec::with_capacity(scene.tris.len() * 3 * TRI_SIZE), lines: Vec::new() };
|
||||
let mut g = GpuScene { tris: Vec::with_capacity(scene.tris.len() * 3 * TRI_SIZE), lines: Vec::new(), top_lines: Vec::new() };
|
||||
for t in &scene.tris {
|
||||
for p in t.p {
|
||||
g.vertex(p.to_f32(), t.normal.to_f32(), t.color);
|
||||
@@ -56,6 +60,9 @@ impl GpuScene {
|
||||
for p in &scene.points {
|
||||
g.line(p.p.to_f32(), p.p.to_f32(), p.color, p.size as f32 * pixels_per_point);
|
||||
}
|
||||
for l in &scene.on_top {
|
||||
push_line(&mut g.top_lines, l.a.to_f32(), l.b.to_f32(), l.color, l.width as f32 * pixels_per_point);
|
||||
}
|
||||
g
|
||||
}
|
||||
fn vertex(&mut self, p: [f32; 3], n: [f32; 3], c: Rgb) {
|
||||
@@ -65,14 +72,19 @@ impl GpuScene {
|
||||
self.tris.extend_from_slice(&[c.0, c.1, c.2, 255]);
|
||||
}
|
||||
fn line(&mut self, a: [f32; 3], b: [f32; 3], c: Rgb, width: f32) {
|
||||
for v in a.iter().chain(&b) {
|
||||
self.lines.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
self.lines.extend_from_slice(&[c.0, c.1, c.2, 255]);
|
||||
self.lines.extend_from_slice(&width.to_le_bytes());
|
||||
push_line(&mut self.lines, a, b, c, width);
|
||||
}
|
||||
}
|
||||
|
||||
/// A line instance into `buf`.
|
||||
fn push_line(buf: &mut Vec<u8>, a: [f32; 3], b: [f32; 3], c: Rgb, width: f32) {
|
||||
for v in a.iter().chain(&b) {
|
||||
buf.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
buf.extend_from_slice(&[c.0, c.1, c.2, 255]);
|
||||
buf.extend_from_slice(&width.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Where the UI hands a new scene to the renderer (taken on the next frame's `prepare`).
|
||||
pub type SceneSlot = Arc<Mutex<Option<GpuScene>>>;
|
||||
|
||||
@@ -81,6 +93,9 @@ pub struct ViewportCallback {
|
||||
/// Changes whenever the scene does; the slot holds the scene for a new key.
|
||||
pub key: u64,
|
||||
pub slot: SceneSlot,
|
||||
/// The same for the setup's symbols.
|
||||
pub symbols_key: u64,
|
||||
pub symbols_slot: SceneSlot,
|
||||
/// The same for the overlay (the selection highlight).
|
||||
pub overlay_key: u64,
|
||||
pub overlay_slot: SceneSlot,
|
||||
@@ -105,6 +120,7 @@ struct Layer {
|
||||
key: Option<u64>,
|
||||
tris: Vec<Batch>,
|
||||
lines: Vec<Batch>,
|
||||
top_lines: Vec<Batch>,
|
||||
}
|
||||
|
||||
impl Layer {
|
||||
@@ -116,9 +132,11 @@ impl Layer {
|
||||
// The old buffers go first, so the new ones don't have to fit beside them.
|
||||
self.tris.clear();
|
||||
self.lines.clear();
|
||||
self.top_lines.clear();
|
||||
let max = device.limits().max_buffer_size;
|
||||
self.tris = upload(device, queue, "fc_tris", &sc.tris, TRI_SIZE, 3, max);
|
||||
self.lines = upload(device, queue, "fc_lines", &sc.lines, LINE_SIZE, 1, max);
|
||||
self.top_lines = upload(device, queue, "fc_top_lines", &sc.top_lines, LINE_SIZE, 1, max);
|
||||
self.key = Some(key);
|
||||
}
|
||||
}
|
||||
@@ -132,14 +150,14 @@ impl Layer {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_lines(&self, pass: &mut wgpu::RenderPass<'static>, line: &wgpu::RenderPipeline) {
|
||||
if !self.lines.is_empty() {
|
||||
pass.set_pipeline(line);
|
||||
for b in &self.lines {
|
||||
pass.set_vertex_buffer(0, b.buffer.slice(..));
|
||||
pass.draw(0..6, 0..b.count);
|
||||
}
|
||||
fn paint_line_batches(batches: &[Batch], pass: &mut wgpu::RenderPass<'static>, line: &wgpu::RenderPipeline) {
|
||||
if !batches.is_empty() {
|
||||
pass.set_pipeline(line);
|
||||
for b in batches {
|
||||
pass.set_vertex_buffer(0, b.buffer.slice(..));
|
||||
pass.draw(0..6, 0..b.count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,10 +167,13 @@ struct Resources {
|
||||
/// The overlay's faces: pulled toward the eye and depth-tested without writing depth.
|
||||
overlay_tri: wgpu::RenderPipeline,
|
||||
line: wgpu::RenderPipeline,
|
||||
/// Lines over everything: no depth test.
|
||||
top_line: wgpu::RenderPipeline,
|
||||
uniform: wgpu::Buffer,
|
||||
bind: wgpu::BindGroup,
|
||||
linear_out: bool,
|
||||
scene: Layer,
|
||||
symbols: Layer,
|
||||
overlay: Layer,
|
||||
}
|
||||
|
||||
@@ -367,10 +388,19 @@ impl Resources {
|
||||
depth(wgpu::CompareFunction::LessEqual, false),
|
||||
Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
),
|
||||
top_line: pipeline(
|
||||
"fc_top_lines",
|
||||
"vs_line",
|
||||
"fs_line",
|
||||
line_layout(),
|
||||
depth(wgpu::CompareFunction::Always, false),
|
||||
Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
),
|
||||
uniform,
|
||||
bind,
|
||||
linear_out: t.format.is_srgb(),
|
||||
scene: Layer::default(),
|
||||
symbols: Layer::default(),
|
||||
overlay: Layer::default(),
|
||||
}
|
||||
}
|
||||
@@ -440,6 +470,7 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
) -> Vec<wgpu::CommandBuffer> {
|
||||
let Some(res) = resources.get_mut::<Resources>() else { return Vec::new() };
|
||||
res.scene.take(device, queue, self.key, &self.slot);
|
||||
res.symbols.take(device, queue, self.symbols_key, &self.symbols_slot);
|
||||
res.overlay.take(device, queue, self.overlay_key, &self.overlay_slot);
|
||||
queue.write_buffer(&res.uniform, 0, &uniform_bytes(self, res.linear_out));
|
||||
Vec::new()
|
||||
@@ -454,11 +485,16 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
pass.set_viewport(vp.left_px as f32, vp.top_px as f32, vp.width_px as f32, vp.height_px as f32, 0.0, 1.0);
|
||||
pass.set_bind_group(0, &res.bind, &[]);
|
||||
// Faces first, the overlay's over the scene's, then every line, so the mesh edges stay on
|
||||
// highlighted faces (as the CPU renderer draws them).
|
||||
// highlighted faces, then the lines over everything (as the CPU renderer draws them).
|
||||
res.scene.paint_tris(pass, &res.tri);
|
||||
res.symbols.paint_tris(pass, &res.tri);
|
||||
res.overlay.paint_tris(pass, &res.overlay_tri);
|
||||
res.scene.paint_lines(pass, &res.line);
|
||||
res.overlay.paint_lines(pass, &res.line);
|
||||
for layer in [&res.scene, &res.symbols, &res.overlay] {
|
||||
paint_line_batches(&layer.lines, pass, &res.line);
|
||||
}
|
||||
for layer in [&res.scene, &res.symbols, &res.overlay] {
|
||||
paint_line_batches(&layer.top_lines, pass, &res.top_line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,11 +560,13 @@ mod tests {
|
||||
tris: vec![SceneTri { p: [Vec3::ZERO, Vec3::X, Vec3::Y], normal: Vec3::Z, color: Rgb(1, 2, 3) }],
|
||||
lines: vec![SceneLine { a: Vec3::ZERO, b: Vec3::Z, color: Rgb(4, 5, 6), width: 1.5 }],
|
||||
points: vec![ScenePoint { p: Vec3::X, color: Rgb(7, 8, 9), size: 4.0 }],
|
||||
on_top: vec![SceneLine { a: Vec3::ZERO, b: Vec3::X, color: Rgb(1, 1, 1), width: 2.0 }],
|
||||
..Default::default()
|
||||
};
|
||||
let g = GpuScene::from_scene(&scene, 2.0);
|
||||
assert_eq!(g.tris.len(), 3 * TRI_SIZE);
|
||||
assert_eq!(g.lines.len(), 2 * LINE_SIZE);
|
||||
assert_eq!(g.top_lines.len(), LINE_SIZE);
|
||||
assert_eq!(&g.tris[24..28], &[1, 2, 3, 255]);
|
||||
// Widths are in physical pixels.
|
||||
assert_eq!(f32::from_le_bytes([g.lines[28], g.lines[29], g.lines[30], g.lines[31]]), 3.0);
|
||||
@@ -536,6 +574,8 @@ mod tests {
|
||||
let cb = ViewportCallback {
|
||||
key: 0,
|
||||
slot: SceneSlot::default(),
|
||||
symbols_key: 0,
|
||||
symbols_slot: SceneSlot::default(),
|
||||
overlay_key: 0,
|
||||
overlay_slot: SceneSlot::default(),
|
||||
view_proj: [[0.0; 4]; 4],
|
||||
|
||||
+135
-20
@@ -55,7 +55,7 @@ use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
use fieldcraft_engine::catalogue::{self, Tab, Workspace};
|
||||
use fieldcraft_engine::{EngineError, Session};
|
||||
use fieldcraft_render::glyphs::Glyphs;
|
||||
use fieldcraft_render::glyphs::{self, Definition, Drawn, Glyphs};
|
||||
use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Scene, StandardView, Vec3};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
@@ -64,6 +64,9 @@ pub use control::ControlRequest;
|
||||
pub use files::{Filter, Services};
|
||||
pub use scene::Style;
|
||||
|
||||
/// The viewport height symbols are sized for before the viewport is first drawn (points).
|
||||
const DEFAULT_VIEW_HEIGHT: f64 = 800.0;
|
||||
|
||||
/// The version shown in Help and over the control channel.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -82,6 +85,9 @@ pub struct UiState {
|
||||
pub show_loads: bool,
|
||||
/// Reference point, coupling and rigid wall symbols.
|
||||
pub show_interactions: bool,
|
||||
/// The setup symbols' size on screen, in points (held to
|
||||
/// [`fieldcraft_render::glyphs::SIZE_RANGE`]).
|
||||
pub symbol_size: f64,
|
||||
pub perspective: bool,
|
||||
pub dark: bool,
|
||||
/// The browser (left).
|
||||
@@ -111,6 +117,7 @@ impl Default for UiState {
|
||||
show_mesh: true,
|
||||
show_loads: true,
|
||||
show_interactions: true,
|
||||
symbol_size: glyphs::DEFAULT_SIZE,
|
||||
perspective: false,
|
||||
dark: true,
|
||||
show_tree: true,
|
||||
@@ -252,8 +259,12 @@ pub struct Viewport {
|
||||
/// The selection highlight for the GPU, and the overlay key last handed to it.
|
||||
pub overlay_slot: gpu::SceneSlot,
|
||||
pub overlay_uploaded: Option<u64>,
|
||||
/// CPU fallback: the texture and what it shows (scene and overlay keys, camera, size).
|
||||
cpu: Option<(egui::TextureHandle, (u64, u64), Camera, [usize; 2])>,
|
||||
/// The setup's symbols for the GPU, and the symbols key last handed to it.
|
||||
pub symbols_slot: gpu::SceneSlot,
|
||||
pub symbols_uploaded: Option<u64>,
|
||||
/// CPU fallback: the texture and what it shows (scene, symbols and overlay keys, camera,
|
||||
/// size).
|
||||
cpu: Option<(egui::TextureHandle, (u64, u64, u64), Camera, [usize; 2])>,
|
||||
/// CPU fallback: the scene key the renderer last refused.
|
||||
cpu_failed: Option<u64>,
|
||||
/// GPU errors reported by the device (see [`gpu::GpuErrors`]), shown in the status bar.
|
||||
@@ -340,8 +351,11 @@ pub struct ModelApp {
|
||||
bodies: (Option<Arc<fieldcraft_geometry::Design>>, Vec<(selection::BodyNames, fieldcraft_geometry::SurfaceMesh)>),
|
||||
/// The frame-independent part of the results plot (scale, contour) and what it was built for.
|
||||
prepared: (Option<PlotKey>, results::Prepared),
|
||||
/// The built scene, its key and the box around its symbols (they reach beyond the model).
|
||||
/// The built scene, its key and the box around its datums (they reach beyond the model).
|
||||
scene: (Option<u64>, Scene, Aabb3),
|
||||
/// The setup's symbols, a layer of their own drawn at the camera's scale, its key and what
|
||||
/// it drew.
|
||||
symbols: (Option<u64>, Scene, Drawn),
|
||||
/// The theme last applied to the context.
|
||||
styled: Option<bool>,
|
||||
/// Input injected by the control channel, fed to egui one pointer event per frame.
|
||||
@@ -432,6 +446,7 @@ impl ModelApp {
|
||||
bodies: (None, Vec::new()),
|
||||
prepared: (None, results::Prepared::default()),
|
||||
scene: (None, Scene::default(), Aabb3::EMPTY),
|
||||
symbols: (None, Scene::default(), Drawn::default()),
|
||||
styled: None,
|
||||
synthetic: Vec::new(),
|
||||
synthetic_mods: egui::Modifiers::default(),
|
||||
@@ -844,9 +859,9 @@ impl ModelApp {
|
||||
&self.display.1
|
||||
}
|
||||
|
||||
/// Definitions whose symbols are a sample (`Clamp: 400 of 12000 nodes`).
|
||||
/// Definitions whose symbols are a sample (`Clamp: 400 of 12000 nodes`), as drawn last.
|
||||
pub fn glyph_notes(&self) -> &[String] {
|
||||
&self.glyphs.notes
|
||||
&self.symbols.2.notes
|
||||
}
|
||||
|
||||
/// The key of the current scene: the model revision, every setting the scene depends on and
|
||||
@@ -898,27 +913,74 @@ impl ModelApp {
|
||||
Some(p) => results::scene(p, style, show, &t),
|
||||
None => scene::build(&self.display.1, style, show, &t),
|
||||
};
|
||||
// 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 mut symbols = scene::symbols(&mut s, &self.glyphs, |n| drawn.node_position(n), show, self.plot.1.is_some(), &t);
|
||||
let mut datums = Aabb3::EMPTY;
|
||||
if show.geometry && self.plot.1.is_none() {
|
||||
symbols.union(scene::datums(&mut s, &self.session.model, &self.display.1.bounds, &t));
|
||||
datums = scene::datums(&mut s, &self.session.model, &self.display.1.bounds, &t);
|
||||
scene::seeds(&mut s, &self.session.model, &self.pick.names, &self.display.1, &t);
|
||||
}
|
||||
self.scene = (Some(key), s, symbols);
|
||||
self.scene = (Some(key), s, datums);
|
||||
}
|
||||
self.scene.1.radius = scene::radius(&self.drawn_bounds(), &self.cam);
|
||||
// The clipping radius holds the symbols too (framing leaves them out: their size in the
|
||||
// model follows the zoom).
|
||||
let mut bounds = self.drawn_bounds();
|
||||
bounds.union(self.symbols.2.bounds);
|
||||
self.scene.1.radius = scene::radius(&bounds, &self.cam);
|
||||
(key, &self.scene.1)
|
||||
}
|
||||
|
||||
/// What the scene built last draws: the model, a results plot's (deformed) shape and the
|
||||
/// setup's symbols.
|
||||
/// The definition selected in the browser, when it has symbols.
|
||||
fn highlighted(&self) -> Option<(Definition, &str)> {
|
||||
let (def, name) = self.selected.as_ref()?;
|
||||
let def = def.symbols()?;
|
||||
Some((def, name.as_str()))
|
||||
}
|
||||
|
||||
/// The setup's symbols for the viewport as drawn last frame (its height in points).
|
||||
pub fn symbols(&mut self) -> (u64, &Scene) {
|
||||
let height = self.viewport.rect.map_or(DEFAULT_VIEW_HEIGHT, |r| f64::from(r.height()));
|
||||
self.symbols_for(height)
|
||||
}
|
||||
|
||||
/// The setup's symbols at the size setting on screen, for a view `height` points (or
|
||||
/// pixels) high with the current camera: a layer of their own, rebuilt when the model, what
|
||||
/// is shown, the plot, the highlighted definition or the size changes, and while zooming
|
||||
/// only when the symbol unit moves to another quarter of a doubling (the mesh's scene is
|
||||
/// never rebuilt for it). Their nodes follow the drawn shape (a results plot's deformed one).
|
||||
pub fn symbols_for(&mut self, height: f64) -> (u64, &Scene) {
|
||||
self.display();
|
||||
self.plot();
|
||||
let unit = self.cam.length_per_pixel(height).and_then(|l| glyphs::symbol_unit(l, self.ui.symbol_size)).and_then(glyphs::quantise_unit);
|
||||
let mut h = std::hash::DefaultHasher::new();
|
||||
let plot = self.plot.1.as_ref().map(|p| Arc::as_ptr(p) as usize);
|
||||
(self.revision, self.ui.show_loads, self.ui.show_interactions, self.ui.dark, plot, unit.map(|u| u.0), self.highlighted()).hash(&mut h);
|
||||
let key = h.finish();
|
||||
if self.symbols.0 != Some(key) {
|
||||
let t = theme::Tokens::get(self.ui.dark);
|
||||
let drawn = self.plot.1.as_ref().map_or(&self.display.1, |p| &p.display);
|
||||
let mut s = Scene::default();
|
||||
let unit = unit.map_or(f64::NAN, |u| u.1);
|
||||
let d =
|
||||
scene::symbols(&mut s, &self.glyphs, |n| drawn.node_position(n), self.ui.show(), self.plot.1.is_some(), unit, self.highlighted(), &t);
|
||||
self.symbols = (Some(key), s, d);
|
||||
}
|
||||
(key, &self.symbols.1)
|
||||
}
|
||||
|
||||
/// What the scene built last draws, as framing sees it: the model, a results plot's
|
||||
/// (deformed) shape, the datums and the reference points.
|
||||
fn drawn_bounds(&self) -> Aabb3 {
|
||||
let mut bounds = self.display.1.bounds;
|
||||
if let Some(p) = &self.plot.1 {
|
||||
bounds.union(p.display.bounds);
|
||||
}
|
||||
bounds.union(self.scene.2);
|
||||
// Reference points are places in the model, framed with it (their symbols are not).
|
||||
for p in self.session.model.reference_points().values() {
|
||||
let p = Vec3::from(p.xyz);
|
||||
if p.is_finite() {
|
||||
bounds.add(p);
|
||||
}
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
@@ -927,8 +989,9 @@ impl ModelApp {
|
||||
pub fn render_png(&mut self, w: usize, h: usize) -> Result<Vec<u8>, fieldcraft_render::RenderError> {
|
||||
let cam = self.cam;
|
||||
let (_, overlay) = self.overlay();
|
||||
let (_, scene) = self.scene();
|
||||
fieldcraft_render::render_layers_png(scene, &[&overlay], &cam, w, h)
|
||||
self.symbols_for(h as f64);
|
||||
self.scene();
|
||||
fieldcraft_render::render_layers_png(&self.scene.1, &[&self.symbols.1, &overlay], &cam, w, h)
|
||||
}
|
||||
|
||||
/// The geometry tags behind the drawn mesh, read again when the model changed.
|
||||
@@ -1776,24 +1839,76 @@ mod tests {
|
||||
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");
|
||||
let count = |app: &mut ModelApp, c: egui::Color32| app.symbols().1.lines.iter().filter(|x| x.color == theme::rgb(c)).count();
|
||||
assert!(count(&mut app, t.glyph_constraint) > 0, "the clamp's triangles");
|
||||
assert!(count(&mut app, t.glyph_load) > 0, "the push's arrows");
|
||||
// Wireframes, in a layer of their own.
|
||||
assert!(app.symbols().1.tris.is_empty());
|
||||
assert!(!app.scene().1.lines.iter().any(|l| l.color == theme::rgb(t.glyph_constraint)));
|
||||
// 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;
|
||||
// Selecting the clamp in the browser highlights its symbols.
|
||||
app.selected = Some((browser::Def::BoundaryCondition, "Clamp".into()));
|
||||
assert_eq!(count(&mut app, t.glyph_constraint), 0);
|
||||
assert!(count(&mut app, t.pick_selected) > 0);
|
||||
assert!(count(&mut app, t.glyph_load) > 0);
|
||||
app.selected = None;
|
||||
// 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.symbols().1.lines.iter().any(|x| x.color == muted.constraint));
|
||||
assert!(app.glyph_notes().is_empty());
|
||||
}
|
||||
|
||||
/// Zooming resizes the symbols (a layer of their own) and leaves the mesh's scene alone; the
|
||||
/// symbols measure the size setting on screen at any zoom.
|
||||
#[test]
|
||||
fn zooming_rebuilds_the_symbols_only_and_keeps_their_size_on_screen() {
|
||||
let mut app = ModelApp::new(Session::default());
|
||||
app.run_script(&fieldcraft_engine::sample::cantilever()).unwrap();
|
||||
app.set_view(StandardView::Front);
|
||||
let h = 600.0;
|
||||
// The length of the longest constraint line on screen: a triangle's side, about its
|
||||
// length (a symbol unit) long.
|
||||
let measure = |app: &mut ModelApp| {
|
||||
let cam = app.cam;
|
||||
let lines = app.symbols_for(h).1.lines.clone();
|
||||
let t = theme::Tokens::get(app.ui.dark);
|
||||
let at = |p: Vec3| cam.to_screen(p, 800.0, h, 1e6).unwrap();
|
||||
lines
|
||||
.iter()
|
||||
.filter(|l| l.color == theme::rgb(t.glyph_constraint))
|
||||
.map(|l| {
|
||||
let (a, b) = (at(l.a), at(l.b));
|
||||
((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt()
|
||||
})
|
||||
.fold(0.0, f64::max)
|
||||
};
|
||||
let scene = app.scene_key();
|
||||
let (symbols, _) = app.symbols_for(h);
|
||||
let mut sizes = Vec::new();
|
||||
for distance in [40.0, 150.0, 900.0, 5000.0] {
|
||||
app.cam.distance = distance;
|
||||
sizes.push(measure(&mut app));
|
||||
}
|
||||
assert_eq!(app.scene_key(), scene, "the camera is not in the scene's key");
|
||||
assert_ne!(app.symbols_for(h).0, symbols, "zooming resizes the symbols");
|
||||
let (lo, hi) = sizes.iter().fold((f64::MAX, 0.0f64), |(lo, hi), s| (lo.min(*s), hi.max(*s)));
|
||||
assert!(lo > 10.0 && hi / lo < 1.2, "{sizes:?}");
|
||||
// The size setting scales them.
|
||||
let before = measure(&mut app);
|
||||
app.ui.symbol_size = 2.0 * glyphs::DEFAULT_SIZE;
|
||||
let after = measure(&mut app);
|
||||
assert!((after / before - 2.0).abs() < 0.2, "{before} -> {after}");
|
||||
assert_eq!(app.scene_key(), scene);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solving_shows_the_result_on_the_results_tab() {
|
||||
let mut app = ModelApp::new(Session::default());
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
//! Geometry (the open design, imported surfaces) draws as faces with their boundaries while
|
||||
//! the model has no element faces; once it is meshed, as its boundaries drawn along the mesh
|
||||
//! (the element edges on geometry edges), so the mesh is not hidden behind coincident faces and
|
||||
//! every line lies on the element faces. The analysis setup's symbols ([`symbols`]) go over the model,
|
||||
//! muted over a results plot.
|
||||
//! every line lies on the element faces. The analysis setup's symbols ([`symbols`]) are a layer of
|
||||
//! their own over the model, a constant size on screen, muted over a results plot.
|
||||
|
||||
use fieldcraft_render::glyphs::{GlyphShow, Glyphs};
|
||||
use fieldcraft_render::glyphs::{Definition, Drawn, GlyphShow, GlyphView, Glyphs};
|
||||
use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Scene, SceneLine, ScenePoint, SceneTri, Vec3};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -62,12 +62,24 @@ impl Show {
|
||||
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<Vec3>, show: Show, results: bool, t: &Tokens) -> Aabb3 {
|
||||
/// Draw the analysis setup's symbols into `s` at the nodes' positions `pos` and symbol unit
|
||||
/// `unit` (model length, [`fieldcraft_render::glyphs::symbol_unit`]), muted over a results plot,
|
||||
/// the `highlight`ed definition's in the selection colour.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn symbols(
|
||||
s: &mut Scene,
|
||||
glyphs: &Glyphs,
|
||||
pos: impl Fn(u32) -> Option<Vec3>,
|
||||
show: Show,
|
||||
results: bool,
|
||||
unit: f64,
|
||||
highlight: Option<(Definition, &str)>,
|
||||
t: &Tokens,
|
||||
) -> Drawn {
|
||||
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)
|
||||
let view = GlyphView { show: GlyphShow { loads: show.loads, interactions: show.interactions }, colors: &colors, unit, highlight };
|
||||
glyphs.draw(pos, &view, s)
|
||||
}
|
||||
|
||||
/// Draw the model's datum planes and axes around `bounds` into `s`; returns the box around them.
|
||||
|
||||
@@ -80,6 +80,8 @@ pub struct Tokens {
|
||||
pub glyph_load: Color32,
|
||||
/// Reference points and couplings.
|
||||
pub glyph_interaction: Color32,
|
||||
/// Contact and tie surface symbols.
|
||||
pub glyph_contact: Color32,
|
||||
/// Temperatures, heat fluxes, convection and heat sources.
|
||||
pub glyph_thermal: Color32,
|
||||
/// Datum planes and axes.
|
||||
@@ -160,6 +162,7 @@ impl Tokens {
|
||||
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_contact: Color32::from_rgb(0x6a, 0x8a, 0x00),
|
||||
glyph_thermal: Color32::from_rgb(0xe8, 0x4c, 0x00),
|
||||
glyph_datum: Color32::from_rgb(0x1e, 0x8c, 0x6e),
|
||||
pick_selected: Color32::from_rgb(0xd6, 0x00, 0x7f),
|
||||
@@ -223,6 +226,7 @@ impl Tokens {
|
||||
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_contact = Color32::from_rgb(0xa0, 0xe6, 0x28);
|
||||
t.glyph_thermal = Color32::from_rgb(0xff, 0x8c, 0x00);
|
||||
t.glyph_datum = Color32::from_rgb(0x5a, 0xd8, 0xaa);
|
||||
t.pick_selected = Color32::from_rgb(0xff, 0x3f, 0xb4);
|
||||
@@ -235,12 +239,15 @@ impl Tokens {
|
||||
self.contour.map(rgb)
|
||||
}
|
||||
|
||||
/// The colours of the analysis setup's symbols; their names in the overlay text colour.
|
||||
/// The colours of the analysis setup's symbols; their names in the overlay text colour, the
|
||||
/// selected definition's in the selection colour.
|
||||
pub fn glyph_colors(&self) -> GlyphColors {
|
||||
GlyphColors {
|
||||
constraint: rgb(self.glyph_constraint),
|
||||
load: rgb(self.glyph_load),
|
||||
interaction: rgb(self.glyph_interaction),
|
||||
contact: rgb(self.glyph_contact),
|
||||
highlight: rgb(self.pick_selected),
|
||||
thermal: rgb(self.glyph_thermal),
|
||||
label: rgb(self.overlay_text),
|
||||
datum: rgb(self.glyph_datum),
|
||||
@@ -368,7 +375,7 @@ mod tests {
|
||||
};
|
||||
for dark in [true, false] {
|
||||
let t = Tokens::get(dark);
|
||||
let symbols = [t.glyph_constraint, t.glyph_load, t.glyph_interaction, t.glyph_thermal];
|
||||
let symbols = [t.glyph_constraint, t.glyph_load, t.glyph_interaction, t.glyph_contact, 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 {
|
||||
@@ -392,8 +399,17 @@ mod tests {
|
||||
let t = Tokens::get(dark);
|
||||
assert!(far(t.pick_selected, t.pick_hover) > 90, "dark={dark}");
|
||||
for c in [t.pick_selected, t.pick_hover] {
|
||||
let others =
|
||||
[t.geometry, t.node, t.glyph_constraint, t.glyph_load, t.glyph_interaction, t.glyph_thermal, t.viewport_bottom, t.mesh_edge];
|
||||
let others = [
|
||||
t.geometry,
|
||||
t.node,
|
||||
t.glyph_constraint,
|
||||
t.glyph_load,
|
||||
t.glyph_interaction,
|
||||
t.glyph_contact,
|
||||
t.glyph_thermal,
|
||||
t.viewport_bottom,
|
||||
t.mesh_edge,
|
||||
];
|
||||
for o in others.into_iter().chain(ElementKind::ALL.map(|k| t.element(k))) {
|
||||
assert!(far(c, o) > 100, "{c:?} vs {o:?} dark={dark}");
|
||||
}
|
||||
|
||||
@@ -363,6 +363,14 @@ fn gradient(painter: &egui::Painter, rect: Rect, top: Color32, bottom: Color32)
|
||||
|
||||
fn paint_gpu(app: &mut ModelApp, ui: &egui::Ui, painter: &egui::Painter, rect: Rect) {
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
// The symbols first: the scene's clipping radius holds them.
|
||||
let symbols_key = app.symbols().0;
|
||||
if app.viewport.symbols_uploaded != Some(symbols_key)
|
||||
&& let Ok(mut slot) = app.viewport.symbols_slot.lock()
|
||||
{
|
||||
*slot = Some(GpuScene::from_scene(&app.symbols.1, ppp));
|
||||
app.viewport.symbols_uploaded = Some(symbols_key);
|
||||
}
|
||||
let key = app.scene().0;
|
||||
let radius = app.scene.1.radius;
|
||||
if app.viewport.uploaded != Some(key) {
|
||||
@@ -385,6 +393,8 @@ fn paint_gpu(app: &mut ModelApp, ui: &egui::Ui, painter: &egui::Painter, rect: R
|
||||
let cb = ViewportCallback {
|
||||
key,
|
||||
slot: app.viewport.slot.clone(),
|
||||
symbols_key,
|
||||
symbols_slot: app.viewport.symbols_slot.clone(),
|
||||
overlay_key,
|
||||
overlay_slot: app.viewport.overlay_slot.clone(),
|
||||
view_proj: cam.view_proj(aspect, radius).to_f32(),
|
||||
@@ -405,11 +415,12 @@ fn paint_cpu(app: &mut ModelApp, ui: &egui::Ui, painter: &egui::Painter, rect: R
|
||||
let size = [w, h];
|
||||
let cam = app.cam;
|
||||
let (overlay_key, overlay) = app.overlay();
|
||||
let key = (app.scene().0, overlay_key);
|
||||
let symbols_key = app.symbols().0;
|
||||
let key = (app.scene().0, symbols_key, overlay_key);
|
||||
let fresh = app.viewport.cpu.as_ref().is_some_and(|(_, k, c, s)| *k == key && *c == cam && *s == size);
|
||||
// A scene the renderer refused is reported once and not retried until it changes.
|
||||
if !fresh && app.viewport.cpu_failed != Some(key.0) {
|
||||
let c = match fieldcraft_render::render_layers(&app.scene.1, &[&overlay], &cam, w, h) {
|
||||
let c = match fieldcraft_render::render_layers(&app.scene.1, &[&app.symbols.1, &overlay], &cam, w, h) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
app.viewport.cpu = None;
|
||||
@@ -450,7 +461,9 @@ fn cpu_size(w: usize, h: usize, max_side: usize) -> (usize, usize) {
|
||||
/// 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();
|
||||
app.symbols();
|
||||
app.scene();
|
||||
let labels: Vec<(Vec3, String)> = app.scene.1.labels.iter().chain(&app.symbols.1.labels).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) {
|
||||
@@ -627,6 +640,9 @@ fn nav_bar(app: &mut ModelApp, ui: &mut egui::Ui, rect: Rect, t: &Tokens) {
|
||||
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");
|
||||
let (lo, hi) = fieldcraft_render::glyphs::SIZE_RANGE;
|
||||
ui.add(egui::Slider::new(&mut app.ui.symbol_size, lo..=hi).step_by(1.0).suffix(" pt").text("Symbol size"))
|
||||
.on_hover_text("The size of boundary condition, load and interaction symbols on screen, at any zoom");
|
||||
ui.separator();
|
||||
ui.checkbox(&mut app.ui.show_tree, "Browser");
|
||||
ui.checkbox(&mut app.ui.show_timeline, "Timeline");
|
||||
|
||||
Reference in new issue
Block a user