forked from 1b-agent/fieldcraft-upstream
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:
1 parent
b62be1ada6
commit
030799dfc8
4 files changed
+73
-4
No files matched your search
@@ -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(¶ms) == "source" => "Use as source",
|
||||
|
||||
@@ -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(¶ms, &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(¶ms, &self.pick.selection).ok_or("no face is selected: pick the faces to relax (Select: Faces)")?
|
||||
selection::with_faces(¶ms, &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")?;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user