ui: jobs with progress, refusals listed, datum preview, form filters and defaults

- A solve or a mesher runs as a job with one progress mechanism: the overlay
  shows the step, stage, count and elapsed time with a bar when bounded, OK
  waits while it runs, and the status sums it up (elements, smallest angle,
  time) (#322, #357).
- Over the control channel such a command is a job too: answered when it
  ends however long it runs, or at once with wait: false, then job.status,
  job.wait, job.cancel; ui.inspect gives job; MCP waits for it (#367).
- A command that cannot run yet lists what it misses in the context panel,
  each with a button to the command that provides it (#323).
- A cancelled or refused solve stays in its workspace (#324, test).
- A mesher form starts from the parameters of the mesh it would replace (#343).
- Seed Edges and edge/vertex/face datum points switch the selection filter
  while open and restore it (#341).
- Datum forms draw the datum live, the Partition form shows its datum, and
  new datums and partitions get the next free name (#347).

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-15 09:14:34 -04:00
1 parent 3eaca26246
commit 4ee855bb27
11 files changed
+447 -28

No files matched your search

+49 -2
View File
@@ -33,8 +33,16 @@ impl ControlRequest {
}
}
/// The key of the interim reply to a request that waits for a background job
/// ([`Outcome::Pending`]): the server then waits for the final reply however long the job takes,
/// since the job always answers (its result, its error, or that it was cancelled).
pub const PENDING: &str = "pending";
pub enum Outcome {
Done(Value),
/// The reply comes when the background job finishes: the request's sender waits in
/// [`crate::files::Loading::waiters`].
Pending,
/// A window screenshot, answered once a frame is presented.
Screenshot {
path: Option<String>,
@@ -170,6 +178,17 @@ fn privacy_json(app: &ModelApp) -> Value {
})
}
/// Answer `req` when the running job finishes; with none running, at once.
fn wait_job(app: &mut ModelApp, req: &ControlRequest) -> Outcome {
match app.loading.as_mut() {
Some(l) => {
l.waiters.push(req.reply.clone());
Outcome::Pending
}
None => ok(json!({"job": null})),
}
}
pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome {
let p = &req.params;
let s = |k: &str| p.get(k).and_then(Value::as_str);
@@ -182,8 +201,25 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
"engine.execute" => {
let Some(id) = s("command") else { return err("missing `command`") };
let params = p.get("params").cloned().filter(|v| !v.is_null()).unwrap_or(json!({}));
// A solve or a mesher runs as a job: the window keeps drawing its progress, and the
// reply comes when it is done however long that takes (or at once with `"wait":
// false`, then `job.status` / `job.wait` / `job.cancel`).
if crate::files::is_long(id) {
return match app.start_job(id, params) {
Err(e) => err(e),
Ok(job) if p.get("wait").and_then(Value::as_bool) == Some(false) => ok(json!({"job": job})),
Ok(_) => wait_job(app, req),
};
}
wrap(app.run(id, params))
}
"job.status" => ok(json!({"job": app.loading.as_ref().map(|l| l.to_json(app.now))})),
"job.wait" => wait_job(app, req),
"job.cancel" => {
let job = app.loading.as_ref().map(|l| l.to_json(app.now));
app.cancel_loading();
ok(json!({"cancelled": job}))
}
"engine.script" => {
let Some(steps) = p.get("commands") else { return err("missing `commands` (a list of {command, params})") };
wrap(app.run_script(steps).map(|()| json!({"journal": app.session.journal.len()})))
@@ -201,6 +237,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
"results": app.session.results_report(),
"probe": app.probe,
"busy": app.busy(),
"job": app.loading.as_ref().map(|l| l.to_json(app.now)),
"solve": solve_json(app),
"selected": app.selected.as_ref().map(|(k, n)| json!({"kind": k, "name": n})).or_else(|| app.selected_seed.as_ref().map(|e| json!({"kind": "seed", "edge": e}))),
"browser": crate::browser::describe(app),
@@ -634,10 +671,15 @@ mod tests {
use super::*;
fn call(app: &mut ModelApp, method: &str, params: Value) -> Value {
let (req, _rx) = ControlRequest::new(method, params);
let (req, rx) = ControlRequest::new(method, params);
match handle(app, &egui::Context::default(), &req) {
Outcome::Done(v) => v,
Outcome::Screenshot { .. } => json!({"screenshot": true}),
// A job's reply comes when it ends.
Outcome::Pending => {
app.wait_loaded();
rx.try_recv().unwrap_or(Value::Null)
}
}
}
@@ -692,10 +734,15 @@ mod tests {
}
fn call_in(app: &mut ModelApp, ctx: &egui::Context, method: &str, params: Value) -> Value {
let (req, _rx) = ControlRequest::new(method, params);
let (req, rx) = ControlRequest::new(method, params);
match handle(app, ctx, &req) {
Outcome::Done(v) => v,
Outcome::Screenshot { .. } => json!({"screenshot": true}),
// A job's reply comes when it ends.
Outcome::Pending => {
app.wait_loaded();
rx.try_recv().unwrap_or(Value::Null)
}
}
}