forked from 1b-agent/fieldcraft-upstream
UI: seeds as ticks along their edges, Mesh › Seeds, and seeding the selection
A seed draws a tick at each node it puts on its edge, so a bias shows as ticks crowding toward an end. The browser lists the seeds under Mesh; a click selects the seeded edge. The Seed Edges form takes the selected edges with Use selection. The toolbar entry is ready (its icon renamed with it). Part of #230. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
e8cada94d0
commit
8df4ea040c
9 files changed
+134
-4
No files matched your search
+1
-1
@@ -72,7 +72,7 @@ is committed and listed here with its exports.
|
||||
| assets/icons/mesh.region.remesh.svg, assets/icons/mesh.region.remesh.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.region.remesh.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.size.adapt.svg, assets/icons/mesh.size.adapt.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.adapt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.size.global.svg, assets/icons/mesh.size.global.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.global.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.size.seed.svg, assets/icons/mesh.size.seed.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.seed.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.seed.edge.svg, assets/icons/mesh.seed.edge.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.seed.edge.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.smooth.svg, assets/icons/mesh.smooth.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.smooth.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.surface.check.svg, assets/icons/mesh.surface.check.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.surface.check.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.tag.svg, assets/icons/mesh.tag.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.tag.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
||||
<title>mesh.size.seed</title>
|
||||
<title>mesh.seed.edge</title>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<path d="M3 19 C7 5 17 5 21 19" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.7 19 C1.7 18.282 2.282 17.7 3 17.7 C3.718 17.7 4.3 18.282 4.3 19 C4.3 19.718 3.718 20.3 3 20.3 C2.282 20.3 1.7 19.718 1.7 19 Z" fill="#4f9ce8"/>
|
||||
File renamed without changes.
File renamed without changes.
@@ -296,6 +296,20 @@ enum Act {
|
||||
ShowResult(String),
|
||||
}
|
||||
|
||||
/// A seed in a few words: `8 exact`, `2.5 min, bias 4 end`.
|
||||
fn seed_note(s: &fieldcraft_model::Seed) -> String {
|
||||
let what = match s.divisions {
|
||||
fieldcraft_model::Divisions::Count(n) => format!("{n}"),
|
||||
fieldcraft_model::Divisions::Size(h) => format!("size {h}"),
|
||||
};
|
||||
let bias = s.bias.map(|b| format!(", bias {} {}", b.ratio, json_name(&b.toward))).unwrap_or_default();
|
||||
format!("{what} {}{bias}", s.constraint.name())
|
||||
}
|
||||
|
||||
fn json_name<T: serde::Serialize>(v: &T) -> String {
|
||||
serde_json::to_value(v).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(app.ui.dark);
|
||||
let mut acts: Vec<Act> = Vec::new();
|
||||
@@ -485,6 +499,40 @@ 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.
|
||||
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(
|
||||
app,
|
||||
ui,
|
||||
egui::Id::new("fc_row_seeds"),
|
||||
&Row {
|
||||
depth: 2,
|
||||
fold: Some(open(app, "seeds")),
|
||||
icon: Some("mesh.seed.edge"),
|
||||
label: "Seeds",
|
||||
note: Some(format!("{}", seeds.len())),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
if r.fold || r.clicked {
|
||||
acts.push(Act::Fold("seeds".into()));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Definitions.
|
||||
let groups: [(&str, &str, &[Def]); 7] = [
|
||||
|
||||
@@ -100,6 +100,7 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
let tip = match target {
|
||||
selection::Target::Members => "The selection becomes the set's members (geometry by name, the mesh by label)",
|
||||
selection::Target::Set | selection::Target::TargetSet => "Makes a set of the selection (model.set.create) and names it here",
|
||||
selection::Target::Edges => "The selected design edges (Select: Edges) become the edges seeded",
|
||||
};
|
||||
use_selection = ui.add_enabled(selected.is_some(), egui::Button::new("Use selection")).on_hover_text(tip).clicked();
|
||||
match &selected {
|
||||
|
||||
@@ -840,6 +840,7 @@ impl ModelApp {
|
||||
let mut symbols = scene::symbols(&mut s, &self.glyphs, |n| drawn.node_position(n), show, self.plot.1.is_some(), &t);
|
||||
if show.geometry && self.plot.1.is_none() {
|
||||
symbols.union(scene::datums(&mut s, &self.session.model, &self.display.1.bounds, &t));
|
||||
scene::seeds(&mut s, &self.session.model, &self.pick.names, &self.display.1, &t);
|
||||
}
|
||||
self.scene = (Some(key), s, symbols);
|
||||
}
|
||||
@@ -1050,6 +1051,9 @@ impl ModelApp {
|
||||
let Some(target) = selection::target(command, ¶ms) else { return Err(format!("{command} takes no selection")) };
|
||||
let filled = match target {
|
||||
selection::Target::Members => selection::with_members(¶ms, &self.pick.selection),
|
||||
selection::Target::Edges => {
|
||||
selection::with_edges(¶ms, &self.pick.selection).ok_or("no edge is selected: pick edges with Select: Edges")?
|
||||
}
|
||||
selection::Target::Set | selection::Target::TargetSet => {
|
||||
let base = params.get("name").and_then(Value::as_str).map(|n| format!("{n}-set"));
|
||||
let set = self.selection_set(base.as_deref())?;
|
||||
|
||||
@@ -115,6 +115,41 @@ pub fn build(display: &DisplayMesh, style: Style, show: Show, t: &Tokens) -> Sce
|
||||
s
|
||||
}
|
||||
|
||||
/// Size of a seed's tick (points).
|
||||
const SEED_TICK: f64 = 6.0;
|
||||
|
||||
/// Draw the model's edge seeds as ticks at the nodes they put along their edges (the drawn
|
||||
/// bodies' B-rep edges, [`DisplayMesh::curves`], named by `names`): the seeded count, spaced by
|
||||
/// the bias, so a grading shows as ticks crowding toward an end.
|
||||
pub fn seeds(s: &mut Scene, model: &fieldcraft_model::Model, names: &[crate::selection::BodyNames], display: &DisplayMesh, t: &Tokens) {
|
||||
if model.seeds().is_empty() {
|
||||
return;
|
||||
}
|
||||
let color = rgb(t.warm);
|
||||
for run in display.curves.chunk_by(|a, b| a.body == b.body && a.edge == b.edge) {
|
||||
let Some(first) = run.first() else { continue };
|
||||
let Some(edge) = names.get(first.body as usize).and_then(|b| b.edge(first.edge)) else { continue };
|
||||
let Some(seed) = model.seeds().iter().find(|seed| seed.edge.selects(&edge)) else { continue };
|
||||
let lengths: Vec<f64> = run.iter().map(|c| c.a.dist(c.b)).collect();
|
||||
let total: f64 = lengths.iter().sum();
|
||||
if !(total > 0.0 && total.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
// Walk the polyline to each node's arc length.
|
||||
let (mut k, mut walked) = (0, 0.0);
|
||||
for f in seed.spacing(seed.count(total)) {
|
||||
let target = f * total;
|
||||
while k + 1 < run.len() && walked + lengths.get(k).copied().unwrap_or(0.0) < target {
|
||||
walked += lengths.get(k).copied().unwrap_or(0.0);
|
||||
k += 1;
|
||||
}
|
||||
let (Some(c), Some(l)) = (run.get(k), lengths.get(k)) else { continue };
|
||||
let u = if *l > 0.0 { ((target - walked) / l).clamp(0.0, 1.0) } else { 0.0 };
|
||||
s.points.push(ScenePoint { p: c.a + (c.b - c.a) * u, color, size: SEED_TICK });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The radius around the camera target that holds the bounds (for the clipping planes).
|
||||
pub fn radius(b: &Aabb3, cam: &Camera) -> f64 {
|
||||
if b.is_empty() { 1.0 } else { (cam.target.dist(b.center()) + b.diagonal() * 0.5).max(1e-3) }
|
||||
@@ -126,6 +161,26 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn seeds_draw_ticks_at_their_nodes() {
|
||||
use fieldcraft_model::{Bias, BiasToward, Divisions, Entity, GeometryRef, Seed, SeedConstraint};
|
||||
let mut d = DisplayMesh::default();
|
||||
// Two edges of 10 mm; the first in two segments.
|
||||
d.add_curves(&[vec![[0.0; 3], [4.0, 0.0, 0.0], [10.0, 0.0, 0.0]], vec![[0.0; 3], [0.0, 10.0, 0.0]]]);
|
||||
let names = [crate::selection::BodyNames { name: "B".into(), faces: vec![], edges: vec!["a|b".into(), "a|c".into()] }];
|
||||
let mut m = Model::default();
|
||||
let edge = GeometryRef::new("B", Entity::Edge("a|b".into())).unwrap();
|
||||
m.set_seed(Seed::new(edge, Divisions::Count(4), Some(Bias { ratio: 8.0, toward: BiasToward::Start }), SeedConstraint::Exact).unwrap())
|
||||
.unwrap();
|
||||
let mut s = Scene::default();
|
||||
seeds(&mut s, &m, &names, &d, &Tokens::dark());
|
||||
let xs: Vec<f64> = s.points.iter().map(|p| p.p.x).collect();
|
||||
let want = [0.0, 10.0 / 15.0, 30.0 / 15.0, 70.0 / 15.0, 10.0];
|
||||
assert_eq!(xs.len(), 5);
|
||||
assert!(xs.iter().zip(want).all(|(a, b)| (a - b).abs() < 1e-9), "{xs:?}");
|
||||
assert!(s.points.iter().all(|p| p.p.y == 0.0), "only the seeded edge has ticks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles_choose_faces_edges_and_nodes() {
|
||||
let mut m = Model::default();
|
||||
|
||||
@@ -279,7 +279,7 @@ impl BodyNames {
|
||||
}
|
||||
|
||||
/// The reference to B-rep edge `edge` of this body, when it has a name.
|
||||
fn edge(&self, edge: u32) -> Option<GeometryRef> {
|
||||
pub fn edge(&self, edge: u32) -> Option<GeometryRef> {
|
||||
GeometryRef::new(self.name.clone(), Entity::Edge(self.edges.get(edge as usize)?.clone())).ok()
|
||||
}
|
||||
}
|
||||
@@ -786,6 +786,8 @@ pub enum Target {
|
||||
/// A set made from the selection, named in `set` (sections, couplings, surface and body
|
||||
/// loads).
|
||||
Set,
|
||||
/// The selected geometry edges, as `edges: [{body, edge}]` (edge seeds).
|
||||
Edges,
|
||||
}
|
||||
|
||||
/// Where command `id`'s form takes the selection, if it does. `params` (the form's current
|
||||
@@ -793,6 +795,7 @@ pub enum Target {
|
||||
pub fn target(id: &str, params: &Value) -> Option<Target> {
|
||||
match id {
|
||||
"model.set.create" | "model.set.edit" => Some(Target::Members),
|
||||
"mesh.seed.edge" | "mesh.seed.clear" => Some(Target::Edges),
|
||||
"model.boundary_condition.create" | "model.boundary_condition.edit" => Some(Target::TargetSet),
|
||||
"model.section.create" | "model.section.edit" | "model.coupling.create" | "model.coupling.edit" => Some(Target::Set),
|
||||
"model.load.create" | "model.load.edit" => {
|
||||
@@ -815,6 +818,25 @@ pub fn with_members(params: &Value, selection: &Selection) -> Value {
|
||||
Value::Object(out)
|
||||
}
|
||||
|
||||
/// `params` with the selection's geometry edges as `edges` (replacing any it had); `None` when no
|
||||
/// edge is selected.
|
||||
pub fn with_edges(params: &Value, selection: &Selection) -> Option<Value> {
|
||||
let edges: Vec<Value> = selection
|
||||
.items()
|
||||
.iter()
|
||||
.filter_map(|i| match i {
|
||||
Item::Geometry(g) if matches!(g.entity, Entity::Edge(_)) => Some(json!(g)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if edges.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut out = params.as_object().cloned().unwrap_or_default();
|
||||
out.insert("edges".into(), Value::Array(edges));
|
||||
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();
|
||||
@@ -825,7 +847,7 @@ pub fn with_set(params: &Value, target: Target, set: &str) -> Value {
|
||||
Target::Set => {
|
||||
out.insert("set".into(), json!(set));
|
||||
}
|
||||
Target::Members => {}
|
||||
Target::Members | Target::Edges => {}
|
||||
}
|
||||
Value::Object(out)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user