forked from 1b-agent/fieldcraft-upstream
ui: typed form fields for label lists, point lists, multi-choices and number-or-auto (#169)
Four field kinds in the schema, each checked and described in JSON Schema: labels (nodes or elements, typed as 1-5, 8 with Use selection taking the viewport's picks), points (rows of x, y, z, added and removed), multi (checkboxes: coupling DOFs, step output) and number or auto (the deformed scale). Node and element lists, probe and Delaunay points and block corners use them; the remaining JSON fields hold geometry-specific shapes. 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
c0cbcadf3d
commit
1f85a8caa3
6 files changed
+420
-21
No files matched your search
@@ -54,8 +54,10 @@ pub const SET: &[Field] = &[
|
||||
)
|
||||
.help("Bodies of the design, or their named faces, edges or vertices. Kept by name, so the set survives remeshing.")
|
||||
.hint("whole bodies, or their faces, edges or vertices"),
|
||||
json("nodes", "Nodes").help("Mesh node labels, as a JSON list.").hint("[labels]"),
|
||||
json("elements", "Elements").help("Mesh element labels, as a JSON list.").hint("[labels]"),
|
||||
labels("nodes", "Nodes", LabelOf::Node).help("Mesh node labels; Use selection takes the picked nodes.").hint("labels: 1, 2, 10-20"),
|
||||
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\"}]"),
|
||||
];
|
||||
@@ -351,9 +353,9 @@ pub const REFERENCE_POINT: &[Field] = &[
|
||||
int("label", "Node label", 0, u32::MAX as i64).help("The node label written to decks.").hint("0: the highest free label"),
|
||||
];
|
||||
|
||||
const DOFS: Field = json("dofs", "Degrees of freedom")
|
||||
const DOFS: Field = multi("dofs", "Degrees of freedom", &[("1", "UX"), ("2", "UY"), ("3", "UZ"), ("4", "URX"), ("5", "URY"), ("6", "URZ")])
|
||||
.help("Which of the six DOFs to couple (1–3 translations, 4–6 rotations); all when empty.")
|
||||
.hint("[1, …, 6], default all");
|
||||
.hint("all");
|
||||
|
||||
pub const COUPLING: &[Field] = &[
|
||||
NAME,
|
||||
@@ -809,9 +811,20 @@ const PROCEDURE: Field = variants(
|
||||
)
|
||||
.help("What the step solves: linear or nonlinear static, buckling, frequencies, explicit dynamics, heat transfer or flow.");
|
||||
|
||||
const OUTPUT: Field = json("output", "Output")
|
||||
.help("Which results to keep, as JSON; by procedure when empty.")
|
||||
.hint("[\"displacement\", \"stress\", …]; by procedure when empty");
|
||||
const OUTPUT: Field = multi(
|
||||
"output",
|
||||
"Output",
|
||||
&[
|
||||
("\"displacement\"", "Displacement"),
|
||||
("\"reaction_force\"", "Reaction force"),
|
||||
("\"stress\"", "Stress"),
|
||||
("\"strain\"", "Strain"),
|
||||
("\"temperature\"", "Temperature"),
|
||||
("\"heat_flux\"", "Heat flux"),
|
||||
],
|
||||
)
|
||||
.help("Which results to keep; by procedure when none is ticked.")
|
||||
.hint("by procedure");
|
||||
const STEP_BCS: Field =
|
||||
references("boundary_conditions", "Boundary conditions", C::BoundaryCondition).help("The boundary conditions active in the step.");
|
||||
const STEP_LOADS: Field = references("loads", "Loads", C::Load).help("The loads active in the step.");
|
||||
@@ -1168,7 +1181,7 @@ pub const BLOCK: &[Field] = &[
|
||||
.help("The element type of the block.")
|
||||
.req()
|
||||
.def("\"hex8\""),
|
||||
json("corners", "Corners").help("The eight corner points (four for 2D), as JSON.").req().hint("[[x, y, z] × 8] (2D: × 4 at z = 0)"),
|
||||
points("corners", "Corners", D::Length, 8).help("The eight corner points (four for 2D, at z = 0).").req(),
|
||||
json("divisions", "Divisions").help("Elements along each edge direction, as JSON.").req().hint("[n12, n14, n15] (2D: [n12, n14])"),
|
||||
json("bias", "Bias").help("Element size ratio along each direction, as JSON; 1 is uniform.").hint("[1, 1, 1]"),
|
||||
json("edges", "Curved and named edges").help("Edge curves and names, as JSON."),
|
||||
@@ -1193,7 +1206,7 @@ pub const PLANAR: &[Field] = &[
|
||||
];
|
||||
|
||||
pub const QUALITY: &[Field] = &[
|
||||
json("elements", "Elements").help("The elements to measure, as JSON labels; all when empty.").hint("[labels]; all when empty"),
|
||||
labels("elements", "Elements", LabelOf::Element).help("The elements to measure; all when empty.").hint("all"),
|
||||
int("bins", "Histogram bins", 1, 1000).help("Number of bars in the quality histogram.").def("10"),
|
||||
];
|
||||
|
||||
@@ -1233,7 +1246,7 @@ pub const ELEMENT_ADD: &[Field] = &[
|
||||
.help("The element type.")
|
||||
.req()
|
||||
.def("\"hex8\""),
|
||||
json("nodes", "Nodes").help("The element's node labels in order, as JSON.").req().hint("[labels]"),
|
||||
labels("nodes", "Nodes", LabelOf::Node).help("The element's node labels in order.").req().hint("labels in order: 1, 2, 3, 4"),
|
||||
];
|
||||
|
||||
pub const SEED_EDGE: &[Field] = &[
|
||||
@@ -1262,7 +1275,7 @@ pub const SEED_EDGE: &[Field] = &[
|
||||
|
||||
pub const SEED_CLEAR: &[Field] = &[json("edges", "Edges").help("The edges to clear, as JSON; every seed when empty.").hint("every seed")];
|
||||
|
||||
pub const DELAUNAY_POINTS: &[Field] = &[json("points", "Points").help("The points to triangulate, as JSON.").req().hint("[[x, y, z], …]")];
|
||||
pub const DELAUNAY_POINTS: &[Field] = &[points("points", "Points", D::Length, 20_000).help("The points to triangulate.").req()];
|
||||
|
||||
pub const TAG: &[Field] = &[
|
||||
group(
|
||||
@@ -1277,10 +1290,10 @@ pub const TAG: &[Field] = &[
|
||||
)
|
||||
.help("The geometry the mesh entities are tagged with.")
|
||||
.req(),
|
||||
json("elements", "Elements").help("Element labels, as JSON."),
|
||||
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."),
|
||||
json("nodes", "Nodes").help("Node labels, as JSON."),
|
||||
labels("nodes", "Nodes", LabelOf::Node).help("Node labels."),
|
||||
];
|
||||
|
||||
// ---- Geometry and files ----
|
||||
@@ -1419,7 +1432,7 @@ pub const CENTRE: &[Field] = &[
|
||||
],
|
||||
)
|
||||
.help("Faces or edges (a hole's faces or rim give its centre), or one vertex."),
|
||||
json("nodes", "Nodes").help("Mesh node labels, as a JSON list: their mean.").hint("[labels]"),
|
||||
labels("nodes", "Nodes", LabelOf::Node).help("Mesh node labels: their mean.").hint("labels: 1, 2, 10-20"),
|
||||
];
|
||||
pub const DATUM_POINT: &[Field] = &[
|
||||
NAME,
|
||||
@@ -1621,13 +1634,13 @@ pub const SOLVE_FLUID: &[Field] = &[
|
||||
];
|
||||
pub const SOLVE_PROBE: &[Field] = &[
|
||||
SOLVED.req(),
|
||||
json("nodes", "Nodes").help("Node labels to read, as JSON.").hint("[labels]"),
|
||||
labels("nodes", "Nodes", LabelOf::Node).help("Node labels to read.").hint("labels: 1, 2, 10-20"),
|
||||
reference("set", "Set", C::Set).help("A set whose nodes to read."),
|
||||
json("at", "Points").help("Points to read at the nearest nodes, as JSON.").hint("[[x, y, z], …]: nearest nodes"),
|
||||
points("at", "Points", D::Length, 10_000).help("Points to read at the nearest nodes."),
|
||||
int("mode", "Mode", 1, 1_000_000).help("The mode to read, for frequency and buckling steps."),
|
||||
int("frame", "Frame", 0, 1_000_000_000).help("The frame to read, for explicit and transient steps."),
|
||||
flag("elements", "Element results").help("Also read element results (stresses) around the nodes."),
|
||||
json("points", "Fluid points").help("Points in the fluid to read, as JSON.").hint("[[x, y, z], …]"),
|
||||
points("points", "Fluid points", D::Length, 10_000).help("Points in the fluid to read."),
|
||||
json("forces", "Fluid walls").help("Walls whose fluid forces to sum, as JSON.").hint("[\"x_min\", \"F2:top\"]"),
|
||||
];
|
||||
|
||||
@@ -1636,7 +1649,7 @@ pub const SOLVE_PROBE: &[Field] = &[
|
||||
pub const DEFORMED: &[Field] = &[
|
||||
SOLVED,
|
||||
flag("show", "Show").help("Show the deformed shape.").def("true"),
|
||||
json("scale", "Scale").help("Displacement magnification: auto, or a factor (1 is true scale).").def("\"auto\"").hint("\"auto\" or a factor"),
|
||||
number_or_auto("scale", "Scale", D::None, Some(0.0)).help("Displacement magnification: auto, or a factor (1 is true scale).").def("\"auto\""),
|
||||
];
|
||||
pub const CONTOUR: &[Field] = &[
|
||||
SOLVED,
|
||||
@@ -1692,7 +1705,7 @@ pub const EXPORT_CSV: &[Field] = &[
|
||||
path("path", "File", true).help("The CSV file to write.").req(),
|
||||
SOLVED,
|
||||
references("sets", "Sets", C::Set).help("Reactions: the sets to sum; the step's BC sets when empty.").hint("the step's BC sets"),
|
||||
json("nodes", "Nodes").help("Probe: the node labels, e.g. [1, 2, 3]."),
|
||||
labels("nodes", "Nodes", LabelOf::Node).help("Probe: the node labels."),
|
||||
];
|
||||
pub const REACTIONS: &[Field] = &[
|
||||
SOLVED,
|
||||
|
||||
+134
-1
@@ -95,10 +95,44 @@ pub enum Kind {
|
||||
fields: &'static [Field],
|
||||
max: usize,
|
||||
},
|
||||
/// A list of mesh labels, nodes or elements (at most [`MAX_LABELS`]).
|
||||
Labels(LabelOf),
|
||||
/// A list of points `[x, y, z]`, at most `max`.
|
||||
Points {
|
||||
dim: Dim,
|
||||
max: usize,
|
||||
},
|
||||
/// Any of these values, as a list: (the value as JSON text, label), e.g. `("1", "UX")` or
|
||||
/// `("\"stress\"", "Stress")`.
|
||||
Multi(&'static [(&'static str, &'static str)]),
|
||||
/// A number at least `min`, or the string `"auto"`.
|
||||
NumberOrAuto {
|
||||
dim: Dim,
|
||||
min: Option<f64>,
|
||||
},
|
||||
/// Any JSON: what the schema does not describe.
|
||||
Json,
|
||||
}
|
||||
|
||||
/// What a [`Kind::Labels`] list labels.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LabelOf {
|
||||
Node,
|
||||
Element,
|
||||
}
|
||||
|
||||
impl LabelOf {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
LabelOf::Node => "node",
|
||||
LabelOf::Element => "element",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Most labels a [`Kind::Labels`] list holds.
|
||||
pub const MAX_LABELS: usize = 1_000_000;
|
||||
|
||||
/// One variant of [`Kind::Variants`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Variant {
|
||||
@@ -354,7 +388,7 @@ impl Field {
|
||||
/// Its physical dimension, for a number or a vector of numbers.
|
||||
pub fn dim(&self) -> Option<Dim> {
|
||||
match self.kind {
|
||||
Kind::Number { dim, .. } | Kind::Vector { dim, .. } => Some(dim),
|
||||
Kind::Number { dim, .. } | Kind::Vector { dim, .. } | Kind::Points { dim, .. } | Kind::NumberOrAuto { dim, .. } => Some(dim),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -451,6 +485,22 @@ pub const fn unit(key: &'static str, label: &'static str) -> Field {
|
||||
pub const fn rows(key: &'static str, label: &'static str, fields: &'static [Field]) -> Field {
|
||||
field(key, label, Kind::Rows { fields, max: MAX_ROWS })
|
||||
}
|
||||
/// A list of node or element labels.
|
||||
pub const fn labels(key: &'static str, label: &'static str, of: LabelOf) -> Field {
|
||||
field(key, label, Kind::Labels(of))
|
||||
}
|
||||
/// A list of points `[x, y, z]`.
|
||||
pub const fn points(key: &'static str, label: &'static str, dim: Dim, max: usize) -> Field {
|
||||
field(key, label, Kind::Points { dim, max })
|
||||
}
|
||||
/// Any of `options` (each a value as JSON text, and a label), as a list.
|
||||
pub const fn multi(key: &'static str, label: &'static str, options: &'static [(&'static str, &'static str)]) -> Field {
|
||||
field(key, label, Kind::Multi(options))
|
||||
}
|
||||
/// A number at least `min`, or "auto".
|
||||
pub const fn number_or_auto(key: &'static str, label: &'static str, dim: Dim, min: Option<f64>) -> Field {
|
||||
field(key, label, Kind::NumberOrAuto { dim, min })
|
||||
}
|
||||
pub const fn json(key: &'static str, label: &'static str) -> Field {
|
||||
field(key, label, Kind::Json)
|
||||
}
|
||||
@@ -461,6 +511,11 @@ pub const fn variant(tag: &'static str, label: &'static str, fields: &'static [F
|
||||
/// The `name` of a new or edited definition.
|
||||
pub const NAME: Field = text("name", "Name").req().help("A name for it, unique among its kind; other definitions refer to it by this name.");
|
||||
|
||||
/// A [`Kind::Multi`] option's value: its JSON text parsed (text that is not JSON is a string).
|
||||
pub fn multi_value(option: &str) -> Value {
|
||||
serde_json::from_str(option).unwrap_or_else(|_| Value::String(option.to_string()))
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
/// The variant of `tag`, for [`Kind::Variants`].
|
||||
pub fn variant(&self, tag: &str) -> Option<&'static Variant> {
|
||||
@@ -670,6 +725,7 @@ impl Problem {
|
||||
format!("one of {}", options.iter().map(|(o, _)| *o).collect::<Vec<_>>().join(", "))
|
||||
}
|
||||
Some(Kind::Either(options)) => format!("one of {}", options.iter().map(|o| o.key).collect::<Vec<_>>().join(", ")),
|
||||
Some(Kind::Multi(options)) => format!("a list of {}", options.iter().map(|(o, _)| *o).collect::<Vec<_>>().join(", ")),
|
||||
_ => "one of the choices".into(),
|
||||
},
|
||||
ProblemKind::NotAVector { len } => format!("{len} numbers"),
|
||||
@@ -956,6 +1012,29 @@ 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::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" }),
|
||||
},
|
||||
Kind::Points { max, .. } => match v.as_array() {
|
||||
Some(a)
|
||||
if a.len() <= max
|
||||
&& a.iter().all(|p| p.as_array().is_some_and(|c| c.len() == 3 && c.iter().all(|x| x.as_f64().is_some_and(f64::is_finite)))) =>
|
||||
{
|
||||
None
|
||||
}
|
||||
_ => Some(ProblemKind::NotAList { max, what: "points [x, y, z]" }),
|
||||
},
|
||||
Kind::Multi(options) => match v.as_array() {
|
||||
Some(a) if a.iter().all(|x| options.iter().any(|(o, _)| multi_value(o) == *x)) => None,
|
||||
_ => Some(ProblemKind::NotAChoice),
|
||||
},
|
||||
Kind::NumberOrAuto { min, .. } => match (v, v.as_f64().filter(|x| x.is_finite())) {
|
||||
(Value::String(s), _) if s == "auto" => None,
|
||||
(_, Some(x)) if min.is_none_or(|lo| x >= lo) => None,
|
||||
(_, Some(_)) => Some(ProblemKind::Below(min.unwrap_or(0.0))),
|
||||
_ => Some(ProblemKind::Invalid("a number or \"auto\"".into())),
|
||||
},
|
||||
Kind::Rows { fields, max } => match v.as_array() {
|
||||
Some(a) if a.len() <= max => {
|
||||
for (i, row) in a.iter().enumerate() {
|
||||
@@ -1062,6 +1141,26 @@ 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::Labels(of) => {
|
||||
json!({"type": "array", "items": {"type": "integer", "minimum": 1, "maximum": u32::MAX}, "maxItems": MAX_LABELS, "x-labels": of.label()})
|
||||
}
|
||||
Kind::Points { dim, max } => {
|
||||
let mut item = json!({"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3});
|
||||
if dim != Dim::None {
|
||||
item["x-dimension"] = json!(format!("{dim:?}").to_lowercase());
|
||||
}
|
||||
json!({"type": "array", "items": item, "maxItems": max})
|
||||
}
|
||||
Kind::Multi(options) => {
|
||||
json!({"type": "array", "items": {"enum": options.iter().map(|(o, _)| multi_value(o)).collect::<Vec<_>>()}, "uniqueItems": true})
|
||||
}
|
||||
Kind::NumberOrAuto { min, .. } => {
|
||||
let mut n = json!({"type": "number"});
|
||||
if let Some(lo) = min {
|
||||
n["minimum"] = json!(lo);
|
||||
}
|
||||
json!({"oneOf": [n, {"const": "auto"}]})
|
||||
}
|
||||
_ => json!({}),
|
||||
};
|
||||
s["title"] = json!(f.label);
|
||||
@@ -1113,6 +1212,40 @@ mod tests {
|
||||
.def("\"force\""),
|
||||
];
|
||||
|
||||
/// The typed lists check their values and describe themselves in JSON Schema (#169).
|
||||
#[test]
|
||||
fn labels_points_multi_and_auto_check() {
|
||||
const F: &[Field] = &[
|
||||
labels("nodes", "Nodes", LabelOf::Node),
|
||||
points("at", "Points", Dim::Length, 2),
|
||||
multi("dofs", "DOFs", &[("1", "UX"), ("2", "UY")]),
|
||||
multi("output", "Output", &[("\"stress\"", "Stress")]),
|
||||
number_or_auto("scale", "Scale", Dim::None, Some(0.0)),
|
||||
];
|
||||
assert!(problems(F, &json!({"nodes": [1, 5], "at": [[0, 0, 1e3]], "dofs": [2], "output": ["stress"], "scale": "auto"})).is_empty());
|
||||
assert!(problems(F, &json!({"scale": 2.5})).is_empty());
|
||||
for bad in [
|
||||
json!({"nodes": [0]}),
|
||||
json!({"nodes": [1.5]}),
|
||||
json!({"nodes": [u64::from(u32::MAX) + 1]}),
|
||||
json!({"at": [[0, 0]]}),
|
||||
json!({"at": [[0, 0, 0], [0, 0, 0], [0, 0, 0]]}),
|
||||
json!({"at": [[0, 0, "x"]]}),
|
||||
json!({"dofs": [3]}),
|
||||
json!({"dofs": ["1"]}),
|
||||
json!({"output": ["strain"]}),
|
||||
json!({"scale": -1}),
|
||||
json!({"scale": "big"}),
|
||||
] {
|
||||
assert_eq!(problems(F, &bad).len(), 1, "{bad}");
|
||||
}
|
||||
let s = json_schema(F);
|
||||
assert_eq!(s["properties"]["dofs"]["items"]["enum"], json!([1, 2]));
|
||||
assert_eq!(s["properties"]["output"]["items"]["enum"], json!(["stress"]));
|
||||
assert_eq!(s["properties"]["at"]["maxItems"], 2);
|
||||
assert_eq!(s["properties"]["scale"]["oneOf"][1]["const"], "auto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefill_switch_and_check() {
|
||||
let p = prefill(LOADISH, &json!({}));
|
||||
|
||||
Reference in new issue
Block a user