diff --git a/crates/ui-egui/src/context.rs b/crates/ui-egui/src/context.rs index 70d028b..60f000c 100644 --- a/crates/ui-egui/src/context.rs +++ b/crates/ui-egui/src/context.rs @@ -164,8 +164,14 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) { selection::Target::Position => "Puts the point at what is selected: a vertex or node, the centre of a hole (its faces or rim), or the centroid of faces and edges", selection::Target::DatumPoint => "Puts the point on what is selected: a vertex, an edge's midpoint, a face's centre, or the centre of a hole or of several items", selection::Target::Bodies => "The selected bodies (Select: Bodies), or the bodies of the selected faces, become the cells", + selection::Target::ExtrudeFaces => "Pick the source face and use it, then pick the target surface and use it", }; - use_selection = ui.add_enabled(selected.is_some(), egui::Button::new("Use selection")).on_hover_text(tip).clicked(); + let button = match target { + selection::Target::ExtrudeFaces if selection::extrude_key(¶ms) == "source" => "Use as source", + selection::Target::ExtrudeFaces => "Use as target surface", + _ => "Use selection", + }; + use_selection = ui.add_enabled(selected.is_some(), egui::Button::new(button)).on_hover_text(tip).clicked(); match &selected { Some(text) => ui.add(egui::Label::new(RichText::new(text).color(t.pick_selected)).wrap()), None => ui.add(egui::Label::new(RichText::new("Pick in the viewport, or choose below").color(t.text_dim)).wrap()), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 0f24ea0..0422cb4 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1723,6 +1723,8 @@ impl ModelApp { } Value::Object(out) } + selection::Target::ExtrudeFaces => selection::with_extrude_faces(¶ms, &self.pick.selection) + .ok_or("no face is selected: pick the source face, then the target surface (Select: Faces)")?, selection::Target::Bodies => { let bodies = selection::bodies(&self.pick.selection).ok_or("no geometry is selected: pick cells with Select: Bodies")?; let mut out = params.as_object().cloned().unwrap_or_default(); diff --git a/crates/ui-egui/src/selection.rs b/crates/ui-egui/src/selection.rs index 7725b6d..328e8e7 100644 --- a/crates/ui-egui/src/selection.rs +++ b/crates/ui-egui/src/selection.rs @@ -852,6 +852,9 @@ pub enum Target { /// The selected bodies (or the bodies of the selected faces, edges and vertices) as the /// `bodies` list (the cells of a partitioned mesh). Bodies, + /// The selected faces as an extrusion's `source` while it has none, then as its + /// `to_surface`: pick the source face, then the target surface. + ExtrudeFaces, } /// Where command `id`'s form takes the selection, if it does. `params` (the form's current @@ -863,6 +866,7 @@ pub fn target(id: &str, params: &Value) -> Option { "model.reference_point.create" | "model.reference_point.edit" => Some(Target::Position), "geometry.datum.point" | "geometry.datum.point.edit" => Some(Target::DatumPoint), "mesh.cells" => Some(Target::Bodies), + "mesh.hex.extrude" => Some(Target::ExtrudeFaces), "model.boundary_condition.create" | "model.boundary_condition.edit" => Some(Target::TargetSet), "model.section.create" | "model.section.edit" | "model.coupling.create" | "model.coupling.edit" | "interaction.rigid_wall" => { Some(Target::Set) @@ -878,8 +882,10 @@ pub fn target(id: &str, params: &Value) -> Option { /// The filter command `id`'s form picks with, when its target is one kind of entity: edges for /// edge seeds, and the vertex, edge or face a datum point is defined on. pub fn form_filter(id: &str, params: &Value) -> Option { - if target(id, params) == Some(Target::Edges) { - return Some(Filter::Edges); + match target(id, params) { + Some(Target::Edges) => return Some(Filter::Edges), + Some(Target::ExtrudeFaces) => return Some(Filter::Faces), + _ => {} } match id { "geometry.datum.point" | "geometry.datum.point.edit" => match params.pointer("/by/type").and_then(Value::as_str) { @@ -923,6 +929,36 @@ pub fn with_edges(params: &Value, selection: &Selection) -> Option { Some(Value::Object(out)) } +/// The key of an extrusion's form the selected faces go into: `source` while it names no face +/// (and no `source_set`), then `to_surface`. +pub fn extrude_key(params: &Value) -> &'static str { + let named = |k: &str| { + params + .get(k) + .is_some_and(|v| v.as_array().is_some_and(|a| a.iter().any(|f| f.get("face").and_then(Value::as_str).is_some_and(|n| !n.is_empty())))) + }; + if named("source") || params.get("source_set").and_then(Value::as_str).is_some_and(|s| !s.is_empty()) { "to_surface" } else { "source" } +} + +/// `params` with the selection's faces as an extrusion's source or target surface +/// ([`extrude_key`]); `None` when no face is selected. +pub fn with_extrude_faces(params: &Value, selection: &Selection) -> Option { + let faces: Vec = selection + .items() + .iter() + .filter_map(|i| match i { + Item::Geometry(g) if matches!(g.entity, Entity::Face(_)) => Some(json!(g)), + _ => None, + }) + .collect(); + if faces.is_empty() { + return None; + } + let mut out = params.as_object().cloned().unwrap_or_default(); + out.insert(extrude_key(params).into(), Value::Array(faces)); + Some(Value::Object(out)) +} + /// `params` with the set `set` as the target. pub fn with_set(params: &Value, target: Target, set: &str) -> Value { let mut out = params.as_object().cloned().unwrap_or_default(); @@ -933,7 +969,7 @@ pub fn with_set(params: &Value, target: Target, set: &str) -> Value { Target::Set => { out.insert("set".into(), json!(set)); } - Target::Members | Target::Edges | Target::Position | Target::DatumPoint | Target::Bodies => {} + Target::Members | Target::Edges | Target::Position | Target::DatumPoint | Target::Bodies | Target::ExtrudeFaces => {} } Value::Object(out) } @@ -1237,6 +1273,26 @@ mod tests { assert_eq!(p.overlay(&sel, None, sel_c, hov_c).tris.len(), s.display.tris.len()); } + /// An extrusion's form takes the source face first, then the target surface (#400). + #[test] + fn an_extrusion_takes_its_source_then_its_target_surface() { + assert_eq!(target("mesh.hex.extrude", &json!({})), Some(Target::ExtrudeFaces)); + assert_eq!(form_filter("mesh.hex.extrude", &json!({})), Some(Filter::Faces)); + let mut s = Selection::default(); + s.apply([Item::Geometry(GeometryRef::face("Post", "F1:-z").unwrap()), Item::Node(4)], Mode::Replace); + let form = with_extrude_faces(&json!({"layers": 4, "source": [{"body": "", "face": ""}]}), &s).unwrap(); + assert_eq!(form, json!({"layers": 4, "source": [{"body": "Post", "face": "F1:-z"}]})); + s.apply([Item::Geometry(GeometryRef::face("Wall", "F7").unwrap())], Mode::Replace); + let form = with_extrude_faces(&form, &s).unwrap(); + assert_eq!(form["to_surface"], json!([{"body": "Wall", "face": "F7"}])); + assert_eq!(form["source"], json!([{"body": "Post", "face": "F1:-z"}]), "the source stays"); + // Picking again replaces the target surface; a source set counts as a source. + assert_eq!(extrude_key(&form), "to_surface"); + assert_eq!(extrude_key(&json!({"source_set": "Section"})), "to_surface"); + s.apply([Item::Node(4)], Mode::Replace); + assert!(with_extrude_faces(&form, &s).is_none(), "no face selected"); + } + #[test] fn selections_add_remove_toggle_and_cap() { let mut s = Selection::default();