forked from 1b-agent/fieldcraft-upstream
ui: Relax Nodes takes picked faces (#471)
Use selection puts the selected design faces in the form as faces, in place of a sphere; Shift/Ctrl-click picks faces while the form is open, and every click does once the form lists faces. 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
3ee622f62a
commit
7d269c58f9
5 files changed
+77
-7
No files matched your search
@@ -1409,8 +1409,8 @@ pub const NODE_RELAX: &[Field] = &[
|
|||||||
.help("Instead of a centre: a sphere round this element (radius default twice its longest edge).")
|
.help("Instead of a centre: a sphere round this element (radius default twice its longest edge).")
|
||||||
.hint("none"),
|
.hint("none"),
|
||||||
face_rows("faces", "Faces")
|
face_rows("faces", "Faces")
|
||||||
.help("Instead of a sphere: every node on these CAD faces and the elements touching them, nodes sliding on the faces. Pick faces in the view, then Use selection.")
|
.help("Instead of a sphere: every node on these CAD faces and the elements touching them, nodes sliding on the faces. Shift-click faces in the view, then Use selection.")
|
||||||
.hint("pick faces"),
|
.hint("Shift-click faces"),
|
||||||
labels("nodes", "Nodes", LabelOf::Node).help("Instead of a sphere: these nodes.").hint("none"),
|
labels("nodes", "Nodes", LabelOf::Node).help("Instead of a sphere: these nodes.").hint("none"),
|
||||||
labels("elements", "Elements", LabelOf::Element).help("Instead of a sphere: the nodes of these elements.").hint("none"),
|
labels("elements", "Elements", LabelOf::Element).help("Instead of a sphere: the nodes of these elements.").hint("none"),
|
||||||
references("bodies", "Bodies", C::Body).help("Only the nodes of these bodies.").hint("every body"),
|
references("bodies", "Bodies", C::Body).help("Only the nodes of these bodies.").hint("every body"),
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||||||
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::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::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",
|
selection::Target::ExtrudeFaces => "Pick the source face and use it, then pick the target surface and use it",
|
||||||
|
selection::Target::Faces => "The selected design faces (Shift-click to pick, Select: Faces) are relaxed instead of a sphere",
|
||||||
};
|
};
|
||||||
let button = match target {
|
let button = match target {
|
||||||
selection::Target::ExtrudeFaces if selection::extrude_key(¶ms) == "source" => "Use as source",
|
selection::Target::ExtrudeFaces if selection::extrude_key(¶ms) == "source" => "Use as source",
|
||||||
|
|||||||
@@ -1252,6 +1252,16 @@ impl ModelApp {
|
|||||||
self.dialog.as_ref().is_some_and(|d| d.command == "mesh.node.relax")
|
self.dialog.as_ref().is_some_and(|d| d.command == "mesh.node.relax")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the open Relax Nodes form relaxes listed faces: clicks in the view then pick faces
|
||||||
|
/// instead of placing a sphere.
|
||||||
|
pub fn relax_by_faces(&self) -> bool {
|
||||||
|
self.dialog
|
||||||
|
.as_ref()
|
||||||
|
.filter(|d| d.command == "mesh.node.relax")
|
||||||
|
.and_then(|d| serde_json::from_str::<Value>(&d.text).ok())
|
||||||
|
.is_some_and(|p| p.get("faces").and_then(Value::as_array).is_some_and(|f| !f.is_empty()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Put the open Relax Nodes form's sphere at `at` (a click on the mesh); a sphere without a
|
/// Put the open Relax Nodes form's sphere at `at` (a click on the mesh); a sphere without a
|
||||||
/// radius yet gets a twentieth of the mesh's size.
|
/// radius yet gets a twentieth of the mesh's size.
|
||||||
pub fn relax_place(&mut self, at: Vec3) -> bool {
|
pub fn relax_place(&mut self, at: Vec3) -> bool {
|
||||||
@@ -1264,8 +1274,9 @@ impl ModelApp {
|
|||||||
let mut params = serde_json::from_str::<Value>(&d.text).ok().filter(Value::is_object).unwrap_or_else(|| json!({}));
|
let mut params = serde_json::from_str::<Value>(&d.text).ok().filter(Value::is_object).unwrap_or_else(|| json!({}));
|
||||||
params["center"] = json!([at.x, at.y, at.z]);
|
params["center"] = json!([at.x, at.y, at.z]);
|
||||||
if let Some(o) = params.as_object_mut() {
|
if let Some(o) = params.as_object_mut() {
|
||||||
o.remove("around_element");
|
for key in ["around_element", "nodes", "elements", "faces"] {
|
||||||
o.remove("nodes");
|
o.remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if params.get("radius").and_then(Value::as_f64).is_none_or(|r| !(r > 0.0)) && size > 0.0 && size.is_finite() {
|
if params.get("radius").and_then(Value::as_f64).is_none_or(|r| !(r > 0.0)) && size > 0.0 && size.is_finite() {
|
||||||
params["radius"] = json!(size / 20.0);
|
params["radius"] = json!(size / 20.0);
|
||||||
@@ -1806,6 +1817,9 @@ impl ModelApp {
|
|||||||
}
|
}
|
||||||
selection::Target::ExtrudeFaces => selection::with_extrude_faces(¶ms, &self.pick.selection)
|
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)")?,
|
.ok_or("no face is selected: pick the source face, then the target surface (Select: Faces)")?,
|
||||||
|
selection::Target::Faces => {
|
||||||
|
selection::with_faces(¶ms, &self.pick.selection).ok_or("no face is selected: pick the faces to relax (Select: Faces)")?
|
||||||
|
}
|
||||||
selection::Target::Bodies => {
|
selection::Target::Bodies => {
|
||||||
let bodies = selection::bodies(&self.pick.selection).ok_or("no geometry is selected: pick cells with Select: 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();
|
let mut out = params.as_object().cloned().unwrap_or_default();
|
||||||
@@ -2511,6 +2525,33 @@ mod tests {
|
|||||||
assert_ne!(app.session.model.node(n).unwrap().xyz, [2.7, 2.6, 2.5]);
|
assert_ne!(app.session.model.node(n).unwrap().xyz, [2.7, 2.6, 2.5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Relax Nodes by face (#471): Use selection puts the picked design faces in the form in
|
||||||
|
/// place of a sphere, clicks then pick faces, and OK relaxes them.
|
||||||
|
#[test]
|
||||||
|
fn relax_nodes_takes_picked_faces() {
|
||||||
|
let mut app = ModelApp::new(Session::default());
|
||||||
|
let dir = std::env::temp_dir().join(format!("fieldcraft-relax-faces-{}", 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();
|
||||||
|
app.run("mesh.tet.generate", json!({"size": 4.0})).unwrap();
|
||||||
|
let body = app.session.design.as_ref().unwrap().bodies[0].clone();
|
||||||
|
let face = body.faces()[0].name.clone();
|
||||||
|
app.start("mesh.node.relax").unwrap();
|
||||||
|
assert!(app.relax_place(Vec3 { x: 1.0, y: 1.0, z: 1.0 }) && !app.relax_by_faces());
|
||||||
|
let picked = fieldcraft_model::geometry::GeometryRef::face(body.name.clone(), face.clone()).unwrap();
|
||||||
|
app.select([selection::Item::Geometry(picked)], selection::Mode::Replace);
|
||||||
|
app.use_selection_in_dialog().unwrap();
|
||||||
|
let params: Value = serde_json::from_str(&app.dialog.as_ref().unwrap().text).unwrap();
|
||||||
|
assert_eq!(params["faces"], json!([{"body": body.name, "face": face}]), "{params}");
|
||||||
|
assert!(params.get("center").is_none() && params.get("radius").is_none(), "{params}");
|
||||||
|
assert!(app.relax_by_faces() && app.relax_preview().is_some());
|
||||||
|
app.dialog = None;
|
||||||
|
app.run("mesh.node.relax", params).unwrap();
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
/// Mesh Quality reads as a sentence in the status bar, not as its JSON report.
|
/// Mesh Quality reads as a sentence in the status bar, not as its JSON report.
|
||||||
#[test]
|
#[test]
|
||||||
fn mesh_quality_reports_in_a_line() {
|
fn mesh_quality_reports_in_a_line() {
|
||||||
|
|||||||
@@ -855,6 +855,9 @@ pub enum Target {
|
|||||||
/// The selected faces as an extrusion's `source` while it has none, then as its
|
/// 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.
|
/// `to_surface`: pick the source face, then the target surface.
|
||||||
ExtrudeFaces,
|
ExtrudeFaces,
|
||||||
|
/// The selected geometry faces as `faces: [{body, face}]`, in place of a sphere or labels
|
||||||
|
/// (the faces Relax Nodes relaxes).
|
||||||
|
Faces,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where command `id`'s form takes the selection, if it does. `params` (the form's current
|
/// Where command `id`'s form takes the selection, if it does. `params` (the form's current
|
||||||
@@ -867,6 +870,7 @@ pub fn target(id: &str, params: &Value) -> Option<Target> {
|
|||||||
"geometry.datum.point" | "geometry.datum.point.edit" => Some(Target::DatumPoint),
|
"geometry.datum.point" | "geometry.datum.point.edit" => Some(Target::DatumPoint),
|
||||||
"mesh.cells" => Some(Target::Bodies),
|
"mesh.cells" => Some(Target::Bodies),
|
||||||
"mesh.hex.extrude" => Some(Target::ExtrudeFaces),
|
"mesh.hex.extrude" => Some(Target::ExtrudeFaces),
|
||||||
|
"mesh.node.relax" => Some(Target::Faces),
|
||||||
"model.boundary_condition.create" | "model.boundary_condition.edit" => Some(Target::TargetSet),
|
"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" => {
|
"model.section.create" | "model.section.edit" | "model.coupling.create" | "model.coupling.edit" | "interaction.rigid_wall" => {
|
||||||
Some(Target::Set)
|
Some(Target::Set)
|
||||||
@@ -929,6 +933,28 @@ pub fn with_edges(params: &Value, selection: &Selection) -> Option<Value> {
|
|||||||
Some(Value::Object(out))
|
Some(Value::Object(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `params` with the selection's geometry faces as `faces`, and no sphere, nodes or elements
|
||||||
|
/// (what Relax Nodes relaxes); `None` when no face is selected.
|
||||||
|
pub fn with_faces(params: &Value, selection: &Selection) -> Option<Value> {
|
||||||
|
let faces: Vec<Value> = 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();
|
||||||
|
for key in ["center", "radius", "around_element", "nodes", "elements"] {
|
||||||
|
out.remove(key);
|
||||||
|
}
|
||||||
|
out.insert("faces".into(), Value::Array(faces));
|
||||||
|
Some(Value::Object(out))
|
||||||
|
}
|
||||||
|
|
||||||
/// The key of an extrusion's form the selected faces go into: `source` while it names no face
|
/// 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`.
|
/// (and no `source_set`), then `to_surface`.
|
||||||
pub fn extrude_key(params: &Value) -> &'static str {
|
pub fn extrude_key(params: &Value) -> &'static str {
|
||||||
@@ -969,7 +995,7 @@ pub fn with_set(params: &Value, target: Target, set: &str) -> Value {
|
|||||||
Target::Set => {
|
Target::Set => {
|
||||||
out.insert("set".into(), json!(set));
|
out.insert("set".into(), json!(set));
|
||||||
}
|
}
|
||||||
Target::Members | Target::Edges | Target::Position | Target::DatumPoint | Target::Bodies | Target::ExtrudeFaces => {}
|
Target::Members | Target::Edges | Target::Position | Target::DatumPoint | Target::Bodies | Target::ExtrudeFaces | Target::Faces => {}
|
||||||
}
|
}
|
||||||
Value::Object(out)
|
Value::Object(out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ pub fn show(app: &mut ModelApp, ui: &mut egui::Ui) {
|
|||||||
if response.clicked()
|
if response.clicked()
|
||||||
&& let Some(at) = response.interact_pointer_pos()
|
&& let Some(at) = response.interact_pointer_pos()
|
||||||
{
|
{
|
||||||
// With Relax Nodes open a click on the mesh places its sphere (#398).
|
// With Relax Nodes open a plain click on the mesh places its sphere (#398); Shift or
|
||||||
let placed = app.relaxing() && app.surface_point(at).is_some_and(|p| app.relax_place(p));
|
// Ctrl picks faces to relax (#471), and so does every click once the form lists faces.
|
||||||
|
let plain = mode(ui.input(|i| i.modifiers)) == Mode::Replace;
|
||||||
|
let placed = app.relaxing() && plain && !app.relax_by_faces() && app.surface_point(at).is_some_and(|p| app.relax_place(p));
|
||||||
if !placed {
|
if !placed {
|
||||||
let _ = app.select_at(at, mode(ui.input(|i| i.modifiers)));
|
let _ = app.select_at(at, mode(ui.input(|i| i.modifiers)));
|
||||||
probe_at(app, at, rect);
|
probe_at(app, at, rect);
|
||||||
|
|||||||
Reference in new issue
Block a user