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 <[email protected]>
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 02:30:24 -04:00
1 parent 9b705f5334
commit f8b92fcd89
6 files changed
+192 -6

No files matched your search

+10 -4
View File
@@ -58,8 +58,14 @@ pub const SET: &[Field] = &[
labels("elements", "Elements", LabelOf::Element)
.help("Mesh element labels; Use selection takes the picked elements.")
.hint("labels: 1, 2, 10-20"),
json("faces", "Element faces").help("Element faces, as a JSON list of element label and local face number.").hint("[{\"element\", \"face\"}]"),
json("edges", "Element edges").help("Element edges, as a JSON list of element label and local edge number.").hint("[{\"element\", \"edge\"}]"),
sides("faces", "Element faces", Side::Face)
.help(
"Element faces as element:face (local face number), ranges of elements sharing one: 1-20:3, 25:1. Use selection takes the picked faces.",
)
.hint("element:face, e.g. 1-20:3, 25:1"),
sides("edges", "Element edges", Side::Edge)
.help("Element edges as element:edge (local edge number), ranges of elements sharing one: 1-20:3, 25:1.")
.hint("element:edge, e.g. 1-20:3"),
];
const ELASTIC: Field = variants(
@@ -1468,8 +1474,8 @@ pub const TAG: &[Field] = &[
.help("The geometry the mesh entities are tagged with.")
.req(),
labels("elements", "Elements", LabelOf::Element).help("Element labels."),
json("faces", "Element faces").help("Element faces, as JSON."),
json("edges", "Element edges").help("Element edges, as JSON."),
sides("faces", "Element faces", Side::Face).help("Element faces as element:face: 1-20:3, 25:1.").hint("element:face"),
sides("edges", "Element edges", Side::Edge).help("Element edges as element:edge: 1-20:3, 25:1.").hint("element:edge"),
labels("nodes", "Nodes", LabelOf::Node).help("Node labels."),
];
+67
View File
@@ -137,6 +137,9 @@ pub enum Kind {
/// A text per item: an object from the names of a collection's items to a string; an item
/// left out takes the command's default.
RefText(Collection),
/// A list of element faces or edges, `{"element", "face"}` or `{"element", "edge"}` (at most
/// [`MAX_LABELS`]).
Sides(Side),
/// Any JSON: what the schema does not describe.
Json,
}
@@ -148,6 +151,23 @@ pub enum LabelOf {
Element,
}
/// What a [`Kind::Sides`] list holds: element faces or element edges.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Side {
Face,
Edge,
}
impl Side {
/// The key of the local number in each entry, and its word.
pub fn key(self) -> &'static str {
match self {
Side::Face => "face",
Side::Edge => "edge",
}
}
}
impl LabelOf {
pub fn label(self) -> &'static str {
match self {
@@ -520,6 +540,10 @@ pub const fn rows(key: &'static str, label: &'static str, fields: &'static [Fiel
pub const fn labels(key: &'static str, label: &'static str, of: LabelOf) -> Field {
field(key, label, Kind::Labels(of))
}
/// A list of element faces or edges.
pub const fn sides(key: &'static str, label: &'static str, side: Side) -> Field {
field(key, label, Kind::Sides(side))
}
/// The columns of a point, `[x, y, z]`.
pub const XYZ: &[&str] = &["x", "y", "z"];
/// A list of points `[x, y, z]`.
@@ -1078,6 +1102,21 @@ fn value_problem(f: &'static Field, v: &Value, path: &str, out: &mut Vec<Problem
(!ok).then_some(ProblemKind::NotAChoice)
}
Kind::Unit | Kind::Json => None,
Kind::Sides(side) => match v.as_array() {
Some(a)
if a.len() <= MAX_LABELS
&& a.iter().all(|x| {
x.as_object().is_some_and(|o| {
o.len() == 2
&& o.get("element").and_then(Value::as_u64).is_some_and(|n| n >= 1 && n <= u64::from(u32::MAX))
&& o.get(side.key()).and_then(Value::as_u64).is_some_and(|n| n >= 1 && n <= u64::from(u8::MAX))
})
}) =>
{
None
}
_ => Some(ProblemKind::NotAList { max: MAX_LABELS, what: if side == Side::Face { "element faces" } else { "element edges" } }),
},
Kind::Labels(_) => match v.as_array() {
Some(a) if a.len() <= MAX_LABELS && a.iter().all(|x| x.as_u64().is_some_and(|n| n >= 1 && n <= u64::from(u32::MAX))) => None,
_ => Some(ProblemKind::NotAList { max: MAX_LABELS, what: "labels" }),
@@ -1240,6 +1279,16 @@ fn field_schema(f: &Field, depth: usize) -> Value {
}).collect::<Vec<_>>()}),
Kind::Rows { fields, max } if depth <= MAX_DEPTH => json!({"type": "array", "items": object_schema(fields, depth + 1), "maxItems": max}),
Kind::Unit => json!({"const": f.key}),
Kind::Sides(side) => json!({
"type": "array",
"maxItems": MAX_LABELS,
"items": {
"type": "object",
"properties": {"element": {"type": "integer", "minimum": 1, "maximum": u32::MAX}, side.key(): {"type": "integer", "minimum": 1, "maximum": u8::MAX}},
"required": ["element", side.key()],
"additionalProperties": false,
},
}),
Kind::Labels(of) => {
json!({"type": "array", "items": {"type": "integer", "minimum": 1, "maximum": u32::MAX}, "maxItems": MAX_LABELS, "x-labels": of.label()})
}
@@ -1406,6 +1455,24 @@ mod tests {
assert_eq!(s["properties"]["as"]["additionalProperties"]["type"], "string");
}
/// A sides list holds element and local face (or edge) objects, many more than a form's rows.
#[test]
fn sides_hold_element_and_local_number() {
const F: &[Field] = &[sides("faces", "Faces", Side::Face), sides("edges", "Edges", Side::Edge)];
let many: Vec<Value> = (1..=5000).map(|e| json!({"element": e, "face": 3})).collect();
assert!(problems(F, &json!({"faces": many, "edges": [{"element": 2, "edge": 12}]})).is_empty());
for bad in [
json!({"faces": [{"element": 0, "face": 1}]}),
json!({"faces": [{"element": 1, "face": 0}]}),
json!({"faces": [{"element": 1, "edge": 1}]}),
json!({"faces": [{"element": 1, "face": 1, "x": 1}]}),
json!({"edges": [3]}),
] {
assert_eq!(problems(F, &bad).len(), 1, "{bad}");
}
assert_eq!(json_schema(F)["properties"]["edges"]["items"]["required"], json!(["element", "edge"]));
}
/// A tuple takes its minimum to its full length of positive numbers, whole ones when asked.
#[test]
fn tuple_takes_two_or_three_positive_numbers() {