forked from 1b-agent/fieldcraft-upstream
engine: headless pictures draw the analysis setup's symbols
view::scene draws the boundary conditions, loads and interactions as the viewport does (muted over a results plot) and frames them; Shown.sampled and the MCP picture info's sampled_symbols list definitions drawn with a sample. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
93f1a03137
commit
68a9553f53
3 files changed
+35
-7
No files matched your search
@@ -6,10 +6,13 @@
|
||||
//! boundaries while the model has no element faces, then as boundaries only; elements in their
|
||||
//! kind's colour with their edges; a results view ([`crate::results::ResultsView`]) draws the
|
||||
//! 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).
|
||||
//! 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.
|
||||
|
||||
use fieldcraft_model::ElementKind;
|
||||
use fieldcraft_render::contour::{Bands, band_color, split_triangle};
|
||||
use fieldcraft_render::glyphs::{GlyphColors, GlyphShow, Glyphs};
|
||||
use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Orientation, Rgb, Scene, SceneLine, SceneTri, Vec3};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -33,6 +36,8 @@ pub struct Palette {
|
||||
pub no_result: Rgb,
|
||||
/// The contour colour scale, low to high.
|
||||
pub contour: [Rgb; 5],
|
||||
/// The analysis setup's symbols.
|
||||
pub glyphs: GlyphColors,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
@@ -49,6 +54,13 @@ impl Palette {
|
||||
geometry_edge: Rgb(0x2a, 0x30, 0x3a),
|
||||
no_result: Rgb(0x9a, 0xa0, 0xaa),
|
||||
contour: [Rgb(0x2b, 0x4f, 0xd8), Rgb(0x2f, 0xb8, 0xe0), Rgb(0x3c, 0xc8, 0x64), Rgb(0xf0, 0xd2, 0x3c), Rgb(0xe0, 0x40, 0x2e)],
|
||||
glyphs: GlyphColors {
|
||||
constraint: Rgb(0x00, 0x8a, 0xa8),
|
||||
load: Rgb(0xd0, 0x24, 0x24),
|
||||
interaction: Rgb(0x9a, 0x30, 0xc0),
|
||||
thermal: Rgb(0xe8, 0x4c, 0x00),
|
||||
label: Rgb(0x26, 0x2a, 0x32),
|
||||
},
|
||||
};
|
||||
|
||||
fn element(&self, kind: ElementKind) -> Rgb {
|
||||
@@ -86,6 +98,8 @@ pub struct Shown {
|
||||
pub contour: Option<(String, (u32, f64), (u32, f64))>,
|
||||
/// Bodies that could not be tessellated for display.
|
||||
pub failed: Vec<String>,
|
||||
/// Definitions whose symbols are a sample (`Clamp: 400 of 12000 nodes`).
|
||||
pub sampled: Vec<String>,
|
||||
}
|
||||
|
||||
/// The model's display data with the open design's bodies and the imported surfaces.
|
||||
@@ -123,11 +137,14 @@ fn model_scene(d: &DisplayMesh, p: &Palette) -> Scene {
|
||||
/// 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) {
|
||||
let (base, failed) = display(s);
|
||||
let mut shown = Shown { failed, ..Shown::default() };
|
||||
let glyphs = Glyphs::from_model(&s.model);
|
||||
let mut shown = Shown { failed, sampled: glyphs.notes.clone(), ..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 bounds = base.bounds;
|
||||
return (model_scene(&base, p), bounds, shown);
|
||||
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));
|
||||
return (scene, bounds, shown);
|
||||
};
|
||||
shown.step = Some(view.step.clone());
|
||||
let pick = view.pick();
|
||||
@@ -169,9 +186,10 @@ 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.add(drawn.bounds.min);
|
||||
bounds.add(drawn.bounds.max);
|
||||
bounds.union(drawn.bounds);
|
||||
bounds.union(symbols);
|
||||
(scene, bounds, shown)
|
||||
}
|
||||
|
||||
@@ -221,6 +239,10 @@ 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.
|
||||
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.load));
|
||||
s.execute("solve.static", &json!({})).unwrap();
|
||||
let (plot, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
||||
assert_ne!(model, plot);
|
||||
@@ -229,6 +251,11 @@ mod tests {
|
||||
assert_eq!(field, "von_mises");
|
||||
assert!(max.1 > min.1);
|
||||
assert!(render_png(&s, &Orientation::of(StandardView::Top), 0, 48, &Palette::LIGHT).is_err());
|
||||
// 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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -119,6 +119,7 @@ impl Backend for Headless {
|
||||
"height": h,
|
||||
"results": shown.step.map(|step| json!({"step": step, "scale": shown.scale, "contour": contour})),
|
||||
"undrawn_bodies": shown.failed,
|
||||
"sampled_symbols": shown.sampled,
|
||||
});
|
||||
Ok(Picture { png, info })
|
||||
}
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ software on the computer.
|
||||
| `show_results` | `step?`, `field?`, `bands?`, `deformed?`, `scale?`, `mode?`, `frame?`, `time?` | The `results.*` commands its arguments ask for (`deformed: false` draws the undeformed mesh; `scale` is a factor, 0 for the undeformed shape so a sequence from 0 swings the deformation out, or `"auto"`); the reply has the contour range (smallest and largest value and their nodes: `field: "displacement"` gives the largest displacement), modes or frames. |
|
||||
| `probe` | `step?`, `nodes?`, `elements?`, `points?`, `forces?`, `mode?`, `frame?` | `solve.probe`: stored results at nodes, elements (explicit) or points (fluid). |
|
||||
| `reactions` | `step?`, `sets?`, `about?` | `results.reactions`: reaction force and moment per node set and in total. |
|
||||
| `screenshot` | `view?`, `camera?`, `source?` (window, model), `width?`, `height?`, `path?` | A PNG image content block and what it shows. Headless: a CPU render framing the model, the geometry and the results display (deformed shape, contour bands). Bridged: the whole app window drawn offscreen (`ui.capture`, the default) or a model render with the app's camera (`ui.render`). `view` turns the camera to a standard view first; `camera` `{yaw, pitch, distance?}` to any orientation, in degrees (yaw 0 looks along +Y, −45 is the iso view's; pitch is the elevation, clamped to ±90) and model units (`ui.camera`; headless, the picture is framed unless `distance` is given). Give one of the two; the orientation is kept for the next picture, so a sequence of `camera` angles orbits smoothly. |
|
||||
| `screenshot` | `view?`, `camera?`, `source?` (window, model), `width?`, `height?`, `path?` | A PNG image content block and what it shows. Headless: a CPU render framing the model, the geometry, the analysis setup's symbols (boundary conditions, loads, reference points, couplings; `sampled_symbols` lists definitions drawn with a sample) and the results display (deformed shape, contour bands). Bridged: the whole app window drawn offscreen (`ui.capture`, the default) or a model render with the app's camera (`ui.render`). `view` turns the camera to a standard view first; `camera` `{yaw, pitch, distance?}` to any orientation, in degrees (yaw 0 looks along +Y, −45 is the iso view's; pitch is the elevation, clamped to ±90) and model units (`ui.camera`; headless, the picture is framed unless `distance` is given). Give one of the two; the orientation is kept for the next picture, so a sequence of `camera` angles orbits smoothly. |
|
||||
| `undo`, `redo` | — | `edit.undo`, `edit.redo`. |
|
||||
|
||||
Resources: `fieldcraft://model` (`model.info`) and `fieldcraft://commands` (the command catalog).
|
||||
|
||||
Reference in new issue
Block a user