Browser: setup state marks and ghost rows on the tree

Each top-level node that is a setup stage carries a mark at the right
of its row beside its count (done, next, blocked) with the reason and
place in its hover, and a node still lacking what its stage needs shows
a ghost row that starts the providing command. ui.inspect reports each
node's state as browser.

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-14 22:49:07 -04:00
1 parent 910d437819
commit f5a84f5f74
3 files changed
+318 -16

No files matched your search

+295 -15
View File
@@ -5,10 +5,18 @@
//! the solved steps' results. Clicking a definition selects it (its properties show on the
//! right), a double click opens its edit form, and clicking a result step shows it.
//!
//! The tree also shows where the model is in the analysis workflow
//! ([`fieldcraft_engine::progress`]): each top-level node that is a setup stage carries a mark at
//! the right of its row, beside its count (✓ done, → the next stage, ⚠ when its command cannot
//! run yet), with the reason and the place in its hover, and a node that still lacks what its
//! stage needs shows a ghost row ("+ Add material…") that starts the providing command. Optional
//! stages (seeds and partitions) carry no mark.
//!
//! Row drawing follows SolidCraft's `crates/ui-egui/src/browser.rs` (MIT OR Apache-2.0), see
//! ATTRIBUTION.md.
use egui::{Color32, FontId, Pos2, Rect, RichText, Sense, Stroke, Vec2, pos2, vec2};
use fieldcraft_engine::progress::{Stage, StageState, Status};
use fieldcraft_engine::schema::Collection;
use fieldcraft_model::Model;
use serde::Serialize;
@@ -26,6 +34,118 @@ const INDENT: f32 = 14.0;
const NOTE_GAP: f32 = 6.0;
/// Free space kept at a row's right end.
const ROW_PAD: f32 = 4.0;
/// The state mark's slot at a top-level row's right end.
const MARK_W: f32 = 14.0;
/// The browser's top-level nodes below the model, in order, with the setup stages each shows
/// (`ui.inspect`'s `browser`). Seeds and partitions ([`Stage::Prepare`]) are optional and sit
/// under Geometry and Mesh without a mark.
pub const NODES: [(&str, &[Stage]); 10] = [
("geometry", &[Stage::Geometry]),
("mesh", &[Stage::Mesh]),
("sets", &[Stage::Sets]),
("materials", &[Stage::Material]),
("sections", &[Stage::Section]),
("steps", &[Stage::Step]),
("loads", &[Stage::BoundaryConditions, Stage::Loads]),
("interactions", &[]),
("fluids", &[]),
("results", &[Stage::Solve, Stage::Results]),
];
fn node_stages(node: &str) -> &'static [Stage] {
NODES.iter().find(|(n, _)| *n == node).map_or(&[], |(_, st)| st)
}
/// A stage's hover: what it needs and where; `None` once done or when optional.
fn stage_tip(s: &StageState) -> Option<String> {
match s.status {
Status::Next => Some(format!("Next: {} — {}", s.label, s.place)),
Status::Blocked => Some(s.detail.clone().unwrap_or_else(|| s.place.clone())),
Status::Todo => Some(format!("{} still to do — {}", s.label, s.place)),
Status::Done | Status::Optional => None,
}
}
/// A node's state and hover from its stages: the most pressing of them (next, blocked, to do),
/// else done; `None` for nodes that are no setup stage.
fn node_state(stages: &[StageState], node: &str) -> Option<(Status, String)> {
let own: Vec<&StageState> = stages.iter().filter(|s| node_stages(node).contains(&s.stage)).collect();
let status =
[Status::Next, Status::Blocked, Status::Todo, Status::Done, Status::Optional].into_iter().find(|st| own.iter().any(|s| s.status == *st))?;
let tip: Vec<String> = own.iter().filter_map(|s| stage_tip(s)).collect();
let tip = if tip.is_empty() { "Done".to_string() } else { tip.join("\n") };
Some((status, tip))
}
/// A ghost row's text: what its stage's command adds.
fn ghost_label(stage: Stage) -> &'static str {
match stage {
Stage::Geometry => "Open design…",
Stage::Prepare => "Seed edges…",
Stage::Mesh => "Generate mesh…",
Stage::Material => "Add material…",
Stage::Section => "Add section…",
Stage::Sets => "Add set…",
Stage::BoundaryConditions => "Add boundary condition…",
Stage::Loads => "Add load…",
Stage::Step => "Add step…",
Stage::Solve => "Solve…",
Stage::Results => "Show results…",
}
}
/// Where a stage's ghost row was drawn (`ui.inspect`'s `layout`).
pub fn ghost_region(stage: Stage) -> &'static str {
match stage {
Stage::Geometry => "browser/add/geometry",
Stage::Prepare => "browser/add/prepare",
Stage::Mesh => "browser/add/mesh",
Stage::Material => "browser/add/material",
Stage::Section => "browser/add/section",
Stage::Sets => "browser/add/sets",
Stage::BoundaryConditions => "browser/add/boundary_conditions",
Stage::Loads => "browser/add/loads",
Stage::Step => "browser/add/step",
Stage::Solve => "browser/add/solve",
Stage::Results => "browser/add/results",
}
}
/// The stages of `node` still to do: each shows a ghost row, except a blocked one behind an
/// earlier stage of the same node still to do (Results offers Solve, not Show results, until a
/// solve).
fn ghosts<'s>(stages: &'s [StageState], node: &str) -> Vec<&'s StageState> {
let mut pending = false;
let mut out = Vec::new();
for s in stages.iter().filter(|s| node_stages(node).contains(&s.stage)) {
let todo = matches!(s.status, Status::Next | Status::Todo | Status::Blocked);
if todo && !(pending && s.status == Status::Blocked) {
out.push(s);
}
pending |= todo;
}
out
}
/// Each top-level node's setup state for `ui.inspect`: `[{node, state, tip, add: [{stage,
/// label, command, status}]}]`; `state` is null for nodes that are no setup stage.
pub fn describe(app: &mut ModelApp) -> Value {
let stages = app.setup_stages();
Value::Array(
NODES
.iter()
.map(|(node, _)| {
let state = node_state(&stages, node);
let add: Vec<Value> = ghosts(&stages, node)
.into_iter()
.map(|s| json!({"stage": s.stage, "label": ghost_label(s.stage), "command": s.command, "status": s.status}))
.collect();
json!({"node": node, "state": state.as_ref().map(|(st, _)| st), "tip": state.map(|(_, tip)| tip), "add": add})
})
.collect(),
)
}
/// A kind of model definition.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
@@ -204,6 +324,10 @@ struct Row<'a> {
note: Option<String>,
selected: bool,
dim: bool,
/// The setup state mark at the row's right end (top-level nodes).
mark: Option<Status>,
/// The hover text (a setup state's reason and place).
tip: Option<String>,
}
/// What happened on a row.
@@ -273,24 +397,33 @@ fn draw_row(app: &mut ModelApp, ui: &mut egui::Ui, id: egui::Id, row: &Row) -> R
let col = if row.dim { t.text_dim } else { t.text };
// The note (a count, a seed's size and bias) keeps its room; the label gets the rest, elided in
// the middle when long, whole on hover.
// The trailing area, right to left: the state mark's slot (top-level rows keep it so their
// counts line up), then the note.
let right = r.right() - ROW_PAD - if row.depth == 1 { MARK_W } else { 0.0 };
if let Some(status) = row.mark {
paint_mark(ui.painter(), Rect::from_center_size(pos2(right + MARK_W / 2.0, cy), Vec2::splat(9.0)), status, &t);
}
let note = row.note.as_ref().map(|n| ui.painter().layout_no_wrap(n.clone(), FontId::proportional(11.5), t.text_dim));
let note_w = note.as_ref().map_or(0.0, |g| g.size().x + NOTE_GAP);
let font = FontId::proportional(12.5);
let room = (r.right() - ROW_PAD - note_w - x).max(0.0);
let room = (right - note_w - x).max(0.0);
let label = crate::elide::middle_in(ui, row.label, &font, room);
let elided = matches!(label, std::borrow::Cow::Owned(_));
let galley = ui.painter().layout_no_wrap(label.into_owned(), font, col);
let lw = galley.size().x;
ui.painter().with_clip_rect(r).galley(pos2(x, cy - galley.size().y / 2.0), galley, col);
if let Some(n) = note {
let at = pos2(x + lw + NOTE_GAP, cy - n.size().y / 2.0);
let at = pos2(right - n.size().x, cy - n.size().y / 2.0);
ui.painter().with_clip_rect(r).galley(at, n, t.text_dim);
}
let tip = match hover_on {
1 => Some(if row.fold == Some(true) { "Collapse" } else { "Expand" }),
2 => Some(if row.eye == Some(true) { "Hide" } else { "Show" }),
_ if elided => Some(row.label),
_ => None,
1 => Some((if row.fold == Some(true) { "Collapse" } else { "Expand" }).to_string()),
2 => Some((if row.eye == Some(true) { "Hide" } else { "Show" }).to_string()),
_ => match (&row.tip, elided) {
(Some(tip), true) => Some(format!("{}\n\n{tip}", row.label)),
(Some(tip), false) => Some(tip.clone()),
(None, true) => Some(row.label.to_string()),
(None, false) => None,
},
};
let resp = match tip {
Some(tip) => resp.on_hover_text(tip),
@@ -302,12 +435,70 @@ fn draw_row(app: &mut ModelApp, ui: &mut egui::Ui, id: egui::Id, row: &Row) -> R
out
}
/// ✓ done (success), → next (accent), ⚠ blocked (muted); nothing for the rest.
fn paint_mark(p: &egui::Painter, r: Rect, status: Status, t: &Tokens) {
let at = |x: f32, y: f32| Pos2::new(r.left() + x * r.width(), r.top() + y * r.height());
match status {
Status::Done => {
let s = Stroke::new(1.6, t.ok);
p.line_segment([at(0.05, 0.55), at(0.4, 0.88)], s);
p.line_segment([at(0.4, 0.88), at(0.95, 0.15)], s);
}
Status::Next => {
let s = Stroke::new(1.6, t.accent);
p.line_segment([at(0.0, 0.5), at(0.95, 0.5)], s);
p.line_segment([at(0.55, 0.15), at(0.95, 0.5)], s);
p.line_segment([at(0.55, 0.85), at(0.95, 0.5)], s);
}
Status::Blocked => {
let s = Stroke::new(1.2, t.text_dim);
p.add(egui::Shape::closed_line(vec![at(0.5, 0.0), at(1.05, 1.0), at(-0.05, 1.0)], s));
p.line_segment([at(0.5, 0.35), at(0.5, 0.65)], s);
p.circle_filled(at(0.5, 0.84), 0.7, t.text_dim);
}
Status::Todo | Status::Optional => {}
}
}
/// A ghost row under a node that lacks what its stage needs: "+ Add material…", muted, the next
/// stage's in the accent colour. Clicking it (or Enter/Space when it has the keyboard focus)
/// starts the stage's command; true when activated.
fn draw_ghost(app: &mut ModelApp, ui: &mut egui::Ui, s: &StageState) -> bool {
let t = Tokens::get(app.ui.dark);
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), ROW_H), Sense::hover());
app.place(ghost_region(s.stage), r);
if !ui.is_rect_visible(r) {
return false;
}
let resp = ui.interact(r, egui::Id::new(("fc_row_add", ghost_region(s.stage))), Sense::click());
if resp.hovered() {
ui.painter().rect_filled(r, 3.0, t.hover);
}
if resp.has_focus() {
ui.painter().rect_stroke(r.shrink(0.5), 3.0, Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
}
let col = if s.status == Status::Next { t.accent } else { t.text_dim };
// Aligned with the node's children: past the fold arrow, the plus in the icon's place.
let x = r.left() + 2.0 + 2.0 * INDENT + 14.0;
let c = pos2(x + 8.0, r.center().y);
let plus = Stroke::new(1.3, col);
ui.painter().line_segment([c - vec2(4.0, 0.0), c + vec2(4.0, 0.0)], plus);
ui.painter().line_segment([c - vec2(0.0, 4.0), c + vec2(0.0, 4.0)], plus);
let label = ghost_label(s.stage);
ui.painter().with_clip_rect(r).text(pos2(x + 21.0, r.center().y), egui::Align2::LEFT_CENTER, label, FontId::proportional(12.5), col);
let resp = resp.on_hover_text(stage_tip(s).unwrap_or_default());
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, label));
resp.clicked()
}
/// What the browser asks for after drawing (it cannot run commands while it borrows the model).
enum Act {
Fold(String),
Select(Def, String),
Edit(Def, String),
ShowResult(String),
/// A ghost row's command.
Start(&'static str),
}
/// A seed in a few words: `8 exact`, `2.5 min, bias 4 end`.
@@ -362,6 +553,10 @@ pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) {
// Errors show in the status bar.
let _ = app.run("results.deformed", json!({"step": step}));
}
Act::Start(command) => {
// A command that cannot run yet says why in the status bar.
let _ = app.start(command);
}
}
}
}
@@ -375,8 +570,19 @@ fn eye_flag<'u>(state: &'u mut crate::UiState, group: &str) -> Option<&'u mut bo
}
}
/// The ghost rows of `node`'s stages still to do.
fn ghost_rows(app: &mut ModelApp, ui: &mut egui::Ui, stages: &[StageState], node: &str, acts: &mut Vec<Act>) {
for s in ghosts(stages, node) {
if draw_ghost(app, ui, s) {
acts.push(Act::Start(s.command));
}
}
}
fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let t = Tokens::get(app.ui.dark);
let stages = app.setup_stages();
let state = |node: &str| node_state(&stages, node).map_or((None, None), |(st, tip)| (Some(st), Some(tip)));
let open = |app: &ModelApp, key: &str| app.tree_open.contains(key);
let name = app.model_name();
let dirty = app.session.is_dirty();
@@ -390,6 +596,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let design = app.session.design.clone();
let surfaces: Vec<String> = app.session.surfaces.iter().map(|s| s.name.clone()).collect();
let bodies = design.as_ref().map_or(0, |d| d.bodies.len());
let (mark, tip) = state("geometry");
let r = draw_row(
app,
ui,
@@ -401,6 +608,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
icon: Some("geometry.open"),
label: "Geometry",
note: Some(format!("{}", bodies + surfaces.len())),
mark,
tip,
..Default::default()
},
);
@@ -455,17 +664,11 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
&Row { depth: 2, icon: Some("geometry.partition"), label: n, note: Some(format!("by {datum}")), ..Default::default() },
);
}
if design.is_none() && surfaces.is_empty() {
draw_row(
app,
ui,
egui::Id::new("fc_row_no_geometry"),
&Row { depth: 2, label: "No geometry: Open Design", dim: true, ..Default::default() },
);
}
}
ghost_rows(app, ui, &stages, "geometry", acts);
// Mesh.
let (nodes, elements) = (app.session.model.nodes().len(), app.session.model.elements().len());
let (mark, tip) = state("mesh");
let r = draw_row(
app,
ui,
@@ -477,6 +680,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
icon: Some("mesh.tet.generate"),
label: "Mesh",
note: Some(format!("{elements}")),
mark,
tip,
..Default::default()
},
);
@@ -549,6 +754,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
}
}
}
ghost_rows(app, ui, &stages, "mesh", acts);
// Definitions.
let groups: [(&str, &str, &[Def]); 7] = [
("sets", "Sets", &[Def::Set]),
@@ -563,6 +769,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let items: Vec<(Def, String)> = kinds.iter().flat_map(|k| k.names(&app.session.model).into_iter().map(move |n| (*k, n))).collect();
let icon = kinds.first().map(|k| k.icon());
let eye = eye_flag(&mut app.ui, key).map(|v| *v);
let (mark, tip) = state(key);
let r = draw_row(
app,
ui,
@@ -575,6 +782,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
label,
note: Some(format!("{}", items.len())),
dim: items.is_empty(),
mark,
tip,
..Default::default()
},
);
@@ -601,10 +810,12 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
}
}
}
ghost_rows(app, ui, &stages, key, acts);
}
// Results.
let steps: Vec<(String, bool)> = app.session.result_steps().map(|(n, c)| (n.to_string(), c)).collect();
let shown = app.session.results_view.as_ref().map(|v| v.step.clone());
let (mark, tip) = state("results");
let r = draw_row(
app,
ui,
@@ -616,6 +827,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
label: "Results",
note: Some(format!("{}", steps.len())),
dim: steps.is_empty(),
mark,
tip,
..Default::default()
},
);
@@ -637,6 +850,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
}
}
}
ghost_rows(app, ui, &stages, "results", acts);
}
#[cfg(test)]
@@ -644,6 +858,72 @@ mod tests {
use fieldcraft_engine::Session;
use super::*;
use Status::*;
/// Each top-level node's state, and the commands of its ghost rows.
fn marks(app: &mut ModelApp) -> Vec<(&'static str, Option<Status>, Vec<&'static str>)> {
let stages = app.setup_stages();
NODES
.iter()
.map(|(n, _)| (*n, node_state(&stages, n).map(|(st, _)| st), ghosts(&stages, n).into_iter().map(|s| s.command).collect()))
.collect()
}
fn mark(app: &mut ModelApp, node: &str) -> Option<Status> {
marks(app).into_iter().find(|(n, ..)| *n == node).and_then(|(_, st, _)| st)
}
/// The setup's progress on the tree from an empty model to a solved one: one next stage at a
/// time, a warning where a command cannot run yet, ghost rows on nodes still lacking theirs,
/// and no mark on nodes that are no setup stage.
#[test]
fn nodes_mark_the_setup_progress() {
let mut app = ModelApp::new(Session::default());
let m = marks(&mut app);
assert_eq!(m[0], ("geometry", Some(Next), vec!["geometry.open"]));
assert_eq!(m[1], ("mesh", Some(Blocked), vec!["mesh.tet.generate"]));
assert_eq!(mark(&mut app, "materials"), Some(Todo));
assert_eq!(mark(&mut app, "results"), Some(Blocked));
assert_eq!(mark(&mut app, "interactions"), None);
assert_eq!(mark(&mut app, "fluids"), None);
let tip = node_state(&app.setup_stages(), "mesh").map(|(_, tip)| tip).unwrap_or_default();
assert!(tip.starts_with("Needs geometry") && tip.ends_with("— Geometry › Open Design"), "{tip}");
let next = |app: &mut ModelApp| marks(app).into_iter().filter(|(_, st, _)| *st == Some(Next)).map(|(n, ..)| n).collect::<Vec<_>>();
assert_eq!(next(&mut app), ["geometry"]);
// A design: meshing is next.
let dir = std::env::temp_dir().join(format!("fieldcraft-browser-marks-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let design = dir.join("plate.solidcraft");
std::fs::write(&design, fieldcraft_geometry::sample::plate_design(&fieldcraft_geometry::sample::Plate::default()).unwrap()).unwrap();
app.run("geometry.open", json!({"path": design.to_string_lossy()})).unwrap();
assert_eq!(marks(&mut app)[0], ("geometry", Some(Done), vec![]));
assert_eq!(next(&mut app), ["mesh"]);
// Meshed: the material is next; the section waits for it.
let body = app.session.design.as_ref().unwrap().bodies[0].name.clone();
app.run("mesh.tet.generate", json!({"body": body, "tolerance": 2.0})).unwrap();
assert_eq!(mark(&mut app, "mesh"), Some(Done));
assert_eq!(next(&mut app), ["materials"]);
assert_eq!(mark(&mut app, "sections"), Some(Blocked));
let _ = std::fs::remove_dir_all(&dir);
// Completely defined: solving is next, under Results.
let mut app = ModelApp::new(Session::default());
app.run_script(&fieldcraft_engine::sample::cantilever()).unwrap();
for (node, st, add) in marks(&mut app) {
match node {
"results" => assert_eq!((st, add), (Some(Next), vec!["solve.run"])),
"interactions" | "fluids" => assert_eq!(st, None),
_ => assert_eq!((node, st, add), (node, Some(Done), vec![])),
}
}
// Solved: every stage done, no ghost rows left.
app.run("solve.run", json!({})).unwrap();
assert!(marks(&mut app).iter().all(|(_, st, add)| matches!(st, None | Some(Done)) && add.is_empty()), "{:?}", marks(&mut app));
let v = describe(&mut app);
assert_eq!(v[2], json!({"node": "sets", "state": "done", "tip": "Done", "add": []}));
}
#[test]
fn definitions_read_back_as_their_parameters() {
+22
View File
@@ -189,6 +189,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
"busy": app.busy(),
"solve": solve_json(app),
"selected": app.selected.as_ref().map(|(k, n)| json!({"kind": k, "name": n})),
"browser": crate::browser::describe(app),
"nav": app.nav,
"layout": app.layout.iter().map(|(k, r)| (k.to_string(), json!([r.left(), r.top(), r.width(), r.height()]))).collect::<serde_json::Map<String, Value>>(),
"prompt": app.prompt.as_ref().map(|p| match p {
@@ -620,6 +621,27 @@ mod tests {
assert!(!app.layout.contains_key("setup"));
}
/// A meshed model's Materials node shows a highlighted ghost row; a click on it opens the
/// material form, and `ui.inspect` reports each node's state.
#[test]
fn a_ghost_row_click_starts_its_command() {
let mut app = ModelApp::new(Session::default());
let ctx = egui::Context::default();
let cube = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
app.run("mesh.block", json!({"body": "B", "kind": "hex8", "corners": cube, "divisions": [1, 1, 1]})).unwrap();
frame(&mut app, &ctx);
let browser = call_in(&mut app, &ctx, "ui.inspect", json!({}))["result"]["browser"].clone();
let materials = browser.as_array().and_then(|b| b.iter().find(|n| n["node"] == "materials")).cloned().unwrap_or_default();
assert_eq!(materials["state"], "next", "{browser}");
assert_eq!(materials["add"][0]["command"], "model.material.create");
let at = app.layout[crate::browser::ghost_region(fieldcraft_engine::progress::Stage::Material)].center();
call_in(&mut app, &ctx, "ui.click", json!({"x": at.x, "y": at.y}));
for _ in 0..4 {
frame(&mut app, &ctx);
}
assert_eq!(app.dialog.as_ref().map(|d| d.command), Some("model.material.create"));
}
/// The cantilever's free end (x = 100), looked at from +x: picked by a simulated click and
/// by `ui.select`, made into a set through the set form's Target row, and resolved.
#[test]
+1 -1
View File
@@ -41,7 +41,7 @@ commands (files, undo, `model.info`) leave the ribbon where it is.
| Method | Params | Notes |
|---|---|---|
| `ui.inspect` | — | view settings (`ui`), camera, viewport rect (points), renderer (`gpu`/`cpu`) and adapter, open command form, status bar, node and element counts, the results display (`results`: what the `results.*` commands reply: view, `stale`, contour range, modes), the probed node (`probe`), `solve` (the title bar's Solve: `enabled`, and `unavailable` as `ui.toolbar`'s entries give it, listing everything `solve.run` misses), `busy` (the background job: a file opening, a solve, a mesher run from its form by a click on OK), `prompt` (`{"unsaved": path}`, `{"design": path}` (a design of another part opened over a meshed or defined model), `"recover"`, `"samples"`), `recent` (recent files), `selected` (the browser's selected definition), `nav` (the left-drag mode), `layout` (each region of the window drawn last frame as `[x, y, w, h]` in points: `app_bar`, `ribbon`, `workspace_switcher`, `document_tab`, `app_bar:<action>`, `browser`, `viewport`, `nav_bar`, `axis_view`, `selection_bar`, `legend` (while results are shown), `context`, `command_line`, `timeline`, `toolbar_dropdown`, `palette`), version |
| `ui.inspect` | — | view settings (`ui`), camera, viewport rect (points), renderer (`gpu`/`cpu`) and adapter, open command form, status bar, node and element counts, the results display (`results`: what the `results.*` commands reply: view, `stale`, contour range, modes), the probed node (`probe`), `solve` (the title bar's Solve: `enabled`, and `unavailable` as `ui.toolbar`'s entries give it, listing everything `solve.run` misses), `busy` (the background job: a file opening, a solve, a mesher run from its form by a click on OK), `prompt` (`{"unsaved": path}`, `{"design": path}` (a design of another part opened over a meshed or defined model), `"recover"`, `"samples"`), `recent` (recent files), `selected` (the browser's selected definition), `browser` (each top-level browser node's setup state: `[{node, state, tip, add}]`, `state` `done`, `next`, `blocked` (⚠: its command cannot run yet; `tip` says what it needs and where), `todo`, `optional` or null for nodes that are no setup stage, `add` its ghost rows `{stage, label, command, status}`; a ghost row's region is `layout`'s `browser/add/<stage>`), `nav` (the left-drag mode), `layout` (each region of the window drawn last frame as `[x, y, w, h]` in points: `app_bar`, `ribbon`, `workspace_switcher`, `document_tab`, `app_bar:<action>`, `browser`, `viewport`, `nav_bar`, `axis_view`, `selection_bar`, `legend` (while results are shown), `context`, `command_line`, `timeline`, `toolbar_dropdown`, `palette`), version |
| `ui.set` | any view setting: `style` (`shaded_edges`, `shaded`, `wireframe`), `show_nodes`, `show_geometry`, `show_mesh`, `show_loads` (boundary condition and load symbols), `show_interactions` (reference points, couplings, rigid walls), `perspective`, `dark`, `show_tree` (the browser), `show_timeline`, `workspace` (`prepare`, `model`, `analysis`, `results`), `tab` (the toolbar tab: `geometry`, `mesh`, `properties`, `assembly`, `steps`, `loads`, `interactions`, `solve`, `results`, `tools`; a tab of another workspace brings that workspace), `palette` (the command search open), `menu` (the open menu: `file`, `help`, `workspace`, `view`, `more`, or a panel's drop-down as `<tab>/<panel>` such as `mesh/Tet`; `null` closes it) | an unknown setting or value is an error; returns the settings |
| `ui.view` | `view`: `front`, `back`, `left`, `right`, `top`, `bottom`, `iso`, `home` (iso and fit), `fit` | every view frames the model as drawn (the deformed shape and the setup's symbols included) in the part of the viewport the overlays (`layout`'s `legend`, `selection_bar`, `axis_view`, `nav_bar`) leave free; `fit` keeps the direction of view. Opening another model, sample or design frames it in the home view (the same design opened again after an edit keeps the camera), and results that come up with part of the model out of sight are framed. Returns the camera |
| `ui.camera` | `yaw`, `pitch` (degrees), `distance?` (model units) | turns the camera to any orientation about its target: `yaw` about Z (0 looks along +Y, the front view; −45 is the iso view's, −90 the right view's), `pitch` the elevation above the XY plane; `distance` moves the eye (the zoom of an orthographic view). Angles must be finite and the distance above 0: the yaw is wrapped, the pitch clamped to ±90 and the distance to 0.001–10⁹; anything else is an error. A sequence of calls orbits smoothly. Returns the camera (`ui.inspect`'s, angles in radians) |