ui: Suppress/Unsuppress on datums and partitions, greyed in the browser (#291)

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 10:17:28 -04:00
1 parent 5189554a6a
commit 84f42f6f8a
5 files changed
+71 -7

No files matched your search

+3 -1
View File
@@ -973,7 +973,9 @@ pub fn datums(
) -> Aabb3 {
let mut d = Draw { scene, bounds: Aabb3::EMPTY, color: colors.datum, width: OUTLINE_WIDTH, on_top: false };
let diagonal = if bounds.is_empty() { 1.0 } else { bounds.diagonal().max(1e-9) };
for (name, datum) in model.datums().iter().filter(|(n, _)| shown(n) || highlight == Some(n.as_str())) {
// A suppressed datum is out of the model: drawn only while selected in the browser.
let drawn = |n: &String| (shown(n) && !model.datum_suppressed(n)) || highlight == Some(n.as_str());
for (name, datum) in model.datums().iter().filter(|(n, _)| drawn(n)) {
let picked = highlight == Some(name.as_str());
let color = if picked { colors.highlight } else { colors.datum };
let outline = if picked { HIGHLIGHT_WIDTH } else { OUTLINE_WIDTH };
+9 -4
View File
@@ -888,12 +888,17 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
"point" => "geometry.datum.point",
_ => "geometry.datum",
};
let row = Row { depth: 2, icon: Some(icon), label: n, note: Some((*kind).into()), ..Default::default() };
let off = app.session.model.datum_suppressed(n);
let note = if off { format!("{kind} · suppressed") } else { (*kind).into() };
let row = Row { depth: 2, icon: Some(icon), label: n, note: Some(note), dim: off, ..Default::default() };
item_row(app, ui, egui::Id::new(("fc_row_datum", i)), row, Target::Def(Def::Datum, n.clone()), acts);
}
let partitions: Vec<(String, String)> = app.session.model.partitions().iter().map(|p| (p.name.clone(), p.datum.clone())).collect();
for (i, (n, datum)) in partitions.iter().enumerate() {
let row = Row { depth: 2, icon: Some("geometry.partition"), label: n, note: Some(format!("by {datum}")), ..Default::default() };
let model = &app.session.model;
let partitions: Vec<(String, String, bool)> =
model.partitions().iter().map(|p| (p.name.clone(), p.datum.clone(), !model.partition_cuts(p))).collect();
for (i, (n, datum, off)) in partitions.iter().enumerate() {
let note = if *off { format!("by {datum} · suppressed") } else { format!("by {datum}") };
let row = Row { depth: 2, icon: Some("geometry.partition"), label: n, note: Some(note), dim: *off, ..Default::default() };
item_row(app, ui, egui::Id::new(("fc_row_partition", i)), row, Target::Def(Def::Partition, n.clone()), acts);
}
}
+5 -1
View File
@@ -384,6 +384,8 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
let action = match (s("action"), s("step")) {
(Some("edit"), _) => crate::menu::Action::Edit,
(Some("rename"), _) => crate::menu::Action::Rename,
(Some("suppress"), _) => crate::menu::Action::Suppress(true),
(Some("unsuppress"), _) => crate::menu::Action::Suppress(false),
(Some("hide"), _) => crate::menu::Action::Hide,
(Some("show"), _) => crate::menu::Action::Show,
(Some("isolate"), _) => crate::menu::Action::Isolate,
@@ -393,7 +395,9 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
crate::menu::Action::Active { step: step.to_string(), active: a == "activate" }
}
(a, _) => {
return err(format!("`action` {a:?}: edit, rename, hide, show, isolate, show_all, delete, or activate/deactivate with `step`"));
return err(format!(
"`action` {a:?}: edit, rename, suppress, unsuppress, hide, show, isolate, show_all, delete, or activate/deactivate with `step`"
));
}
};
crate::menu::apply(app, target, action);
+53
View File
@@ -45,6 +45,8 @@ pub enum Action {
Edit,
/// Rename a definition (`model.rename`'s form, filled in).
Rename,
/// Suppress a datum or partition, or bring it back.
Suppress(bool),
Hide,
Show,
Isolate,
@@ -82,6 +84,14 @@ pub fn show(app: &ModelApp, ui: &mut egui::Ui, target: &Target) -> Option<Action
if def.rename_kind().is_some() {
entry(ui, true, "Rename…", "model.rename: every definition that refers to it follows", Action::Rename);
}
if let Some((command, suppressed)) = suppression(app, *def, name) {
let (label, tip) = if suppressed {
("Unsuppress", format!("{command} {{\"suppressed\": false}}: back in the model"))
} else {
("Suppress", format!("{command}: keep it, but leave it out of the model"))
};
entry(ui, true, label, &tip, Action::Suppress(!suppressed));
}
}
if let Target::Seed(_) = target {
entry(ui, true, "Edit…", "mesh.seed.edge", Action::Edit);
@@ -157,6 +167,13 @@ pub fn apply(app: &mut ModelApp, target: Target, action: Action) {
}
Ok(Value::Null)
}
Action::Suppress(on) => match &target {
Target::Def(def, name) => match suppression(app, *def, name) {
Some((command, _)) => app.run(command, json!({"name": name, "suppressed": on})),
None => Ok(Value::Null),
},
_ => Ok(Value::Null),
},
Action::Hide => app.run("view.hide", items(&target)),
Action::Show => app.run("view.show", items(&target)),
Action::Isolate => app.run("view.isolate", items(&target)),
@@ -179,6 +196,17 @@ pub fn apply(app: &mut ModelApp, target: Target, action: Action) {
};
}
/// The command that suppresses `name` of `kind` (a datum or a partition), and whether it is
/// suppressed now.
fn suppression(app: &ModelApp, kind: Def, name: &str) -> Option<(&'static str, bool)> {
let model = &app.session.model;
match kind {
Def::Datum => Some(("geometry.datum.suppress", model.datum_suppressed(name))),
Def::Partition => Some(("geometry.partition.suppress", model.partitions().iter().find(|p| p.name == name)?.suppressed)),
_ => None,
}
}
/// `mesh.seed.edge`'s parameters that make `seed` as it is: its edit form, filled in.
pub fn seed_params(seed: &fieldcraft_model::Seed) -> Option<Value> {
let mut v = serde_json::to_value(seed).ok()?;
@@ -312,6 +340,31 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}
/// Suppress and Unsuppress on a partition's and a datum's menu run their suppress commands:
/// the cells merge back and come again; a suppressed datum is not drawn (#291).
#[test]
fn suppress_from_the_menu_merges_the_cells_back() {
let (mut app, dir) = plate();
let bodies = |app: &ModelApp| app.session.design.as_ref().map_or(0, |d| d.bodies.len());
assert_eq!(bodies(&app), 2);
apply(&mut app, Target::Def(Def::Partition, "Half".into()), Action::Suppress(true));
assert_eq!(bodies(&app), 1, "{:?}", app.status);
assert_eq!(suppression(&app, Def::Partition, "Half"), Some(("geometry.partition.suppress", true)));
apply(&mut app, Target::Def(Def::Partition, "Half".into()), Action::Suppress(false));
assert_eq!(bodies(&app), 2);
apply(&mut app, Target::Def(Def::Datum, "Mid".into()), Action::Suppress(true));
assert_eq!(bodies(&app), 1);
let drawn = |app: &mut ModelApp| {
let b = app.display().bounds;
let mut s = fieldcraft_render::Scene::default();
fieldcraft_render::glyphs::datums(&app.session.model, &b, &|_| true, None, &crate::theme::Tokens::dark().glyph_colors(), &mut s);
s.labels.into_iter().map(|l| l.text).collect::<Vec<_>>()
};
assert_eq!(drawn(&mut app), ["P"], "the suppressed plane is not drawn");
assert_eq!(suppression(&app, Def::Set, "x"), None);
let _ = std::fs::remove_dir_all(dir);
}
/// A screen point on datum `name`'s drawn lines.
fn on_datum(app: &mut ModelApp, name: &str) -> egui::Pos2 {
let bounds = app.display().bounds;
+1 -1
View File
@@ -56,7 +56,7 @@ commands (files, undo, `model.info`) leave the ribbon where it is.
| `ui.selection` | — | the viewport's selection: `filter`, `box_mode` (box selection on), `count`, `capped`, `summary`, `items`, `params` (the selection as a set's members), `hover` (what a click would pick under the pointer) |
| `ui.prompt` | `answer`: for `unsaved` `save`, `discard` or `cancel`; for `design` `new` (a new model with the design), `keep` (the design over the current model) or `cancel`; for `replace_mesh` `replace` (runs the form, replacing the mesh) or `cancel` (the form stays open); for `delete` (the item menu's Delete… or the Delete key: `{delete: question, kind, name, users, allowed}`) `delete` (with a datum's partitions when it has any; refused when `allowed` is false) or `cancel`; for `recover` `recover` (with `id` from `file.recovery.list`) or `close`; for `samples` `open` (with `name`) or `close`; for `crashed` (the start after a crash: `{crashed: [reports]}`) `report` (opens Report a Bug for the newest) or `close`; for `report_bug` (`{report_bug: draft}`) `set` (any of `title`, `observed`, `share_session`, `include_model`; replies with the draft) or `close` — the browser opens only from a click (docs/crash-reports.md); for `consent` (the first run's question about anonymous usage) `share` or `decline`; for `settings` (Help › Settings…) `set` (any of `usage_sharing`, `usage_automation`; or `update_check`, `update_skip` (a version, `""` for none)) or `close` (docs/privacy.md, docs/updates.md); for `update` (a downloaded and verified update: `{update: state}`) `restart` (saves or asks about unsaved changes first, then closes the window to install) or `later` (installs when the app quits) | answers the window's question (`ui.inspect`'s `prompt`) |
| `ui.settings` | — | opens Help › Settings… (answered with `ui.prompt`); replies with the privacy state as `ui.inspect`'s `privacy` gives it: `available` (this run may share usage), `usage_sharing` (`null` until asked), `usage_automation`, `recording`, `queued` |
| `ui.item` | `kind` (`body`, `surface`, or a definition kind as `ui.select` takes it, `datum`, `partition`), `name`, `action`: `edit`, `rename`, `hide`, `show`, `isolate`, `show_all`, `delete`, or `activate`/`deactivate` with `step` | an item menu's entry, as a right click on the item and a click on the entry: runs the same command (`rename` opens the `model.rename` form filled in; `delete` opens the delete prompt, answered with `ui.prompt`) |
| `ui.item` | `kind` (`body`, `surface`, or a definition kind as `ui.select` takes it, `datum`, `partition`), `name`, `action`: `edit`, `rename`, `suppress`/`unsuppress` (a datum or partition), `hide`, `show`, `isolate`, `show_all`, `delete`, or `activate`/`deactivate` with `step` | an item menu's entry, as a right click on the item and a click on the entry: runs the same command (`rename` opens the `model.rename` form filled in; `delete` opens the delete prompt, answered with `ui.prompt`) |
| `ui.dialog` | `params?` (JSON object, fills the form), `use_selection?`, `run?`, `submit?`, `cancel?` | drives the open command form, in that order; `use_selection` is the form's Target row (see [Selection](#selection)); `run` returns the command's result and closes the form on success; `submit` is the form's OK button: a mesher that would replace a body's mesh first asks (`prompt` `replace_mesh`, with the question and what else goes: sets listing the old labels, solved results), and meshers run in the background (`busy`); a form opened by another form's **Create …** button (a reference field with nothing to pick) goes back to that form on `run` (with the new item picked) or `cancel` |
| `ui.click` | `x`, `y` (screen points), `button?`: `left`/`right`/`middle`, `shift?`, `ctrl?`, `alt?`, `double?` | real pointer input; in the viewport a click selects what is under it (Shift adds, Ctrl/Cmd adds or removes, a click on nothing clears the selection), and a click on a results plot also probes the nearest node of the face under it (`solve.probe`; `ui.inspect` shows the values) |
| `ui.move` | `x`, `y` | |