forked from 1b-agent/fieldcraft-upstream
ui: mesh.plan.apply, mesh.cells and geometry.partition run as jobs (#545)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
b5f71612af
commit
2729511cb5
2 files changed
+70
-9
No files matched your search
@@ -136,6 +136,9 @@ pub const BACKGROUND: &[&str] = &[
|
||||
"mesh.hex.octree",
|
||||
"mesh.hex.project",
|
||||
"mesh.node.relax",
|
||||
"mesh.plan.apply",
|
||||
"mesh.cells",
|
||||
"geometry.partition",
|
||||
];
|
||||
|
||||
/// Most entries the recent-files list keeps, and the longest path it keeps.
|
||||
@@ -800,7 +803,7 @@ impl ModelApp {
|
||||
self.recent.add(p);
|
||||
}
|
||||
if self.dialog.as_ref().is_some_and(|d| o.form == Some(d.command)) {
|
||||
self.dialog = None;
|
||||
self.form_done();
|
||||
}
|
||||
let warnings: Vec<&str> = o.reply.get("warnings").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).collect();
|
||||
if warnings.is_empty() {
|
||||
@@ -1732,6 +1735,56 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// The partition, the cells mesher and the plan's apply run as jobs (#545): the window
|
||||
/// stays free while they run, the model they leave is the one the command makes run
|
||||
/// directly, bit for bit, in one undo step, and a cancelled one leaves the model as it was.
|
||||
#[test]
|
||||
fn partition_cells_and_plan_apply_run_as_jobs_like_the_command() {
|
||||
let dir = temp("jobs-545");
|
||||
let design = dir.join("plate.solidcraft").to_string_lossy().to_string();
|
||||
std::fs::write(&design, fieldcraft_geometry::sample::plate_design(&fieldcraft_geometry::sample::Plate::default()).unwrap()).unwrap();
|
||||
let mut app = ModelApp::new(Session::default());
|
||||
app.open_path(&design);
|
||||
app.wait_loaded();
|
||||
app.run("geometry.datum", json!({"name": "X30", "by": {"type": "offset", "plane": "yz", "offset": 30}})).unwrap();
|
||||
let plan = app.run("mesh.plan", json!({"body": "Body1"})).unwrap();
|
||||
let steps = [
|
||||
("mesh.plan.apply", json!({"plan": plan["name"]})),
|
||||
("geometry.partition", json!({"name": "Half", "datum": "X30"})),
|
||||
("mesh.cells", json!({"bodies": ["Body1/a", "Body1/b"], "size": 2.5})),
|
||||
];
|
||||
for (command, params) in steps {
|
||||
assert!(is_long(command), "{command}");
|
||||
let before = app.session.clone();
|
||||
let mut direct = app.session.clone();
|
||||
let expected = direct.execute(command, ¶ms).unwrap_or_else(|e| panic!("{command}: {e}"));
|
||||
|
||||
// Cancelled: nothing changes.
|
||||
app.start_job(command, params.clone()).unwrap();
|
||||
app.cancel_loading();
|
||||
assert!(app.busy().is_none());
|
||||
assert!(app.session.model == before.model && app.session.journal.len() == before.journal.len(), "{command}: cancelled");
|
||||
|
||||
app.start_job(command, params.clone()).unwrap();
|
||||
assert_eq!(app.busy(), Some(crate::command_label(command)), "{command} runs off the window thread");
|
||||
app.wait_loaded();
|
||||
assert!(app.status.as_ref().is_some_and(|s| !s.error), "{command}: {:?}", app.status);
|
||||
assert!(app.session.model == direct.model, "{command}: the job's model is the command's");
|
||||
let bodies = |s: &Session| s.design.as_ref().map(|d| d.bodies.iter().map(|b| b.name.clone()).collect::<Vec<_>>());
|
||||
assert_eq!(bodies(&app.session), bodies(&direct), "{command}");
|
||||
assert_eq!(app.session.journal.len(), before.journal.len() + 1, "{command}: one undo step");
|
||||
let elements = expected["elements"].as_u64().map(|n| n as usize);
|
||||
assert!(elements.is_none_or(|n| n == app.session.model.elements().len()), "{command}: {expected}");
|
||||
|
||||
// Undo takes the whole command back; redo brings it again.
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert!(app.session.model == before.model, "{command}: undo");
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert!(app.session.model == direct.model, "{command}: redo");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// The control channel's `ui.start` never opens a picker: it opens the form.
|
||||
#[test]
|
||||
fn programmatic_starts_never_open_a_picker() {
|
||||
|
||||
@@ -904,6 +904,21 @@ impl ModelApp {
|
||||
self.dialog = Some(dialog);
|
||||
}
|
||||
|
||||
/// The open form's command succeeded (here or as a job): a definition it made is the
|
||||
/// selection, its properties in the panel, and the form closes (back to the form it was
|
||||
/// opened from, if any).
|
||||
pub(crate) fn form_done(&mut self) {
|
||||
if let Some(d) = &self.dialog
|
||||
&& let Some(kind) = browser::Def::ALL.into_iter().find(|k| k.command("create") == d.command)
|
||||
&& let Ok(params) = serde_json::from_str::<Value>(&d.text)
|
||||
&& let Some(name) = params.get("name").and_then(Value::as_str)
|
||||
&& kind.exists(&self.session.model, name)
|
||||
{
|
||||
self.selected = Some((kind, name.to_string()));
|
||||
}
|
||||
self.dialog_done();
|
||||
}
|
||||
|
||||
/// Run the open parameter form; it closes when the command succeeds.
|
||||
pub fn run_dialog(&mut self) -> Result<Value, String> {
|
||||
let (command, params) = self.dialog_params()?;
|
||||
@@ -922,16 +937,9 @@ impl ModelApp {
|
||||
}
|
||||
return Err(message);
|
||||
}
|
||||
let made = params.get("name").and_then(Value::as_str).map(str::to_string);
|
||||
match self.run_form(command, params, true) {
|
||||
Ok(v) => {
|
||||
// A definition made in its form is the selection, its properties in the panel.
|
||||
if let (Some(kind), Some(name)) = (browser::Def::ALL.into_iter().find(|k| k.command("create") == command), made)
|
||||
&& kind.exists(&self.session.model, &name)
|
||||
{
|
||||
self.selected = Some((kind, name));
|
||||
}
|
||||
self.dialog_done();
|
||||
self.form_done();
|
||||
Ok(v)
|
||||
}
|
||||
Err(Failure { message, refused }) => {
|
||||
|
||||
Reference in new issue
Block a user