ui: draw size controls in the viewport, pick their faces (#435)

Faces and bodies under a size control are tinted, a box drawn as its
edges and a sphere as a translucent ball, each labelled with its name
and size. The Local Size form takes the selected faces.

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-16 17:08:27 -04:00
1 parent b62be1ada6
commit 030799dfc8
4 files changed
+73 -4

No files matched your search

+1 -1
View File
@@ -165,7 +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::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::Faces => "The selected design faces (Shift-click to pick, Select: Faces) are relaxed instead of a sphere",
selection::Target::Faces => "The selected design faces (Shift-click to pick, Select: Faces) become the form's faces, in place of a sphere",
};
let button = match target {
selection::Target::ExtrudeFaces if selection::extrude_key(&params) == "source" => "Use as source",
+2 -1
View File
@@ -1153,6 +1153,7 @@ impl ModelApp {
datums.union(scene::datum_preview(&mut s, d, &self.display.1.bounds, &t));
}
scene::seeds(&mut s, &self.session.model, &self.pick.names, &self.display.1, &t);
scene::sizes(&mut s, &self.session.model, &self.pick.names, &self.display.1, &t);
}
self.scene = (Some(key), s, datums);
}
@@ -1822,7 +1823,7 @@ impl ModelApp {
selection::Target::ExtrudeFaces => selection::with_extrude_faces(&params, &self.pick.selection)
.ok_or("no face is selected: pick the source face, then the target surface (Select: Faces)")?,
selection::Target::Faces => {
selection::with_faces(&params, &self.pick.selection).ok_or("no face is selected: pick the faces to relax (Select: Faces)")?
selection::with_faces(&params, &self.pick.selection).ok_or("no face is selected: pick the faces (Select: Faces)")?
}
selection::Target::Bodies => {
let bodies = selection::bodies(&self.pick.selection).ok_or("no geometry is selected: pick cells with Select: Bodies")?;
+68
View File
@@ -137,6 +137,57 @@ pub fn seeds(s: &mut Scene, model: &fieldcraft_model::Model, names: &[crate::sel
}
}
/// Draw the model's size controls (mesh.size.set) where they apply: the faces and bodies they
/// refine tinted on the drawn geometry, a box as its twelve edges, a sphere as
/// [`sphere`] draws one, each labelled with its name and size.
pub fn sizes(s: &mut Scene, model: &fieldcraft_model::Model, names: &[crate::selection::BodyNames], display: &DisplayMesh, t: &Tokens) {
use fieldcraft_model::{Entity, GeometryRef, SizeRegion};
if model.size_controls().is_empty() {
return;
}
let color = rgb(t.warm);
for (name, c) in model.size_controls() {
let text = format!("{name} {}", c.size);
let v = |p: [f64; 3]| Vec3 { x: p[0], y: p[1], z: p[2] };
match &c.region {
SizeRegion::Box { min, max } => {
let corner = |k: usize| v([0, 1, 2].map(|a| if (k >> a) & 1 == 1 { max[a] } else { min[a] }));
for k in 0..8usize {
for a in 0..3 {
if (k >> a) & 1 == 0 {
s.lines.push(SceneLine { a: corner(k), b: corner(k | (1 << a)), color, width: 1.5 });
}
}
}
s.labels.push(SceneLabel { p: (corner(0) + corner(7)) * 0.5, text, color });
}
SizeRegion::Sphere { center, radius } => {
sphere(s, v(*center), *radius, color);
s.labels.push(SceneLabel { p: v(*center), text, color });
}
SizeRegion::Geometry(refs) => {
let (mut sum, mut n) = (Vec3 { x: 0.0, y: 0.0, z: 0.0 }, 0usize);
for tri in &display.surface_tris {
let Some(body) = names.get(tri.body as usize) else { continue };
let face = body.faces.get(tri.face as usize).and_then(|f| GeometryRef::face(body.name.clone(), f.clone()).ok());
let hit = refs.iter().any(|r| match &r.entity {
Entity::Body => r.body == body.name,
_ => face.as_ref().is_some_and(|f| r.selects(f)),
});
if hit {
s.fills.push(SceneFill { tri: SceneTri { p: tri.p, normal: tri.normal, color }, alpha: 0.3 });
sum = sum + (tri.p[0] + tri.p[1] + tri.p[2]) * (1.0 / 3.0);
n += 1;
}
}
if n > 0 {
s.labels.push(SceneLabel { p: sum * (1.0 / n as f64), text, color });
}
}
}
}
}
/// How many element edges of the drawn mesh lie along the geometry edge `run` (its polyline,
/// `length` long): those whose ends and middle are all on it, counted once per element edge
/// (a quadratic edge is drawn in pieces). 0 when the mesh has none there (not meshed).
@@ -265,4 +316,21 @@ mod tests {
assert_eq!(s.tris.first().map(|t| t.color), None);
assert!(radius(&d.bounds, &Camera::default()) > 0.5);
}
#[test]
fn size_controls_draw_where_they_apply() {
use fieldcraft_model::{SizeControl, SizeRegion};
let mut m = Model::default();
m.set_size_control("Zone", SizeControl { region: SizeRegion::Box { min: [0.0; 3], max: [1.0; 3] }, size: 0.5 }).unwrap();
m.set_size_control("Ball", SizeControl { region: SizeRegion::Sphere { center: [0.0; 3], radius: 2.0 }, size: 0.5 }).unwrap();
let mut s = Scene::default();
sizes(&mut s, &m, &[], &DisplayMesh::default(), &Tokens::dark());
assert!(s.lines.len() >= 12 && !s.fills.is_empty(), "the box's edges and the sphere");
let mut labels: Vec<&str> = s.labels.iter().map(|l| l.text.as_str()).collect();
labels.sort();
assert_eq!(labels, ["Ball 0.5", "Zone 0.5"]);
let mut empty = Scene::default();
sizes(&mut empty, &Model::default(), &[], &DisplayMesh::default(), &Tokens::dark());
assert!(empty.lines.is_empty() && empty.labels.is_empty());
}
}
+2 -2
View File
@@ -856,7 +856,7 @@ pub enum Target {
/// `to_surface`: pick the source face, then the target surface.
ExtrudeFaces,
/// The selected geometry faces as `faces: [{body, face}]`, in place of a sphere or labels
/// (the faces Relax Nodes relaxes).
/// (the faces Relax Nodes relaxes, the faces a local size refines).
Faces,
}
@@ -870,7 +870,7 @@ pub fn target(id: &str, params: &Value) -> Option<Target> {
"geometry.datum.point" | "geometry.datum.point.edit" => Some(Target::DatumPoint),
"mesh.cells" => Some(Target::Bodies),
"mesh.hex.extrude" => Some(Target::ExtrudeFaces),
"mesh.node.relax" => Some(Target::Faces),
"mesh.node.relax" | "mesh.size.set" => Some(Target::Faces),
"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)