forked from 1b-agent/fieldcraft-upstream
ui: element faces and edges as element:side lists
A Sides field kind: element faces or edges typed as element:side with ranges (1-20:3, 25:1), no row cap, and Use selection for picked faces. Set and mesh-tag forms use it instead of a JSON box (#449). 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
9b705f5334
commit
f8b92fcd89
6 files changed
+192
-6
No files matched your search
@@ -178,12 +178,13 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
};
|
||||
});
|
||||
}
|
||||
// The nodes and elements picked in the viewport, for the label lists' Use selection.
|
||||
// The nodes, elements and element faces picked in the viewport, for Use selection.
|
||||
let mut picked = crate::form::Picked::default();
|
||||
for item in app.pick.selection.items() {
|
||||
match item {
|
||||
selection::Item::Node(n) => picked.nodes.push(u64::from(*n)),
|
||||
selection::Item::Element(e) => picked.elements.push(u64::from(*e)),
|
||||
selection::Item::Face(f) => picked.faces.push((u64::from(f.element), u64::from(f.face))),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,13 @@ fn scalar(f: &Field, v: &Value, units: UnitSystem) -> String {
|
||||
_ => crate::context::value_text(v),
|
||||
},
|
||||
Kind::Rows { .. } => v.as_array().map_or_else(|| crate::context::value_text(v), |a| plural(a.len(), "row")),
|
||||
Kind::Sides(side) => match v
|
||||
.as_array()
|
||||
.and_then(|a| a.iter().map(|x| Some((x.get("element")?.as_u64()?, x.get(side.key())?.as_u64()?))).collect::<Option<Vec<_>>>())
|
||||
{
|
||||
Some(l) => crate::fields::format_sides(&l),
|
||||
None => crate::context::value_text(v),
|
||||
},
|
||||
Kind::Labels(_) => match v.as_array().and_then(|a| a.iter().map(Value::as_u64).collect::<Option<Vec<_>>>()) {
|
||||
Some(l) => crate::fields::format_labels(&l),
|
||||
None => crate::context::value_text(v),
|
||||
|
||||
@@ -228,6 +228,44 @@ pub fn parse_labels(text: &str, max: usize) -> Option<Vec<u64>> {
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Element faces or edges as typed: `element:side`, runs of consecutive elements with the same
|
||||
/// side as `a-b:side` ("1-5:3, 8:1").
|
||||
pub fn format_sides(sides: &[(u64, u64)]) -> String {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let mut i = 0;
|
||||
while let Some(&(a, side)) = sides.get(i) {
|
||||
let mut j = i;
|
||||
while sides.get(j + 1).is_some_and(|&(n, s)| s == side && Some(n) == sides.get(j).and_then(|(m, _)| m.checked_add(1))) {
|
||||
j += 1;
|
||||
}
|
||||
let b = sides.get(j).map_or(a, |(n, _)| *n);
|
||||
out.push(match j - i {
|
||||
0 => format!("{a}:{side}"),
|
||||
1 => format!("{a}:{side}, {b}:{side}"),
|
||||
_ => format!("{a}-{b}:{side}"),
|
||||
});
|
||||
i = j + 1;
|
||||
}
|
||||
out.join(", ")
|
||||
}
|
||||
|
||||
/// Element faces or edges typed as `element:side` or `a-b:side`, separated by commas or spaces;
|
||||
/// `None` when an element is not a label from 1, a side not 1–255, or there would be more than
|
||||
/// `max`.
|
||||
pub fn parse_sides(text: &str, max: usize) -> Option<Vec<(u64, u64)>> {
|
||||
let mut out = Vec::new();
|
||||
for part in text.split(|c: char| c == ',' || c.is_whitespace()).filter(|p| !p.is_empty()) {
|
||||
let (elements, side) = part.split_once(':')?;
|
||||
let side = side.parse::<u64>().ok().filter(|s| (1..=u64::from(u8::MAX)).contains(s))?;
|
||||
let labels = parse_labels(elements, max.saturating_sub(out.len()))?;
|
||||
if labels.is_empty() {
|
||||
return None;
|
||||
}
|
||||
out.extend(labels.into_iter().map(|e| (e, side)));
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A short label before a field ("X"), dim.
|
||||
pub fn caption(ui: &mut Ui, s: &Style, text: &str) {
|
||||
ui.label(RichText::new(text).small().color(s.dim));
|
||||
@@ -257,6 +295,18 @@ mod tests {
|
||||
assert_eq!(parse_labels(&format_labels(&[3, 4, 5, 9]), 100), Some(vec![3, 4, 5, 9]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sides_print_and_parse_as_ranges() {
|
||||
let s = [(1, 3), (2, 3), (3, 3), (5, 1), (6, 2), (7, 2)];
|
||||
assert_eq!(format_sides(&s), "1-3:3, 5:1, 6:2, 7:2");
|
||||
assert_eq!(parse_sides(&format_sides(&s), 100).as_deref(), Some(&s[..]));
|
||||
assert_eq!(parse_sides("4:1 9-10:6", 100), Some(vec![(4, 1), (9, 6), (10, 6)]));
|
||||
assert_eq!(parse_sides("", 100), Some(vec![]));
|
||||
for bad in ["4", "4:", ":3", "0:1", "4:0", "4:256", "1-200:1", "x:1"] {
|
||||
assert_eq!(parse_sides(bad, 100), None, "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_print_as_typed() {
|
||||
assert_eq!(format_number(210000.0), "210000");
|
||||
|
||||
@@ -47,6 +47,8 @@ pub struct Context<'a> {
|
||||
pub struct Picked {
|
||||
pub nodes: Vec<u64>,
|
||||
pub elements: Vec<u64>,
|
||||
/// Element faces: (element, local face).
|
||||
pub faces: Vec<(u64, u64)>,
|
||||
}
|
||||
|
||||
/// What a frame of the form did.
|
||||
@@ -333,6 +335,7 @@ impl Form<'_> {
|
||||
}
|
||||
Kind::Json => self.json(ui, id, value, f.hint),
|
||||
Kind::Labels(of) => self.labels(ui, id, value, of, f.hint),
|
||||
Kind::Sides(side) => self.sides(ui, id, value, side, f.hint),
|
||||
Kind::Points { columns, max, .. } => self.points(ui, id, value, f, columns, max),
|
||||
Kind::Multi(options) => self.multi(ui, value, options),
|
||||
Kind::NumberOrAuto { .. } => self.number_or_auto(ui, id, value, f),
|
||||
@@ -739,6 +742,58 @@ impl Form<'_> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Element faces or edges typed as `element:side` ranges ([`fields::parse_sides`]); for faces,
|
||||
/// Use selection takes the faces picked in the viewport.
|
||||
fn sides(&mut self, ui: &mut Ui, id: Id, value: Option<&Value>, side: schema::Side, hint: &str) -> Option<Value> {
|
||||
let key = side.key();
|
||||
let current: Option<Vec<(u64, u64)>> = match value {
|
||||
None | Some(Value::Null) => Some(Vec::new()),
|
||||
Some(v) => v.as_array().and_then(|a| a.iter().map(|x| Some((x.get("element")?.as_u64()?, x.get(key)?.as_u64()?))).collect()),
|
||||
};
|
||||
let Some(current) = current else {
|
||||
self.json_only(ui);
|
||||
return None;
|
||||
};
|
||||
let to_value =
|
||||
|l: &[(u64, u64)]| if l.is_empty() { Value::Null } else { Value::Array(l.iter().map(|(e, s)| json!({"element": e, key: s})).collect()) };
|
||||
let shown = fields::format_sides(¤t);
|
||||
let mut text = ui.data(|d| d.get_temp::<(String, String)>(id)).filter(|(_, over)| *over == shown).map_or_else(|| shown.clone(), |(t, _)| t);
|
||||
let mut out = None;
|
||||
let mut over = shown.clone();
|
||||
ui.vertical(|ui| {
|
||||
let parsed = fields::parse_sides(&text, schema::MAX_LABELS);
|
||||
let mut edit =
|
||||
egui::TextEdit::singleline(&mut text).id(id).desired_width(fill(ui)).hint_text(RichText::new(hint).italics().color(self.style.dim));
|
||||
if parsed.is_none() {
|
||||
edit = edit.text_color(self.style.error);
|
||||
self.out.invalid += 1;
|
||||
}
|
||||
if ui.add(edit).changed()
|
||||
&& let Some(l) = fields::parse_sides(&text, schema::MAX_LABELS)
|
||||
{
|
||||
over = fields::format_sides(&l);
|
||||
out = Some(to_value(&l));
|
||||
}
|
||||
if side == schema::Side::Face {
|
||||
let picked = self.cx.picked.faces.clone();
|
||||
let label = format!("Use selection ({} face{})", picked.len(), if picked.len() == 1 { "" } else { "s" });
|
||||
let tip = "The element faces picked in the viewport (Select: Element faces)";
|
||||
if ui
|
||||
.add_enabled(!picked.is_empty(), egui::Button::new(RichText::new(label).small()))
|
||||
.on_hover_text(tip)
|
||||
.on_disabled_hover_text(tip)
|
||||
.clicked()
|
||||
{
|
||||
text = fields::format_sides(&picked);
|
||||
over = text.clone();
|
||||
out = Some(to_value(&picked));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.data_mut(|d| d.insert_temp(id, (text, over)));
|
||||
out
|
||||
}
|
||||
|
||||
/// A list of points: a row of x, y, z each, removable, and Add point.
|
||||
fn points(&mut self, ui: &mut Ui, id: Id, value: Option<&Value>, f: &Field, columns: &[&str], max: usize) -> Option<Value> {
|
||||
let current: Vec<Value> = match value {
|
||||
@@ -1312,7 +1367,7 @@ mod tests {
|
||||
attempted: false,
|
||||
refused: None,
|
||||
problems: &[],
|
||||
picked: Picked { nodes: vec![4, 5], elements: Vec::new() },
|
||||
picked: Picked { nodes: vec![4, 5], ..Picked::default() },
|
||||
};
|
||||
show(ui, &Tokens::get(false), &cx, form, &mut params);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user