forked from 1b-agent/fieldcraft-upstream
Results display (M4) and offscreen full-UI capture (#28)
* ui.capture: the whole window drawn offscreen and rasterised on the CPU ui.screenshot falls back to it when no frame is presented (hidden window, locked screen), so agents can look at panels and the toolbar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Draw the open design and imported surfaces in the viewport DisplayMesh keeps node labels on every element primitive (for results), gains add_surface (geometry faces and their boundaries, coincident points welded) and displaced (elements moved by nodal offsets). Bodies are tessellated at a thousandth of their size; meshed models show the geometry as boundaries only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * results.* commands: deformed shape, contours, mode shapes, animation, reaction sums The results display is session state (ResultsView), set by commands that are not edits. Nodal fields (displacement, von Mises, principal and component stresses; principal stresses in closed form after Smith 1961) and reaction force and moment sums per set. Results kept from an earlier revision stay viewable, marked stale; solve.probe says when a result is stale. The Results tab's display and reaction entries are ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Results display: deformed shape, contours with legend, mode shapes and animation, probe, reaction sums The viewport draws the session's results view: elements moved by their nodes' displacements or a mode shape (automatic or given scale), faces split exactly along contour band boundaries into flat-coloured pieces (same on GPU and CPU), a legend with levels and extremes, a results panel whose controls run the results.* commands, a click probing the nearest node (solve.probe), and stale results marked. Solving from the form shows the result on the Results tab. sample::cantilever is a solvable model for tests and demos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Simplify the results display (review) One Contour in the plot; the results scene reuses scene::build; a solve shows its result through the engine (ResultsView::for_result) and the UI raises the Results tab when a view appears; the plot's scale and contour values are kept across animation frames; design bodies are tessellated once per design; the first content is framed where the display is rebuilt; field_values in one match; probe reuses results::displacements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Results display for explicit steps: field frames, energy histories The results view works on the engine's step results (static, frequency, explicit): ResultsView.frame and results.frame {frame | time} pick an explicit field frame; the deformed shape (true scale) and contours (element stresses averaged to the nodes, velocity) follow it; animation runs through the frames. The Results panel has a frame slider and plots the kinetic, internal, hourglass and external energies with their values at the frame. sample::impact is a steel bar flying onto a rigid wall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs/features.md after the rebase Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
30 files changed
+2989
-86
No files matched your search
+117
-6
@@ -1,10 +1,14 @@
|
||||
//! The sample model the desktop app opens with `--sample`: a script of ordinary commands, so it
|
||||
//! builds the same way a user's script does.
|
||||
//! Sample models as scripts of ordinary commands, so they build the same way a user's script
|
||||
//! does.
|
||||
//!
|
||||
//! Two parts side by side: a 40 × 20 × 10 block of 5 mm hex8 cells, and a 15 mm cube split into
|
||||
//! 162 tet4 elements (each 5 mm cell into six tetrahedra around its main diagonal, the Kuhn
|
||||
//! subdivision, which is conforming between neighbouring cells). Every element is positively
|
||||
//! oriented.
|
||||
//! [`script`], the model the desktop app opens with `--sample`: two parts side by side, a
|
||||
//! 40 × 20 × 10 block of 5 mm hex8 cells, and a 15 mm cube split into 162 tet4 elements (each
|
||||
//! 5 mm cell into six tetrahedra around its main diagonal, the Kuhn subdivision, which is
|
||||
//! conforming between neighbouring cells). Every element is positively oriented.
|
||||
//!
|
||||
//! [`cantilever`], a model ready to solve: a steel beam clamped at one end with a load on the
|
||||
//! other, a static and a frequency step. [`impact`]: the same beam flying onto a rigid wall, an
|
||||
//! explicit dynamic step.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -91,6 +95,90 @@ pub fn script() -> Value {
|
||||
Value::Array(steps)
|
||||
}
|
||||
|
||||
/// The cantilever's length (x), width (y) and height (z) in mm, and its hex8 cells along each.
|
||||
pub const CANTILEVER_SIZE: [f64; 3] = [100.0, 10.0, 20.0];
|
||||
const CANTILEVER_CELLS: [u32; 3] = [20, 2, 4];
|
||||
|
||||
/// The beam's mesh in steel (E = 210 GPa, ν = 0.3, ρ = 7.85 t/m³ in mm, t, s, N, MPa): nodes,
|
||||
/// hex8 elements, sets `beam` (elements), `root` (nodes at x = 0) and `tip` (at x = L), the
|
||||
/// material `Steel` and a solid section `Beam` with the given formulation.
|
||||
fn beam(formulation: Value) -> Vec<Value> {
|
||||
let [nx, ny, nz] = CANTILEVER_CELLS;
|
||||
let [lx, ly, lz] = CANTILEVER_SIZE;
|
||||
let id = |i: u32, j: u32, k: u32| 1 + i + j * (nx + 1) + k * (nx + 1) * (ny + 1);
|
||||
let mut steps = vec![json!({"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}})];
|
||||
for k in 0..=nz {
|
||||
for j in 0..=ny {
|
||||
for i in 0..=nx {
|
||||
let xyz = [lx * f64::from(i) / f64::from(nx), ly * f64::from(j) / f64::from(ny), lz * f64::from(k) / f64::from(nz)];
|
||||
steps.push(json!({"command": "mesh.node.add", "params": {"label": id(i, j, k), "xyz": xyz}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut element = 0u32;
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
element += 1;
|
||||
let nodes = [
|
||||
id(i, j, k),
|
||||
id(i + 1, j, k),
|
||||
id(i + 1, j + 1, k),
|
||||
id(i, j + 1, k),
|
||||
id(i, j, k + 1),
|
||||
id(i + 1, j, k + 1),
|
||||
id(i + 1, j + 1, k + 1),
|
||||
id(i, j + 1, k + 1),
|
||||
];
|
||||
steps.push(json!({"command": "mesh.element.add", "params": {"label": element, "kind": "hex8", "nodes": nodes}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
let end = |i: u32| -> Vec<u32> { (0..=nz).flat_map(|k| (0..=ny).map(move |j| id(i, j, k))).collect() };
|
||||
steps.extend([
|
||||
json!({"command": "model.set.create", "params": {"name": "beam", "elements": (1..=element).collect::<Vec<_>>()}}),
|
||||
json!({"command": "model.set.create", "params": {"name": "root", "nodes": end(0)}}),
|
||||
json!({"command": "model.set.create", "params": {"name": "tip", "nodes": end(nx)}}),
|
||||
json!({"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3}, "density": 7.85e-9}}),
|
||||
json!({"command": "model.section.create", "params": {"name": "Beam", "set": "beam", "material": "Steel", "kind": {"type": "solid"}, "formulation": formulation}}),
|
||||
]);
|
||||
steps
|
||||
}
|
||||
|
||||
/// A steel cantilever of hex8 elements with incompatible modes ([`CANTILEVER_SIZE`]), clamped at
|
||||
/// x = 0 (set `root`), with a total force of 1000 N down on the free end (set `tip`); step
|
||||
/// `Load` (static) and step `Modes` (six modes). Euler–Bernoulli puts the tip
|
||||
/// 4 P L³ / (E w h³) = 0.238 mm down.
|
||||
pub fn cantilever() -> Value {
|
||||
let mut steps = beam(json!({"incompatible_modes": true}));
|
||||
steps.extend([
|
||||
json!({"command": "model.boundary_condition.create", "params": {"name": "Clamp", "type": "fixed", "target": {"set": "root"}}}),
|
||||
json!({"command": "model.load.create", "params": {"name": "Push", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -1000]}}),
|
||||
json!({"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["Clamp"], "loads": ["Push"]}}),
|
||||
json!({"command": "model.step.create", "params": {"name": "Modes", "procedure": {"type": "frequency", "modes": 6}, "boundary_conditions": ["Clamp"]}}),
|
||||
]);
|
||||
Value::Array(steps)
|
||||
}
|
||||
|
||||
/// The same steel beam in one-point hex8 elements, flying at 10 m/s onto a rigid wall at x = 0:
|
||||
/// step `Impact` (explicit dynamics, 40 µs, ten field frames, energies and the tip's history).
|
||||
/// The compression wave runs the 100 mm bar in about 19 µs (c = √(E/ρ) ≈ 5.2 mm/µs).
|
||||
pub fn impact() -> Value {
|
||||
let mut steps = beam(json!({"integration": "reduced"}));
|
||||
let step = json!({
|
||||
"type": "explicit_dynamic", "end_time": 4e-5, "output_interval": 4e-6, "history_interval": 2e-7, "history_nodes": "tip",
|
||||
"initial_velocities": [{"set": "all_nodes", "velocity": [-10000, 0, 0]}],
|
||||
"rigid_walls": [{"set": "root", "point": [0, 0, 0], "normal": [1, 0, 0]}]
|
||||
});
|
||||
let [nx, ny, nz] = CANTILEVER_CELLS;
|
||||
let nodes = (nx + 1) * (ny + 1) * (nz + 1);
|
||||
steps.extend([
|
||||
json!({"command": "model.set.create", "params": {"name": "all_nodes", "nodes": (1..=nodes).collect::<Vec<_>>()}}),
|
||||
json!({"command": "model.step.create", "params": {"name": "Impact", "procedure": step}}),
|
||||
]);
|
||||
Value::Array(steps)
|
||||
}
|
||||
|
||||
/// Six times the signed volume of the tetrahedron `p[0..4]` (positive when p3 lies on the side
|
||||
/// p0 → p1 → p2 turns counter-clockwise around).
|
||||
fn signed_volume(p: &[[f64; 3]]) -> f64 {
|
||||
@@ -124,4 +212,27 @@ mod tests {
|
||||
}
|
||||
assert!((volume - 15.0f64.powi(3)).abs() < 1e-9, "{volume}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cantilever_solves_close_to_beam_theory() {
|
||||
let mut s = Session::default();
|
||||
s.run_script(&cantilever()).unwrap();
|
||||
let r = s.execute("solve.static", &json!({})).unwrap();
|
||||
let [l, w, h] = CANTILEVER_SIZE;
|
||||
let beam = 4.0 * 1000.0 * l.powi(3) / (210000.0 * w * h.powi(3));
|
||||
let tip = r["max_displacement"]["value"].as_f64().unwrap();
|
||||
// Shear deformation adds a little (L/h = 5).
|
||||
assert!(tip > beam && tip < 1.1 * beam, "{tip} vs {beam}");
|
||||
let f = s.execute("solve.frequency", &json!({})).unwrap();
|
||||
assert_eq!(f["modes"].as_array().map(Vec::len), Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_impact_runs_and_keeps_frames_with_stresses() {
|
||||
let mut s = Session::default();
|
||||
s.run_script(&impact()).unwrap();
|
||||
let r = s.execute("solve.explicit", &json!({})).unwrap();
|
||||
assert_eq!((r["completed"].as_bool(), r["frames"].as_u64()), (Some(true), Some(11)), "{r}");
|
||||
assert!(r["energy_balance"].as_f64().unwrap() < 0.05, "{r}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user