forked from 1b-agent/fieldcraft-upstream
ui: a selected set is highlighted in the viewport; seeds are selected, described, edited and deleted from the browser (#336, #352)
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
a53c79cb55
commit
de8cdd0b03
7 files changed
+226
-50
No files matched your search
@@ -549,17 +549,16 @@ fn item_row(app: &mut ModelApp, ui: &mut egui::Ui, id: egui::Id, mut row: Row, t
|
||||
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);
|
||||
match &target {
|
||||
Target::Def(kind, name) => row.selected = app.selected.as_ref().is_some_and(|(k, n)| k == kind && n == name),
|
||||
Target::Seed(edge) => row.selected = app.selected_seed.as_ref() == Some(edge),
|
||||
Target::Body(_) | Target::Surface(_) => {}
|
||||
}
|
||||
let r = draw_row(app, ui, id, &row);
|
||||
// A right click selects the row its menu acts on (another item's highlight would say the
|
||||
// menu is about that one).
|
||||
if r.response.as_ref().is_some_and(egui::Response::secondary_clicked) {
|
||||
acts.push(Act::Focus(match &target {
|
||||
Target::Def(kind, name) => Some((*kind, name.clone())),
|
||||
_ => None,
|
||||
}));
|
||||
acts.push(Act::Focus(target.clone()));
|
||||
}
|
||||
if let Some(resp) = &r.response {
|
||||
resp.context_menu(|ui| {
|
||||
@@ -570,11 +569,13 @@ fn item_row(app: &mut ModelApp, ui: &mut egui::Ui, id: egui::Id, mut row: Row, t
|
||||
}
|
||||
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));
|
||||
} else if r.double && matches!(target, Target::Def(..) | Target::Seed(_)) {
|
||||
acts.push(Act::Menu(target, Action::Edit));
|
||||
} else if r.clicked {
|
||||
match target {
|
||||
Target::Def(kind, name) => acts.push(Act::Select(kind, name)),
|
||||
Target::Seed(edge) => acts.push(Act::SelectSeed(edge)),
|
||||
Target::Body(_) | Target::Surface(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -639,9 +640,10 @@ fn draw_ghost(app: &mut ModelApp, ui: &mut egui::Ui, s: &StageState) -> bool {
|
||||
enum Act {
|
||||
Fold(String),
|
||||
Select(Def, String),
|
||||
/// Select this definition (nothing for an item that is none), as a right click does.
|
||||
Focus(Option<(Def, String)>),
|
||||
Edit(Def, String),
|
||||
/// Select (or deselect) the seed on this edge.
|
||||
SelectSeed(fieldcraft_model::GeometryRef),
|
||||
/// Select the item a right click opened its menu on (nothing for an item that cannot be).
|
||||
Focus(Target),
|
||||
ShowResult(String),
|
||||
/// A ghost row's command.
|
||||
Start(&'static str),
|
||||
@@ -688,15 +690,21 @@ pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
Act::Select(kind, name) => {
|
||||
app.selected = if app.selected.as_ref() == Some(&(kind, name.clone())) { None } else { Some((kind, name)) };
|
||||
let again = app.selected.as_ref() == Some(&(kind, name.clone()));
|
||||
app.select_definition((!again).then_some((kind, name)));
|
||||
}
|
||||
Act::Focus(selected) => app.selected = selected,
|
||||
Act::Edit(kind, name) => {
|
||||
if let Some((command, params)) = kind.edit(&app.session.model, &name) {
|
||||
app.selected = Some((kind, name));
|
||||
app.open_form(&command, params);
|
||||
Act::SelectSeed(edge) => {
|
||||
if app.selected_seed.as_ref() == Some(&edge) {
|
||||
app.selected_seed = None;
|
||||
} else {
|
||||
app.select_seed(edge);
|
||||
}
|
||||
}
|
||||
Act::Focus(target) => match target {
|
||||
Target::Def(kind, name) => app.select_definition(Some((kind, name))),
|
||||
Target::Seed(edge) => app.select_seed(edge),
|
||||
Target::Body(_) | Target::Surface(_) => app.select_definition(None),
|
||||
},
|
||||
Act::Menu(target, action) => menu::apply(app, target, action),
|
||||
Act::ShowResult(step) => {
|
||||
// Errors show in the status bar.
|
||||
@@ -856,7 +864,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
|
||||
&Row { depth: 2, swatch: Some(t.element(kind)), label: kind.name(), note: Some(format!("{n}")), ..Default::default() },
|
||||
);
|
||||
}
|
||||
// Edge seeds: a click selects the seeded edge in the viewport.
|
||||
// Edge seeds: a click selects the seed (its properties on the right) and its edge in the
|
||||
// viewport; a double click or the menu edits or deletes it.
|
||||
let seeds: Vec<(fieldcraft_model::GeometryRef, String)> = app.session.model.seeds().iter().map(|s| (s.edge.clone(), seed_note(s))).collect();
|
||||
if !seeds.is_empty() {
|
||||
let r = draw_row(
|
||||
@@ -878,15 +887,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
|
||||
if open(app, "seeds") {
|
||||
for (i, (edge, note)) in seeds.into_iter().enumerate() {
|
||||
let label = format!("{} · {}", edge.body, edge.entity.name().unwrap_or_default());
|
||||
let r = draw_row(
|
||||
app,
|
||||
ui,
|
||||
egui::Id::new(("fc_row_seed", i)),
|
||||
&Row { depth: 3, label: &label, note: Some(note), ..Default::default() },
|
||||
);
|
||||
if r.clicked {
|
||||
app.select([crate::selection::Item::Geometry(edge)], crate::selection::Mode::Replace);
|
||||
}
|
||||
let row = Row { depth: 3, icon: Some("mesh.seed.edge"), label: &label, note: Some(note), ..Default::default() };
|
||||
item_row(app, ui, egui::Id::new(("fc_row_seed", i)), row, Target::Seed(edge), acts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub const WIDTH: f32 = 310.0;
|
||||
|
||||
/// Whether the panel has anything to show.
|
||||
pub fn has_context(app: &mut ModelApp) -> bool {
|
||||
app.dialog.is_some() || app.selected.is_some() || app.reply.is_some() || app.plot().is_some()
|
||||
app.dialog.is_some() || app.selected.is_some() || app.selected_seed.is_some() || app.reply.is_some() || app.plot().is_some()
|
||||
}
|
||||
|
||||
pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
@@ -41,6 +41,10 @@ pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
properties(app, ui, &t, kind, &name);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
if let Some(edge) = app.selected_seed.clone() {
|
||||
seed_card(app, ui, &t, edge);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
if let Some(plot) = app.plot() {
|
||||
let mut close = false;
|
||||
heading(ui, &t, Level::Title, |ui| {
|
||||
@@ -252,13 +256,7 @@ fn properties(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, kind: Def, name
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
egui::Grid::new("fc_properties").num_columns(2).spacing(vec2(10.0, 3.0)).show(ui, |ui| {
|
||||
for (k, text) in rows.iter() {
|
||||
ui.label(RichText::new(k).color(t.text_dim));
|
||||
ui.add(egui::Label::new(text).wrap());
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
property_grid(ui, t, "fc_properties", &rows);
|
||||
ui.add_space(6.0);
|
||||
let (mut edit, mut delete) = (false, false);
|
||||
let editing = kind.edit_command(&app.session.model, name);
|
||||
@@ -269,7 +267,7 @@ fn properties(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, kind: Def, name
|
||||
delete = ui.button("Delete…").on_hover_text(format!("{} (Delete key)", kind.command("delete"))).clicked();
|
||||
});
|
||||
if deselect {
|
||||
app.selected = None;
|
||||
app.select_definition(None);
|
||||
} else if edit && let Some((command, params)) = kind.edit(&app.session.model, name) {
|
||||
app.open_form(&command, params);
|
||||
} else if delete {
|
||||
@@ -277,6 +275,47 @@ fn properties(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, kind: Def, name
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected seed: its edge, divisions, bias and constraint, with Edit (the Seed Edges form,
|
||||
/// filled in) and Delete (`mesh.seed.clear`).
|
||||
fn seed_card(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, edge: fieldcraft_model::GeometryRef) {
|
||||
let Some(seed) = app.seed(&edge) else { return };
|
||||
let rows = crate::describe::seed_rows(&app.session.model, seed);
|
||||
let mut deselect = false;
|
||||
heading(ui, t, Level::Title, |ui| {
|
||||
ui.label(RichText::new("SEED").font(label_font(ui.ctx(), 12.0)));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
deselect = ui.small_button("×").on_hover_text("Deselect").clicked();
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
property_grid(ui, t, "fc_seed_properties", &rows);
|
||||
ui.add_space(6.0);
|
||||
let (mut edit, mut delete) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
edit = ui.button("Edit…").on_hover_text("mesh.seed.edge").clicked();
|
||||
delete = ui.button("Delete").on_hover_text("mesh.seed.clear (undo brings it back)").clicked();
|
||||
});
|
||||
let target = crate::menu::Target::Seed(edge);
|
||||
if deselect {
|
||||
app.selected_seed = None;
|
||||
} else if edit {
|
||||
crate::menu::apply(app, target, crate::menu::Action::Edit);
|
||||
} else if delete {
|
||||
crate::menu::apply(app, target, crate::menu::Action::Delete);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rows of (label, text) as a two-column grid.
|
||||
fn property_grid(ui: &mut egui::Ui, t: &Tokens, id: &str, rows: &[(String, String)]) {
|
||||
egui::Grid::new(id).num_columns(2).spacing(vec2(10.0, 3.0)).show(ui, |ui| {
|
||||
for (k, text) in rows {
|
||||
ui.label(RichText::new(k).color(t.text_dim));
|
||||
ui.add(egui::Label::new(text).wrap());
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A field's value as short text: numbers and strings as they are, long lists as their length
|
||||
/// and first items, other lists and objects as compact JSON (long ones shortened).
|
||||
pub(crate) fn value_text(v: &Value) -> String {
|
||||
|
||||
@@ -202,7 +202,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
"probe": app.probe,
|
||||
"busy": app.busy(),
|
||||
"solve": solve_json(app),
|
||||
"selected": app.selected.as_ref().map(|(k, n)| json!({"kind": k, "name": n})),
|
||||
"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),
|
||||
"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>>(),
|
||||
|
||||
@@ -68,6 +68,18 @@ pub fn rows(model: &Model, kind: Def, name: &str) -> Option<Vec<(String, String)
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The seed on `edge` as (label, text) rows: the edge, then `mesh.seed.edge`'s fields.
|
||||
pub fn seed_rows(model: &Model, seed: &fieldcraft_model::Seed) -> Vec<(String, String)> {
|
||||
let mut out = vec![("Edge".to_string(), format!("{} · {}", seed.edge.body, seed.edge.entity.name().unwrap_or_default()))];
|
||||
let params = crate::menu::seed_params(seed).unwrap_or_default();
|
||||
if let (Some(fields), Some(obj)) = (fieldcraft_engine::find_command("mesh.seed.edge").and_then(|s| s.form), params.as_object()) {
|
||||
let mut obj = obj.clone();
|
||||
obj.remove("edges");
|
||||
object(fields, &obj, model.units(), &mut out, &mut Vec::new(), 0);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// "contact_pressure" → "Contact pressure".
|
||||
fn sentence(key: &str) -> String {
|
||||
let s = key.replace('_', " ");
|
||||
|
||||
@@ -421,6 +421,9 @@ pub struct ModelApp {
|
||||
pub tree_open: std::collections::BTreeSet<String>,
|
||||
/// The definition selected in the browser: (kind, name), its properties on the right.
|
||||
pub selected: Option<(browser::Def, String)>,
|
||||
/// The edge seed selected in the browser (by its edge), its properties on the right; never
|
||||
/// together with [`ModelApp::selected`].
|
||||
pub selected_seed: Option<fieldcraft_model::GeometryRef>,
|
||||
/// What is picked in the viewport.
|
||||
pub pick: PickState,
|
||||
/// Set while [`ModelApp::capture`] draws the window offscreen: no pointer there is the
|
||||
@@ -501,6 +504,7 @@ impl ModelApp {
|
||||
command_line: String::new(),
|
||||
tree_open: ["mesh", "results"].into_iter().map(str::to_string).collect(),
|
||||
selected: None,
|
||||
selected_seed: None,
|
||||
pick: PickState::default(),
|
||||
capturing: false,
|
||||
nav: NavMode::default(),
|
||||
@@ -1206,7 +1210,8 @@ impl ModelApp {
|
||||
h.finish()
|
||||
};
|
||||
let scene = self.scene_key();
|
||||
let selected = hash(&|h| (scene, self.pick.generation).hash(h));
|
||||
let set = self.selected_set();
|
||||
let selected = hash(&|h| (scene, self.pick.generation, &set).hash(h));
|
||||
let key = hash(&|h| (selected, self.pick.hover_generation).hash(h));
|
||||
if self.pick.overlay.0 == Some(key) {
|
||||
return (key, self.pick.overlay.1.clone());
|
||||
@@ -1215,8 +1220,16 @@ impl ModelApp {
|
||||
// The selection's part is kept while only the hover changes.
|
||||
if self.pick.selected_overlay.0 != Some(selected) {
|
||||
let sel = std::mem::take(&mut self.pick.selection);
|
||||
let s = self.with_picker(|p| p.overlay(&sel, None, theme::rgb(t.pick_selected), theme::rgb(t.pick_hover)));
|
||||
let mut s = self.with_picker(|p| p.overlay(&sel, None, theme::rgb(t.pick_selected), theme::rgb(t.pick_hover)));
|
||||
self.pick.selection = sel;
|
||||
// The set selected in the browser: its members in the highlight of selected symbols.
|
||||
if let Some(items) = set.as_ref().and_then(|name| self.set_items(name)) {
|
||||
let highlight = t.glyph_colors().highlight;
|
||||
let m = self.with_picker(|p| p.items_overlay(&items, highlight));
|
||||
s.tris.extend(m.tris);
|
||||
s.lines.extend(m.lines);
|
||||
s.points.extend(m.points);
|
||||
}
|
||||
self.pick.selected_overlay = (Some(selected), Arc::new(s));
|
||||
}
|
||||
let mut s = (*self.pick.selected_overlay.1).clone();
|
||||
@@ -1232,6 +1245,44 @@ impl ModelApp {
|
||||
(key, self.pick.overlay.1.clone())
|
||||
}
|
||||
|
||||
/// The seed on `edge`, if it has one.
|
||||
pub fn seed(&self, edge: &fieldcraft_model::GeometryRef) -> Option<&fieldcraft_model::Seed> {
|
||||
self.session.model.seeds().iter().find(|s| s.edge == *edge)
|
||||
}
|
||||
|
||||
/// Select (or with `None` deselect) a definition in the browser, as a click on its row does.
|
||||
pub fn select_definition(&mut self, selected: Option<(browser::Def, String)>) {
|
||||
if selected.is_some() {
|
||||
self.selected_seed = None;
|
||||
}
|
||||
self.selected = selected;
|
||||
}
|
||||
|
||||
/// Select the seed on `edge` in the browser: its properties on the right, its edge picked in
|
||||
/// the viewport.
|
||||
pub fn select_seed(&mut self, edge: fieldcraft_model::GeometryRef) {
|
||||
self.selected = None;
|
||||
self.select([selection::Item::Geometry(edge.clone())], selection::Mode::Replace);
|
||||
self.selected_seed = Some(edge);
|
||||
}
|
||||
|
||||
/// The set selected in the browser, if a set is.
|
||||
fn selected_set(&self) -> Option<String> {
|
||||
match &self.selected {
|
||||
Some((browser::Def::Set, name)) => Some(name.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set `name`'s members as viewport items (design entities by name, mesh items by label).
|
||||
fn set_items(&self, name: &str) -> Option<Vec<selection::Item>> {
|
||||
let set = self.session.model.sets().get(name)?;
|
||||
let items = set.geometry.iter().cloned().map(selection::Item::Geometry);
|
||||
let items = items.chain(set.nodes.iter().map(|n| selection::Item::Node(*n)));
|
||||
let items = items.chain(set.elements.iter().map(|e| selection::Item::Element(*e)));
|
||||
Some(items.chain(set.faces.iter().map(|f| selection::Item::Face(*f))).collect())
|
||||
}
|
||||
|
||||
/// Drop selected nodes, elements and element faces the model no longer has (after an edit,
|
||||
/// an undo or a remesh); geometry stays selected by name.
|
||||
fn check_selection(&mut self) {
|
||||
@@ -1351,6 +1402,7 @@ impl ModelApp {
|
||||
if self.framed_document != Some(self.session.opened()) {
|
||||
if self.framed_document.is_some() {
|
||||
self.selected = None;
|
||||
self.selected_seed = None;
|
||||
self.reply = None;
|
||||
}
|
||||
self.framed_document = Some(self.session.opened());
|
||||
@@ -1419,6 +1471,9 @@ impl ModelApp {
|
||||
if self.selected.as_ref().is_some_and(|(kind, name)| !kind.exists(&self.session.model, name)) {
|
||||
self.selected = None;
|
||||
}
|
||||
if self.selected_seed.as_ref().is_some_and(|edge| self.seed(edge).is_none()) {
|
||||
self.selected_seed = None;
|
||||
}
|
||||
if self.session.results_view.as_ref().is_some_and(|v| v.animate) {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(results::PERIOD / results::FRAMES as f64));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! The item menu: what a right click on a browser row offers, and on a datum or a partition's cut
|
||||
//! face in the viewport. Edit… opens the item's edit form, Hide/Show and Isolate change the view
|
||||
//! (`view.*`), Active in step switches a boundary condition or load on or off per step
|
||||
//! (`model.step.activate`), and Delete… asks first ([`crate::files::Prompt::Delete`]): it lists
|
||||
//! (`model.step.activate`), and Delete… asks first (a seed's Delete clears it at once: undo brings
|
||||
//! it back) ([`crate::files::Prompt::Delete`]): it lists
|
||||
//! the definitions that use the item, and refuses, or for a datum deletes its partitions with it.
|
||||
//! Every entry runs the same command as the ribbon, the forms and scripts.
|
||||
|
||||
@@ -20,6 +21,8 @@ pub enum Target {
|
||||
Surface(String),
|
||||
/// A model definition, datum or partition.
|
||||
Def(Def, String),
|
||||
/// The seed on a design edge.
|
||||
Seed(fieldcraft_model::GeometryRef),
|
||||
}
|
||||
|
||||
impl Target {
|
||||
@@ -29,6 +32,7 @@ impl Target {
|
||||
Target::Body(n) => (ItemKind::Body, n),
|
||||
Target::Surface(n) => (ItemKind::Surface, n),
|
||||
Target::Def(def, n) => (def.item_kind()?, n),
|
||||
Target::Seed(_) => return None,
|
||||
};
|
||||
Some(ViewItem { kind, name: name.clone() })
|
||||
}
|
||||
@@ -73,6 +77,9 @@ pub fn show(app: &ModelApp, ui: &mut egui::Ui, target: &Target) -> Option<Action
|
||||
let tip = edit.clone().unwrap_or_else(|| "Not editable: delete it and make it again".to_string());
|
||||
entry(ui, edit.is_some(), "Edit…", &tip, Action::Edit);
|
||||
}
|
||||
if let Target::Seed(_) = target {
|
||||
entry(ui, true, "Edit…", "mesh.seed.edge", Action::Edit);
|
||||
}
|
||||
if let Some(item) = target.item() {
|
||||
if app.session.shows(item.kind, &item.name) {
|
||||
entry(ui, true, "Hide", "view.hide", Action::Hide);
|
||||
@@ -103,6 +110,12 @@ pub fn show(app: &ModelApp, ui: &mut egui::Ui, target: &Target) -> Option<Action
|
||||
chosen = Some(Action::Delete);
|
||||
}
|
||||
}
|
||||
if let Target::Seed(_) = target {
|
||||
ui.separator();
|
||||
if ui.button("Delete").on_hover_text("mesh.seed.clear (undo brings it back)").clicked() {
|
||||
chosen = Some(Action::Delete);
|
||||
}
|
||||
}
|
||||
if chosen.is_some() {
|
||||
ui.close();
|
||||
}
|
||||
@@ -115,12 +128,18 @@ pub fn apply(app: &mut ModelApp, target: Target, action: Action) {
|
||||
// Errors show in the status bar.
|
||||
let _ = match action {
|
||||
Action::Edit => {
|
||||
if let Target::Def(def, name) = target
|
||||
&& let Some((command, params)) = def.edit(&app.session.model, &name)
|
||||
if let Target::Def(def, name) = &target
|
||||
&& let Some((command, params)) = def.edit(&app.session.model, name)
|
||||
{
|
||||
app.selected = Some((def, name));
|
||||
app.select_definition(Some((*def, name.clone())));
|
||||
app.open_form(&command, params);
|
||||
}
|
||||
if let Target::Seed(edge) = target
|
||||
&& let Some(params) = app.seed(&edge).and_then(seed_params)
|
||||
{
|
||||
app.select_seed(edge);
|
||||
app.open_form("mesh.seed.edge", params);
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Action::Hide => app.run("view.hide", items(&target)),
|
||||
@@ -134,15 +153,26 @@ pub fn apply(app: &mut ModelApp, target: Target, action: Action) {
|
||||
}
|
||||
_ => Ok(Value::Null),
|
||||
},
|
||||
Action::Delete => {
|
||||
if let Target::Def(def, name) = target {
|
||||
Action::Delete => match target {
|
||||
Target::Def(def, name) => {
|
||||
app.ask_delete(def, &name);
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Target::Seed(edge) => app.run("mesh.seed.clear", json!({"edges": [edge]})),
|
||||
Target::Body(_) | Target::Surface(_) => Ok(Value::Null),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// `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()?;
|
||||
let o = v.as_object_mut()?;
|
||||
let edge = o.remove("edge")?;
|
||||
o.insert("edges".into(), json!([edge]));
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// The question a deletion asks ([`crate::files::Prompt::Delete`]).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DeleteAsk {
|
||||
@@ -281,6 +311,37 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A seed is selected, described, edited and deleted from the browser like a definition
|
||||
/// (#352).
|
||||
#[test]
|
||||
fn seeds_are_selected_edited_and_deleted() {
|
||||
let (mut app, dir) = plate();
|
||||
let bodies = app.run("geometry.bodies", json!({})).unwrap();
|
||||
let edge = json!({"body": bodies[0]["name"], "edge": bodies[0]["edges"][0]["name"]});
|
||||
app.run("mesh.seed.edge", json!({"edges": [edge], "count": 6, "bias": {"ratio": 4, "toward": "end"}})).unwrap();
|
||||
let seed = app.session.model.seeds()[0].clone();
|
||||
app.select_seed(seed.edge.clone());
|
||||
assert!(app.selected.is_none() && crate::context::has_context(&mut app));
|
||||
assert_eq!(app.pick.selection.len(), 1, "its edge is picked");
|
||||
let rows = crate::describe::seed_rows(&app.session.model, &seed);
|
||||
let text: Vec<String> = rows.iter().map(|(k, v)| format!("{k}: {v}")).collect();
|
||||
assert!(text.iter().any(|r| r.contains('6')) && text.iter().any(|r| r.contains('4')), "{text:?}");
|
||||
assert!(!text.iter().any(|r| r.contains('{')), "{text:?}");
|
||||
apply(&mut app, Target::Seed(seed.edge.clone()), Action::Edit);
|
||||
let d = app.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.command, "mesh.seed.edge");
|
||||
let params: Value = serde_json::from_str(&d.text).unwrap();
|
||||
assert_eq!((params["count"].clone(), params["edges"][0]["body"].clone()), (json!(6), bodies[0]["name"].clone()), "{params}");
|
||||
app.close_dialog();
|
||||
apply(&mut app, Target::Seed(seed.edge.clone()), Action::Delete);
|
||||
assert!(app.session.model.seeds().is_empty());
|
||||
let ctx = egui::Context::default();
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| app.logic(ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
assert!(app.selected_seed.is_none(), "a deleted seed is no longer selected");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Delete asks first: a datum a partition uses names it and deletes both on confirmation;
|
||||
/// a definition in use elsewhere is refused.
|
||||
#[test]
|
||||
|
||||
@@ -596,6 +596,13 @@ impl Picker<'_> {
|
||||
s
|
||||
}
|
||||
|
||||
/// `items` (a set's members) drawn over the scene in `color`.
|
||||
pub fn items_overlay(&self, items: &[Item], color: Rgb) -> Scene {
|
||||
let mut s = Scene::default();
|
||||
self.mark(&Marks::of(self, items.iter()), color, &mut s);
|
||||
s
|
||||
}
|
||||
|
||||
/// The hovered item drawn over the scene in `color`. With `fill` off (while result contours
|
||||
/// are shown) its faces are left out and only their outline, edges and nodes are drawn, so
|
||||
/// the hover never hides the colours under the pointer.
|
||||
|
||||
Reference in new issue
Block a user