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
|
//! 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
|
//! 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
|
//! 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_model::ElementKind;
|
||||||
use fieldcraft_render::contour::{Bands, band_color, split_triangle};
|
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 fieldcraft_render::{Aabb3, Camera, DisplayMesh, Orientation, Rgb, Scene, SceneLine, SceneTri, Vec3};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@@ -33,6 +36,8 @@ pub struct Palette {
|
|||||||
pub no_result: Rgb,
|
pub no_result: Rgb,
|
||||||
/// The contour colour scale, low to high.
|
/// The contour colour scale, low to high.
|
||||||
pub contour: [Rgb; 5],
|
pub contour: [Rgb; 5],
|
||||||
|
/// The analysis setup's symbols.
|
||||||
|
pub glyphs: GlyphColors,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Palette {
|
impl Palette {
|
||||||
@@ -49,6 +54,13 @@ impl Palette {
|
|||||||
geometry_edge: Rgb(0x2a, 0x30, 0x3a),
|
geometry_edge: Rgb(0x2a, 0x30, 0x3a),
|
||||||
no_result: Rgb(0x9a, 0xa0, 0xaa),
|
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)],
|
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 {
|
fn element(&self, kind: ElementKind) -> Rgb {
|
||||||
@@ -86,6 +98,8 @@ pub struct Shown {
|
|||||||
pub contour: Option<(String, (u32, f64), (u32, f64))>,
|
pub contour: Option<(String, (u32, f64), (u32, f64))>,
|
||||||
/// Bodies that could not be tessellated for display.
|
/// Bodies that could not be tessellated for display.
|
||||||
pub failed: Vec<String>,
|
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.
|
/// 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.
|
/// 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) {
|
pub fn scene(s: &Session, p: &Palette) -> (Scene, Aabb3, Shown) {
|
||||||
let (base, failed) = display(s);
|
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 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 Some((view, result)) = plot else {
|
||||||
let bounds = base.bounds;
|
let mut bounds = base.bounds;
|
||||||
return (model_scene(&base, p), bounds, shown);
|
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());
|
shown.step = Some(view.step.clone());
|
||||||
let pick = view.pick();
|
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;
|
let mut bounds = base.bounds;
|
||||||
bounds.add(drawn.bounds.min);
|
bounds.union(drawn.bounds);
|
||||||
bounds.add(drawn.bounds.max);
|
bounds.union(symbols);
|
||||||
(scene, bounds, shown)
|
(scene, bounds, shown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +239,10 @@ mod tests {
|
|||||||
s.run_script(&crate::sample::cantilever()).unwrap();
|
s.run_script(&crate::sample::cantilever()).unwrap();
|
||||||
let (model, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
let (model, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
||||||
assert_eq!(shown, Shown::default());
|
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();
|
s.execute("solve.static", &json!({})).unwrap();
|
||||||
let (plot, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
let (plot, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
||||||
assert_ne!(model, plot);
|
assert_ne!(model, plot);
|
||||||
@@ -229,6 +251,11 @@ mod tests {
|
|||||||
assert_eq!(field, "von_mises");
|
assert_eq!(field, "von_mises");
|
||||||
assert!(max.1 > min.1);
|
assert!(max.1 > min.1);
|
||||||
assert!(render_png(&s, &Orientation::of(StandardView::Top), 0, 48, &Palette::LIGHT).is_err());
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ impl Backend for Headless {
|
|||||||
"height": h,
|
"height": h,
|
||||||
"results": shown.step.map(|step| json!({"step": step, "scale": shown.scale, "contour": contour})),
|
"results": shown.step.map(|step| json!({"step": step, "scale": shown.scale, "contour": contour})),
|
||||||
"undrawn_bodies": shown.failed,
|
"undrawn_bodies": shown.failed,
|
||||||
|
"sampled_symbols": shown.sampled,
|
||||||
});
|
});
|
||||||
Ok(Picture { png, info })
|
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. |
|
| `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). |
|
| `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. |
|
| `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`. |
|
| `undo`, `redo` | — | `edit.undo`, `edit.redo`. |
|
||||||
|
|
||||||
Resources: `fieldcraft://model` (`model.info`) and `fieldcraft://commands` (the command catalog).
|
Resources: `fieldcraft://model` (`model.info`) and `fieldcraft://commands` (the command catalog).
|
||||||
|
|||||||
Reference in new issue
Block a user