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:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-17 08:12:04 -04:00
1 parent b5f71612af
commit 2729511cb5
2 files changed
+70 -9

No files matched your search

+54 -1
View File
@@ -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, &params).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() {
+16 -8
View File
@@ -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 }) => {