forked from 1b-agent/fieldcraft-upstream
engine: animate a fluid step's frames
results.animate refused fluid steps because nothing deforms. A fluid step instead plays its frames over time, the start-up transient from the field at rest to the settled flow, the way an explicit step plays its frames. The animation clock maps across the fluid frames once per cycle, and the fluid results panel gains an Animate control beside its frame slider. Part of #164.
This commit is contained in:
1 parent
7fdb348d05
commit
6996fd71b0
4 files changed
+52
-15
No files matched your search
@@ -44,7 +44,7 @@ pub const MODES: CommandSpec = CommandSpec {
|
||||
pub const ANIMATE: CommandSpec = CommandSpec {
|
||||
id: "results.animate",
|
||||
label: "Animate",
|
||||
params: r#"{"on": true} — animates the deformed shape: a mode swings through its positive and negative shape, a static step grows from undeformed to deformed"#,
|
||||
params: r#"{"on": true} — animates the result over time: a mode swings through its positive and negative shape, a static step grows from undeformed to deformed, an explicit or fluid step plays its frames in order"#,
|
||||
form: Some(super::forms::ANIMATE),
|
||||
kind: CommandKind::Session,
|
||||
requires: &[Requirement::Result],
|
||||
@@ -341,12 +341,12 @@ fn animate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Ok(s.results_report());
|
||||
}
|
||||
let step = step_for(s, &json!({}), Want::Any)?;
|
||||
if s.latest_result(&step).is_some_and(|(r, _)| matches!(r.kind(), Kind::Fluid(_))) {
|
||||
return Err(EngineError::Params(format!("step `{step}` is a fluid step: nothing deforms; show its frames with results.frame")));
|
||||
}
|
||||
let fluid = s.latest_result(&step).is_some_and(|(r, _)| matches!(r.kind(), Kind::Fluid(_)));
|
||||
let v = view_of(s, &step)?;
|
||||
v.animate = true;
|
||||
v.deformed = true;
|
||||
// A fluid step has nothing to deform; animating plays its frames (as results.frame does) in
|
||||
// order. A structural step animates its deformation (or an explicit step its frames).
|
||||
v.deformed = !fluid;
|
||||
Ok(s.results_report())
|
||||
}
|
||||
|
||||
@@ -1484,7 +1484,10 @@ mod tests {
|
||||
assert_eq!((r["traced"]["seeds"].as_u64(), r["traced"]["points"].as_u64()), (Some(5), Some(20)), "{r}");
|
||||
assert!(s.execute("results.streamlines", &json!({"show": false})).unwrap()["streamlines"].is_null());
|
||||
assert_eq!(s.execute("results.surface", &json!({"show": false})).unwrap()["surface"], false);
|
||||
assert!(s.execute("results.animate", &json!({})).is_err(), "nothing deforms");
|
||||
// A fluid step animates its frames (the start-up transient), not a deformation.
|
||||
let r = s.execute("results.animate", &json!({})).unwrap();
|
||||
assert_eq!((r["view"]["animate"].as_bool(), r["view"]["deformed"].as_bool()), (Some(true), Some(false)), "{r}");
|
||||
assert!(s.execute("results.animate", &json!({"on": false})).unwrap()["view"]["animate"] == json!(false));
|
||||
assert_eq!((s.journal.len(), s.revision()), (journal, revision), "display commands are not edits");
|
||||
// Structural steps have none of it.
|
||||
let mut t = solved();
|
||||
|
||||
@@ -1115,7 +1115,7 @@ impl ModelApp {
|
||||
/// frame; `None` when no results are shown.
|
||||
fn plot_key(&self) -> Option<PlotKey> {
|
||||
let (view, result, current) = results::shown(&self.session)?;
|
||||
let frame = results::frame(view, self.now);
|
||||
let frame = results::frame(view, result, self.now);
|
||||
let pick = results::pick(view, result, frame);
|
||||
Some(PlotKey { revision: self.revision, result: Arc::as_ptr(result) as usize, current, view: view.clone(), pick, frame })
|
||||
}
|
||||
|
||||
@@ -143,9 +143,12 @@ pub fn shown(session: &Session) -> Option<(&ResultsView, &Arc<StepResult>, bool)
|
||||
Some((view, result, current))
|
||||
}
|
||||
|
||||
/// The animation frame shown at time `now` (seconds), when animating.
|
||||
pub fn frame(view: &ResultsView, now: f64) -> Option<u64> {
|
||||
(view.animate && view.deformed && now.is_finite()).then(|| ((now.max(0.0) / PERIOD * FRAMES as f64).floor() as u64) % FRAMES)
|
||||
/// The animation frame shown at time `now` (seconds), when animating. A deformed step swings its
|
||||
/// shape; a fluid step plays its frames though it does not deform; a step with nothing to animate
|
||||
/// (Animate on but Deformed off, and not fluid) stays still.
|
||||
pub fn frame(view: &ResultsView, result: &StepResult, now: f64) -> Option<u64> {
|
||||
let plays = view.deformed || matches!(result.kind(), Kind::Fluid(_));
|
||||
(view.animate && plays && now.is_finite()).then(|| ((now.max(0.0) / PERIOD * FRAMES as f64).floor() as u64) % FRAMES)
|
||||
}
|
||||
|
||||
/// The deformation multiplier of an animation frame: a mode swings through ±1, a static step
|
||||
@@ -168,6 +171,7 @@ pub fn pick(view: &ResultsView, result: &StepResult, frame: Option<u64>) -> Pick
|
||||
let mut p = view.pick();
|
||||
let frames = match result.kind() {
|
||||
Kind::Explicit(e) => e.frames.len(),
|
||||
Kind::Fluid(f) => f.frames.len(),
|
||||
Kind::Static(r) => r.nonlinear.as_ref().map_or(0, |h| h.frames.len()),
|
||||
_ => 0,
|
||||
};
|
||||
@@ -481,12 +485,34 @@ pub(crate) mod tests {
|
||||
let scene = scene(&p, Style::ShadedEdges, crate::scene::Show::DEFAULT, &t);
|
||||
assert!(scene.tris.len() > base.surface_tris.len(), "the slice is drawn");
|
||||
assert!(!scene.lines.is_empty(), "streamlines and the part's edges are drawn");
|
||||
// Pressure on the slice; the first frame (at rest at the inlet speed everywhere).
|
||||
// Pressure on the slice; the first frame. This plate_in_a_tank fixture sets no initial
|
||||
// velocity, so it starts at rest (unlike the flow-past sample's uniform inlet field).
|
||||
s.execute("results.slice", &json!({"field": "pressure", "frame": 0})).unwrap();
|
||||
let p = plot_at(&s, &base, None).unwrap();
|
||||
assert_eq!((p.contour.as_ref().map(|c| c.field), p.pick.frame), (Some(Field::Pressure), Some(0)));
|
||||
}
|
||||
|
||||
/// Animating a fluid step plays its frames: the clock maps across the frames once per cycle,
|
||||
/// and results.animate turns it on without deforming the part (the flow start-up transient).
|
||||
#[test]
|
||||
fn a_fluid_step_animates_its_frames() {
|
||||
let mut s = plate_in_a_tank(false);
|
||||
let (base, _) = fieldcraft_engine::view::display(&s);
|
||||
s.execute("results.animate", &json!({})).unwrap();
|
||||
let view = s.results_view.as_ref().unwrap();
|
||||
let result = s.latest_result(&view.step).unwrap().0;
|
||||
assert!(view.animate && !view.deformed, "fluid animates its frames, not a deformation");
|
||||
// The animation clock runs even though the fluid step does not deform.
|
||||
assert_eq!(frame(view, result, 0.0), Some(0));
|
||||
assert_eq!(frame(view, result, PERIOD * 0.5), Some(FRAMES / 2));
|
||||
// The clock maps across the three frames: the plate_in_a_tank field, which starts at rest
|
||||
// (the fixture sets no initial velocity, unlike the flow-past sample), then the settled flow.
|
||||
let n = plot_at(&s, &base, None).unwrap().frame().unwrap().2;
|
||||
assert_eq!(n, 3);
|
||||
assert_eq!(plot_at(&s, &base, Some(0)).unwrap().pick.frame, Some(0));
|
||||
assert_eq!(plot_at(&s, &base, Some(FRAMES - 1)).unwrap().pick.frame, Some(n - 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heated_flow_fields_have_units_colours_and_face_markers() {
|
||||
let mut s = plate_in_a_tank(true);
|
||||
@@ -531,10 +557,14 @@ pub(crate) mod tests {
|
||||
s.execute("results.animate", &json!({})).unwrap();
|
||||
let base = DisplayMesh::from_model(&s.model);
|
||||
let view = s.results_view.clone().unwrap();
|
||||
assert_eq!(frame(&view, 0.0), Some(0));
|
||||
assert_eq!(frame(&view, PERIOD * 0.25), Some(FRAMES / 4));
|
||||
assert_eq!(frame(&view, PERIOD * 1.25), Some(FRAMES / 4));
|
||||
let (_, result, _) = shown(&s).unwrap();
|
||||
let result = s.latest_result(&view.step).unwrap().0;
|
||||
assert_eq!(frame(&view, result, 0.0), Some(0));
|
||||
assert_eq!(frame(&view, result, PERIOD * 0.25), Some(FRAMES / 4));
|
||||
assert_eq!(frame(&view, result, PERIOD * 1.25), Some(FRAMES / 4));
|
||||
// Animate on but Deformed off, and not a fluid step: nothing to animate, so it stays still.
|
||||
let mut still = view.clone();
|
||||
still.deformed = false;
|
||||
assert_eq!(frame(&still, result, PERIOD * 0.25), None);
|
||||
assert!((swing(result, Some(FRAMES / 4)) - 1.0).abs() < 1e-12);
|
||||
assert!((swing(result, Some(3 * FRAMES / 4)) + 1.0).abs() < 1e-12);
|
||||
// At frame 0 a mode is undeformed; a quarter cycle later fully deformed.
|
||||
|
||||
@@ -364,6 +364,10 @@ pub fn controls(app: &mut ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens)
|
||||
if ui.add(egui::Slider::new(&mut frame, 0..=n.saturating_sub(1))).changed() {
|
||||
run.push(("results.frame", json!({"frame": frame})));
|
||||
}
|
||||
let mut animate = plot.view.animate;
|
||||
if ui.checkbox(&mut animate, "Animate").on_hover_text("Play the frames from the start-up transient to the settled flow").changed() {
|
||||
run.push(("results.animate", json!({"on": animate})));
|
||||
}
|
||||
});
|
||||
ui.label(RichText::new(format!("t = {}", with_unit(number(at), Quantity::Time.unit(plot.units).as_deref()))).color(t.text_dim));
|
||||
} else if let Some((k, factor, n)) = plot.frame().filter(|_| matches!(plot.result.kind(), Kind::Static(_))) {
|
||||
|
||||
Reference in new issue
Block a user