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 {
|
pub const ANIMATE: CommandSpec = CommandSpec {
|
||||||
id: "results.animate",
|
id: "results.animate",
|
||||||
label: "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),
|
form: Some(super::forms::ANIMATE),
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
requires: &[Requirement::Result],
|
requires: &[Requirement::Result],
|
||||||
@@ -341,12 +341,12 @@ fn animate(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
return Ok(s.results_report());
|
return Ok(s.results_report());
|
||||||
}
|
}
|
||||||
let step = step_for(s, &json!({}), Want::Any)?;
|
let step = step_for(s, &json!({}), Want::Any)?;
|
||||||
if s.latest_result(&step).is_some_and(|(r, _)| matches!(r.kind(), Kind::Fluid(_))) {
|
let fluid = 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 v = view_of(s, &step)?;
|
let v = view_of(s, &step)?;
|
||||||
v.animate = true;
|
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())
|
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_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!(s.execute("results.streamlines", &json!({"show": false})).unwrap()["streamlines"].is_null());
|
||||||
assert_eq!(s.execute("results.surface", &json!({"show": false})).unwrap()["surface"], false);
|
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");
|
assert_eq!((s.journal.len(), s.revision()), (journal, revision), "display commands are not edits");
|
||||||
// Structural steps have none of it.
|
// Structural steps have none of it.
|
||||||
let mut t = solved();
|
let mut t = solved();
|
||||||
|
|||||||
@@ -1115,7 +1115,7 @@ impl ModelApp {
|
|||||||
/// frame; `None` when no results are shown.
|
/// frame; `None` when no results are shown.
|
||||||
fn plot_key(&self) -> Option<PlotKey> {
|
fn plot_key(&self) -> Option<PlotKey> {
|
||||||
let (view, result, current) = results::shown(&self.session)?;
|
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);
|
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 })
|
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))
|
Some((view, result, current))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The animation frame shown at time `now` (seconds), when animating.
|
/// The animation frame shown at time `now` (seconds), when animating. A deformed step swings its
|
||||||
pub fn frame(view: &ResultsView, now: f64) -> Option<u64> {
|
/// shape; a fluid step plays its frames though it does not deform; a step with nothing to animate
|
||||||
(view.animate && view.deformed && now.is_finite()).then(|| ((now.max(0.0) / PERIOD * FRAMES as f64).floor() as u64) % FRAMES)
|
/// (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
|
/// 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 mut p = view.pick();
|
||||||
let frames = match result.kind() {
|
let frames = match result.kind() {
|
||||||
Kind::Explicit(e) => e.frames.len(),
|
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()),
|
Kind::Static(r) => r.nonlinear.as_ref().map_or(0, |h| h.frames.len()),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
@@ -481,12 +485,34 @@ pub(crate) mod tests {
|
|||||||
let scene = scene(&p, Style::ShadedEdges, crate::scene::Show::DEFAULT, &t);
|
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.tris.len() > base.surface_tris.len(), "the slice is drawn");
|
||||||
assert!(!scene.lines.is_empty(), "streamlines and the part's edges are 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();
|
s.execute("results.slice", &json!({"field": "pressure", "frame": 0})).unwrap();
|
||||||
let p = plot_at(&s, &base, None).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)));
|
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]
|
#[test]
|
||||||
fn heated_flow_fields_have_units_colours_and_face_markers() {
|
fn heated_flow_fields_have_units_colours_and_face_markers() {
|
||||||
let mut s = plate_in_a_tank(true);
|
let mut s = plate_in_a_tank(true);
|
||||||
@@ -531,10 +557,14 @@ pub(crate) mod tests {
|
|||||||
s.execute("results.animate", &json!({})).unwrap();
|
s.execute("results.animate", &json!({})).unwrap();
|
||||||
let base = DisplayMesh::from_model(&s.model);
|
let base = DisplayMesh::from_model(&s.model);
|
||||||
let view = s.results_view.clone().unwrap();
|
let view = s.results_view.clone().unwrap();
|
||||||
assert_eq!(frame(&view, 0.0), Some(0));
|
let result = s.latest_result(&view.step).unwrap().0;
|
||||||
assert_eq!(frame(&view, PERIOD * 0.25), Some(FRAMES / 4));
|
assert_eq!(frame(&view, result, 0.0), Some(0));
|
||||||
assert_eq!(frame(&view, PERIOD * 1.25), Some(FRAMES / 4));
|
assert_eq!(frame(&view, result, PERIOD * 0.25), Some(FRAMES / 4));
|
||||||
let (_, result, _) = shown(&s).unwrap();
|
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(FRAMES / 4)) - 1.0).abs() < 1e-12);
|
||||||
assert!((swing(result, Some(3 * 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.
|
// 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() {
|
if ui.add(egui::Slider::new(&mut frame, 0..=n.saturating_sub(1))).changed() {
|
||||||
run.push(("results.frame", json!({"frame": frame})));
|
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));
|
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(_))) {
|
} else if let Some((k, factor, n)) = plot.frame().filter(|_| matches!(plot.result.kind(), Kind::Static(_))) {
|
||||||
|
|||||||
Reference in new issue
Block a user