forked from 1b-agent/fieldcraft-upstream
Browser and viewport: item menu, per-item eyes, Delete key with confirmation
Right-click on any browser item (body, surface, datum, partition, set, material, section, step, boundary condition, load, reference point, coupling, contact, fluid domain) opens its menu: Edit…, Hide/Show, Isolate, Show All, Active in step (boundary conditions and loads) and Delete…. A right click on a datum or a partition's cut face in the viewport opens the same menu. Each item the viewport draws on its own has an eye; hidden items are neither drawn nor picked. Datums and partitions are selectable with properties. Delete… and the Delete key ask first (the prompt lists what uses the item: a datum's partitions are deleted with it, other definitions in use are refused); ui.prompt answers it. Every entry runs the same command as the ribbon and forms. 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
d7ec7f486a
commit
08f669acad
12 files changed
+646
-78
No files matched your search
+153
-56
@@ -2,8 +2,11 @@
|
||||
//! partitions) and Mesh with eye toggles, then the model's
|
||||
//! definitions (sets, materials, sections, steps, loads and boundary conditions, interactions)
|
||||
//! with counts (Loads & BCs and Interactions with eye toggles for their viewport symbols), and
|
||||
//! 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 solved steps' results. Clicking a definition, datum or partition selects it (its
|
||||
//! properties show on the right), a double click opens its edit form, and clicking a result step
|
||||
//! shows it. Each item the viewport draws on its own (bodies, surfaces, datums, boundary
|
||||
//! conditions, loads, reference points, couplings) has its own eye, and a right click on an item
|
||||
//! opens its menu ([`crate::menu`]: edit, hide, isolate, active in step, delete).
|
||||
//!
|
||||
//! 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
|
||||
@@ -24,6 +27,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::ModelApp;
|
||||
use crate::icons::{self, Look};
|
||||
use crate::menu::{self, Action, Target};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// The browser's width.
|
||||
@@ -161,9 +165,27 @@ pub enum Def {
|
||||
Coupling,
|
||||
Contact,
|
||||
FluidDomain,
|
||||
/// A datum plane, axis or point.
|
||||
Datum,
|
||||
Partition,
|
||||
}
|
||||
|
||||
impl Def {
|
||||
pub const ALL: [Def; 12] = [
|
||||
Def::Set,
|
||||
Def::Material,
|
||||
Def::Section,
|
||||
Def::Step,
|
||||
Def::BoundaryCondition,
|
||||
Def::Load,
|
||||
Def::ReferencePoint,
|
||||
Def::Coupling,
|
||||
Def::Contact,
|
||||
Def::FluidDomain,
|
||||
Def::Datum,
|
||||
Def::Partition,
|
||||
];
|
||||
|
||||
/// The kind of setup symbols a definition of this kind draws, if any.
|
||||
pub fn symbols(self) -> Option<fieldcraft_render::glyphs::Definition> {
|
||||
use fieldcraft_render::glyphs::Definition;
|
||||
@@ -174,7 +196,7 @@ impl Def {
|
||||
Def::Coupling => Definition::Coupling,
|
||||
Def::Contact => Definition::Contact,
|
||||
Def::Step => Definition::Step,
|
||||
Def::Set | Def::Material | Def::Section | Def::FluidDomain => return None,
|
||||
Def::Set | Def::Material | Def::Section | Def::FluidDomain | Def::Datum | Def::Partition => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -191,6 +213,8 @@ impl Def {
|
||||
Def::Coupling => Collection::Coupling,
|
||||
Def::Contact => Collection::Contact,
|
||||
Def::FluidDomain => Collection::FluidDomain,
|
||||
Def::Datum => Collection::Datum,
|
||||
Def::Partition => Collection::Partition,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +231,8 @@ impl Def {
|
||||
Def::Coupling => "coupling",
|
||||
Def::Contact => "contact",
|
||||
Def::FluidDomain => "fluid_domain",
|
||||
Def::Datum => "datum",
|
||||
Def::Partition => "partition",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +248,8 @@ impl Def {
|
||||
Def::Coupling => "Coupling",
|
||||
Def::Contact => "Contact",
|
||||
Def::FluidDomain => "Fluid Domain",
|
||||
Def::Datum => "Datum",
|
||||
Def::Partition => "Partition",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,19 +266,75 @@ impl Def {
|
||||
Def::Coupling => "model.coupling.create",
|
||||
Def::Contact => "interaction.contact.pair",
|
||||
Def::FluidDomain => "model.fluid_domain.create",
|
||||
Def::Datum => "geometry.datum",
|
||||
Def::Partition => "geometry.partition",
|
||||
}
|
||||
}
|
||||
|
||||
/// The command of this kind with `verb` (`create`, `edit`, `delete`); contacts are
|
||||
/// interactions, created as pairs or general contact.
|
||||
/// interactions, created as pairs or general contact; datums and partitions are geometry
|
||||
/// (a datum's edit command depends on its kind: [`Def::edit`]).
|
||||
pub fn command(self, verb: &str) -> String {
|
||||
match (self, verb) {
|
||||
(Def::Contact, "create") => "interaction.contact.pair".into(),
|
||||
(Def::Contact, _) => format!("interaction.contact.{verb}"),
|
||||
(Def::Datum | Def::Partition, "create") => self.icon().into(),
|
||||
(Def::Datum | Def::Partition, _) => format!("geometry.{}.{verb}", self.stem()),
|
||||
_ => format!("model.{}.{verb}", self.stem()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The command that edits definition `name` (without building its parameters); `None` when
|
||||
/// it is gone or cannot be edited.
|
||||
pub fn edit_command(self, m: &Model, name: &str) -> Option<String> {
|
||||
match self {
|
||||
Def::Partition => None,
|
||||
Def::Datum => m.datum(name).map(|d| fieldcraft_engine::cmd::datum_edit_params(name, d).0.to_string()),
|
||||
_ => self.exists(m, name).then(|| self.command("edit")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The command that edits definition `name` and its parameters as it is (its edit form,
|
||||
/// filled in); `None` when it is gone or cannot be edited (a partition).
|
||||
pub fn edit(self, m: &Model, name: &str) -> Option<(String, Value)> {
|
||||
match self {
|
||||
Def::Partition => None,
|
||||
Def::Datum => m.datum(name).map(|d| {
|
||||
let (command, params) = fieldcraft_engine::cmd::datum_edit_params(name, d);
|
||||
(command.to_string(), params)
|
||||
}),
|
||||
_ => definition(m, self, name).map(|v| (self.command("edit"), v)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The viewport item it is, for the kinds the viewport draws one by one (and can hide).
|
||||
pub fn item_kind(self) -> Option<fieldcraft_engine::ItemKind> {
|
||||
use fieldcraft_engine::ItemKind as K;
|
||||
match self {
|
||||
Def::BoundaryCondition => Some(K::BoundaryCondition),
|
||||
Def::Load => Some(K::Load),
|
||||
Def::ReferencePoint => Some(K::ReferencePoint),
|
||||
Def::Coupling => Some(K::Coupling),
|
||||
Def::Datum => Some(K::Datum),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The definitions that refer to `name`, as `kind \`name\`` strings: what refuses its
|
||||
/// deletion (a datum's partitions are deleted with it instead, when confirmed).
|
||||
pub fn users(self, m: &Model, name: &str) -> Vec<String> {
|
||||
match self {
|
||||
Def::Set => m.set_users(name),
|
||||
Def::Material => m.material_users(name),
|
||||
Def::BoundaryCondition => m.boundary_condition_users(name),
|
||||
Def::Load => m.load_users(name),
|
||||
Def::ReferencePoint => m.reference_point_users(name),
|
||||
Def::FluidDomain => m.fluid_domain_users(name),
|
||||
Def::Datum => m.datum_users(name),
|
||||
Def::Section | Def::Step | Def::Coupling | Def::Contact | Def::Partition => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many definitions of this kind the model has.
|
||||
pub fn count(self, m: &Model) -> usize {
|
||||
match self {
|
||||
@@ -264,6 +348,8 @@ impl Def {
|
||||
Def::Coupling => m.couplings().len(),
|
||||
Def::Contact => m.contacts().len(),
|
||||
Def::FluidDomain => m.fluid_domains().len(),
|
||||
Def::Datum => m.datums().len(),
|
||||
Def::Partition => m.partitions().len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +366,8 @@ impl Def {
|
||||
Def::Coupling => m.couplings().contains_key(name),
|
||||
Def::Contact => m.contacts().contains_key(name),
|
||||
Def::FluidDomain => m.fluid_domain(name).is_some(),
|
||||
Def::Datum => m.datum(name).is_some(),
|
||||
Def::Partition => m.partitions().iter().any(|p| p.name == name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +384,8 @@ impl Def {
|
||||
Def::Coupling => m.couplings().keys().cloned().collect(),
|
||||
Def::Contact => m.contacts().keys().cloned().collect(),
|
||||
Def::FluidDomain => m.fluid_domains().keys().cloned().collect(),
|
||||
Def::Datum => m.datums().keys().cloned().collect(),
|
||||
Def::Partition => m.partitions().iter().map(|p| p.name.clone()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,6 +403,8 @@ pub fn definition(m: &Model, kind: Def, name: &str) -> Option<Value> {
|
||||
Def::Coupling => serde_json::to_value(m.couplings().get(name)?),
|
||||
Def::Contact => serde_json::to_value(m.contacts().get(name)?),
|
||||
Def::FluidDomain => serde_json::to_value(m.fluid_domain(name)?),
|
||||
Def::Datum => serde_json::to_value(m.datum(name)?),
|
||||
Def::Partition => serde_json::to_value(m.partitions().iter().find(|p| p.name == name)?),
|
||||
}
|
||||
.ok()?;
|
||||
let mut v = if v.is_object() { v } else { json!({"value": v}) };
|
||||
@@ -351,6 +443,8 @@ struct RowResp {
|
||||
double: bool,
|
||||
fold: bool,
|
||||
eye: bool,
|
||||
/// The row's response, for its context menu (`None` when the row was not drawn).
|
||||
response: Option<egui::Response>,
|
||||
}
|
||||
|
||||
fn draw_row(app: &mut ModelApp, ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
|
||||
@@ -446,9 +540,39 @@ fn draw_row(app: &mut ModelApp, ui: &mut egui::Ui, id: egui::Id, row: &Row) -> R
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, row.label));
|
||||
out.clicked = resp.clicked() && click_on == 0;
|
||||
out.double = resp.double_clicked() && click_on == 0;
|
||||
out.response = Some(resp);
|
||||
out
|
||||
}
|
||||
|
||||
/// A row for one item: its eye when the viewport draws it on its own, selection by a click and
|
||||
/// its edit form by a double click (definitions, datums, partitions), and its menu by a right
|
||||
/// click.
|
||||
fn item_row(app: &mut ModelApp, ui: &mut egui::Ui, id: egui::Id, mut row: Row, target: Target, acts: &mut Vec<Act>) {
|
||||
if let Some(item) = target.item() {
|
||||
row.eye = Some(app.session.shows(item.kind, &item.name));
|
||||
}
|
||||
if let Target::Def(kind, name) = &target {
|
||||
row.selected = app.selected.as_ref().is_some_and(|(k, n)| k == kind && n == name);
|
||||
}
|
||||
let r = draw_row(app, ui, id, &row);
|
||||
if let Some(resp) = &r.response {
|
||||
resp.context_menu(|ui| {
|
||||
if let Some(a) = menu::show(app, ui, &target) {
|
||||
acts.push(Act::Menu(target.clone(), a));
|
||||
}
|
||||
});
|
||||
}
|
||||
if r.eye {
|
||||
acts.push(Act::Menu(target, if row.eye == Some(true) { Action::Hide } else { Action::Show }));
|
||||
} else if let Target::Def(kind, name) = target {
|
||||
if r.double {
|
||||
acts.push(Act::Edit(kind, name));
|
||||
} else if r.clicked {
|
||||
acts.push(Act::Select(kind, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ✓ 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());
|
||||
@@ -513,6 +637,8 @@ enum Act {
|
||||
ShowResult(String),
|
||||
/// A ghost row's command.
|
||||
Start(&'static str),
|
||||
/// An item menu's entry, or an item's eye.
|
||||
Menu(Target, Action),
|
||||
}
|
||||
|
||||
/// A seed in a few words: `8 exact`, `2.5 min, bias 4 end`.
|
||||
@@ -557,11 +683,12 @@ pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
app.selected = if app.selected.as_ref() == Some(&(kind, name.clone())) { None } else { Some((kind, name)) };
|
||||
}
|
||||
Act::Edit(kind, name) => {
|
||||
if let Some(v) = definition(&app.session.model, kind, &name) {
|
||||
if let Some((command, params)) = kind.edit(&app.session.model, &name) {
|
||||
app.selected = Some((kind, name));
|
||||
app.open_form(&kind.command("edit"), v);
|
||||
app.open_form(&command, params);
|
||||
}
|
||||
}
|
||||
Act::Menu(target, action) => menu::apply(app, target, action),
|
||||
Act::ShowResult(step) => {
|
||||
// Errors show in the status bar.
|
||||
let _ = app.run("results.deformed", json!({"step": step}));
|
||||
@@ -641,40 +768,29 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
|
||||
&Row { depth: 2, icon: Some("geometry.open"), label: &label, note: Some(format!("{bodies} bodies")), ..Default::default() },
|
||||
);
|
||||
for (i, b) in d.bodies.iter().enumerate() {
|
||||
draw_row(
|
||||
app,
|
||||
ui,
|
||||
egui::Id::new(("fc_row_body", i)),
|
||||
&Row { depth: 3, icon: Some("geometry.bodies"), label: &b.name, ..Default::default() },
|
||||
);
|
||||
let row = Row { depth: 3, icon: Some("geometry.bodies"), label: &b.name, ..Default::default() };
|
||||
item_row(app, ui, egui::Id::new(("fc_row_body", i)), row, Target::Body(b.name.clone()), acts);
|
||||
}
|
||||
}
|
||||
for (i, s) in surfaces.iter().enumerate() {
|
||||
draw_row(
|
||||
app,
|
||||
ui,
|
||||
egui::Id::new(("fc_row_surface", i)),
|
||||
&Row { depth: 2, icon: Some("geometry.import.mesh"), label: s, ..Default::default() },
|
||||
);
|
||||
let row = Row { depth: 2, icon: Some("geometry.import.mesh"), label: s, ..Default::default() };
|
||||
item_row(app, ui, egui::Id::new(("fc_row_surface", i)), row, Target::Surface(s.clone()), acts);
|
||||
}
|
||||
// Datum planes and axes, then the partitions in the order they cut.
|
||||
// Datum planes, axes and points, then the partitions in the order they cut.
|
||||
let datums: Vec<(String, &'static str)> = app.session.model.datums().iter().map(|(n, d)| (n.clone(), d.kind())).collect();
|
||||
for (i, (n, kind)) in datums.iter().enumerate() {
|
||||
draw_row(
|
||||
app,
|
||||
ui,
|
||||
egui::Id::new(("fc_row_datum", i)),
|
||||
&Row { depth: 2, icon: Some("geometry.datum"), label: n, note: Some((*kind).into()), ..Default::default() },
|
||||
);
|
||||
let icon = match *kind {
|
||||
"axis" => "geometry.datum.axis",
|
||||
"point" => "geometry.datum.point",
|
||||
_ => "geometry.datum",
|
||||
};
|
||||
let row = Row { depth: 2, icon: Some(icon), label: n, note: Some((*kind).into()), ..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() {
|
||||
draw_row(
|
||||
app,
|
||||
ui,
|
||||
egui::Id::new(("fc_row_partition", i)),
|
||||
&Row { depth: 2, icon: Some("geometry.partition"), label: n, note: Some(format!("by {datum}")), ..Default::default() },
|
||||
);
|
||||
let row = Row { depth: 2, icon: Some("geometry.partition"), label: n, note: Some(format!("by {datum}")), ..Default::default() };
|
||||
item_row(app, ui, egui::Id::new(("fc_row_partition", i)), row, Target::Def(Def::Partition, n.clone()), acts);
|
||||
}
|
||||
}
|
||||
ghost_rows(app, ui, &stages, "geometry", acts);
|
||||
@@ -808,18 +924,9 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
|
||||
}
|
||||
if open(app, key) {
|
||||
for (kind, name) in items {
|
||||
let selected = app.selected.as_ref() == Some(&(kind, name.clone()));
|
||||
let r = draw_row(
|
||||
app,
|
||||
ui,
|
||||
egui::Id::new(("fc_row_def", kind.stem(), &name)),
|
||||
&Row { depth: 2, icon: Some(kind.icon()), label: &name, selected, ..Default::default() },
|
||||
);
|
||||
if r.double {
|
||||
acts.push(Act::Edit(kind, name));
|
||||
} else if r.clicked {
|
||||
acts.push(Act::Select(kind, name));
|
||||
}
|
||||
let id = egui::Id::new(("fc_row_def", kind.stem(), &name));
|
||||
let row = Row { depth: 2, icon: Some(kind.icon()), label: &name, ..Default::default() };
|
||||
item_row(app, ui, id, row, Target::Def(kind, name.clone()), acts);
|
||||
}
|
||||
}
|
||||
ghost_rows(app, ui, &stages, key, acts);
|
||||
@@ -947,19 +1054,9 @@ mod tests {
|
||||
assert_eq!(steel["name"], "Steel");
|
||||
assert!(steel.get("elastic").is_some(), "{steel}");
|
||||
assert!(definition(m, Def::Load, "nope").is_none());
|
||||
for kind in [
|
||||
Def::Set,
|
||||
Def::Material,
|
||||
Def::Section,
|
||||
Def::Step,
|
||||
Def::BoundaryCondition,
|
||||
Def::Load,
|
||||
Def::ReferencePoint,
|
||||
Def::Coupling,
|
||||
Def::Contact,
|
||||
Def::FluidDomain,
|
||||
] {
|
||||
for verb in ["create", "edit", "delete"] {
|
||||
for kind in Def::ALL {
|
||||
let verbs: &[&str] = if kind == Def::Partition { &["create", "delete"] } else { &["create", "edit", "delete"] };
|
||||
for verb in verbs {
|
||||
assert!(fieldcraft_engine::find_command(&kind.command(verb)).is_some(), "{kind:?} {verb}");
|
||||
}
|
||||
assert!(fieldcraft_engine::catalogue::find(kind.icon()).is_some());
|
||||
|
||||
Reference in new issue
Block a user