forked from 1b-agent/fieldcraft-upstream
ui: nonlinear load path as a table of fraction and factor
Kind::Points gains its columns, so a list of number rows other than [x, y, z] gets the same typed editor; the load path uses it (#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
1c18a463e7
commit
61f0d42ec4
4 files changed
+50
-18
No files matched your search
@@ -661,9 +661,9 @@ const PROCEDURE: Field = variants(
|
|||||||
flag("line_search", "Line search")
|
flag("line_search", "Line search")
|
||||||
.help("Scale each Newton correction to reduce the residual; helps with hard convergence.")
|
.help("Scale each Newton correction to reduce the residual; helps with hard convergence.")
|
||||||
.def("false"),
|
.def("false"),
|
||||||
json("load_path", "Load path")
|
table("load_path", "Load path", D::None, &["fraction", "factor"], fieldcraft_model::MAX_LOAD_PATH_POINTS)
|
||||||
.help("How the load factor varies through the step, as JSON pairs of step fraction and factor; a linear ramp when empty.")
|
.help("How the load factor varies through the step: rows of step fraction (0 to 1) and load factor; a linear ramp when empty.")
|
||||||
.hint("[[fraction, factor], …]; a ramp when empty"),
|
.hint("a ramp when empty"),
|
||||||
group(
|
group(
|
||||||
"imperfection",
|
"imperfection",
|
||||||
"Imperfection",
|
"Imperfection",
|
||||||
|
|||||||
@@ -97,9 +97,11 @@ pub enum Kind {
|
|||||||
},
|
},
|
||||||
/// A list of mesh labels, nodes or elements (at most [`MAX_LABELS`]).
|
/// A list of mesh labels, nodes or elements (at most [`MAX_LABELS`]).
|
||||||
Labels(LabelOf),
|
Labels(LabelOf),
|
||||||
/// A list of points `[x, y, z]`, at most `max`.
|
/// A list of points `[x, y, z]`, or of number rows with these `columns` (a load path's
|
||||||
|
/// `[fraction, factor]` pairs), at most `max`.
|
||||||
Points {
|
Points {
|
||||||
dim: Dim,
|
dim: Dim,
|
||||||
|
columns: &'static [&'static str],
|
||||||
max: usize,
|
max: usize,
|
||||||
},
|
},
|
||||||
/// Any of these values, as a list: (the value as JSON text, label), e.g. `("1", "UX")` or
|
/// Any of these values, as a list: (the value as JSON text, label), e.g. `("1", "UX")` or
|
||||||
@@ -489,9 +491,15 @@ 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 {
|
pub const fn labels(key: &'static str, label: &'static str, of: LabelOf) -> Field {
|
||||||
field(key, label, Kind::Labels(of))
|
field(key, label, Kind::Labels(of))
|
||||||
}
|
}
|
||||||
|
/// The columns of a point, `[x, y, z]`.
|
||||||
|
pub const XYZ: &[&str] = &["x", "y", "z"];
|
||||||
/// A list of points `[x, y, z]`.
|
/// A list of points `[x, y, z]`.
|
||||||
pub const fn points(key: &'static str, label: &'static str, dim: Dim, max: usize) -> Field {
|
pub const fn points(key: &'static str, label: &'static str, dim: Dim, max: usize) -> Field {
|
||||||
field(key, label, Kind::Points { dim, max })
|
field(key, label, Kind::Points { dim, columns: XYZ, max })
|
||||||
|
}
|
||||||
|
/// A table: a list of number rows with these columns.
|
||||||
|
pub const fn table(key: &'static str, label: &'static str, dim: Dim, columns: &'static [&'static str], max: usize) -> Field {
|
||||||
|
field(key, label, Kind::Points { dim, columns, max })
|
||||||
}
|
}
|
||||||
/// Any of `options` (each a value as JSON text, and a label), as a list.
|
/// 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 {
|
pub const fn multi(key: &'static str, label: &'static str, options: &'static [(&'static str, &'static str)]) -> Field {
|
||||||
@@ -1016,14 +1024,16 @@ fn value_problem(f: &'static Field, v: &Value, path: &str, out: &mut Vec<Problem
|
|||||||
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(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" }),
|
_ => Some(ProblemKind::NotAList { max: MAX_LABELS, what: "labels" }),
|
||||||
},
|
},
|
||||||
Kind::Points { max, .. } => match v.as_array() {
|
Kind::Points { columns, max, .. } => match v.as_array() {
|
||||||
Some(a)
|
Some(a)
|
||||||
if a.len() <= max
|
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)))) =>
|
&& a.iter().all(|p| {
|
||||||
|
p.as_array().is_some_and(|c| c.len() == columns.len() && c.iter().all(|x| x.as_f64().is_some_and(f64::is_finite)))
|
||||||
|
}) =>
|
||||||
{
|
{
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
_ => Some(ProblemKind::NotAList { max, what: "points [x, y, z]" }),
|
_ => Some(ProblemKind::NotAList { max, what: if columns == XYZ { "points [x, y, z]" } else { "rows of numbers" } }),
|
||||||
},
|
},
|
||||||
Kind::Multi(options) => match v.as_array() {
|
Kind::Multi(options) => match v.as_array() {
|
||||||
Some(a) if a.iter().all(|x| options.iter().any(|(o, _)| multi_value(o) == *x)) => None,
|
Some(a) if a.iter().all(|x| options.iter().any(|(o, _)| multi_value(o) == *x)) => None,
|
||||||
@@ -1144,8 +1154,9 @@ fn field_schema(f: &Field, depth: usize) -> Value {
|
|||||||
Kind::Labels(of) => {
|
Kind::Labels(of) => {
|
||||||
json!({"type": "array", "items": {"type": "integer", "minimum": 1, "maximum": u32::MAX}, "maxItems": MAX_LABELS, "x-labels": of.label()})
|
json!({"type": "array", "items": {"type": "integer", "minimum": 1, "maximum": u32::MAX}, "maxItems": MAX_LABELS, "x-labels": of.label()})
|
||||||
}
|
}
|
||||||
Kind::Points { dim, max } => {
|
Kind::Points { dim, columns, max } => {
|
||||||
let mut item = json!({"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3});
|
let n = columns.len();
|
||||||
|
let mut item = json!({"type": "array", "items": {"type": "number"}, "minItems": n, "maxItems": n, "x-columns": columns});
|
||||||
if dim != Dim::None {
|
if dim != Dim::None {
|
||||||
item["x-dimension"] = json!(format!("{dim:?}").to_lowercase());
|
item["x-dimension"] = json!(format!("{dim:?}").to_lowercase());
|
||||||
}
|
}
|
||||||
@@ -1246,6 +1257,19 @@ mod tests {
|
|||||||
assert_eq!(s["properties"]["scale"]["oneOf"][1]["const"], "auto");
|
assert_eq!(s["properties"]["scale"]["oneOf"][1]["const"], "auto");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A table is a list of number rows of its column count: a load path's [fraction, factor].
|
||||||
|
#[test]
|
||||||
|
fn table_rows_have_its_columns() {
|
||||||
|
const F: &[Field] = &[table("path", "Path", Dim::None, &["fraction", "factor"], 3)];
|
||||||
|
assert!(problems(F, &json!({"path": [[0, 0], [0.5, 1], [1, 1]]})).is_empty());
|
||||||
|
for bad in [json!({"path": [[0, 0, 0]]}), json!({"path": [[0]]}), json!({"path": [[0, 0], [0, 0], [0, 0], [1, 1]]})] {
|
||||||
|
assert_eq!(problems(F, &bad).len(), 1, "{bad}");
|
||||||
|
}
|
||||||
|
let s = json_schema(F);
|
||||||
|
assert_eq!(s["properties"]["path"]["items"]["minItems"], 2);
|
||||||
|
assert_eq!(s["properties"]["path"]["items"]["x-columns"], json!(["fraction", "factor"]));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prefill_switch_and_check() {
|
fn prefill_switch_and_check() {
|
||||||
let p = prefill(LOADISH, &json!({}));
|
let p = prefill(LOADISH, &json!({}));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! choices, so a definition is described by the same words as the form that edits it, never as
|
//! choices, so a definition is described by the same words as the form that edits it, never as
|
||||||
//! its JSON.
|
//! its JSON.
|
||||||
|
|
||||||
use fieldcraft_engine::schema::{Field, Kind};
|
use fieldcraft_engine::schema::{self, Field, Kind};
|
||||||
use fieldcraft_model::{Model, UnitSystem};
|
use fieldcraft_model::{Model, UnitSystem};
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
@@ -183,7 +183,9 @@ fn scalar(f: &Field, v: &Value, units: UnitSystem) -> String {
|
|||||||
Some(l) => crate::fields::format_labels(&l),
|
Some(l) => crate::fields::format_labels(&l),
|
||||||
None => crate::context::value_text(v),
|
None => crate::context::value_text(v),
|
||||||
},
|
},
|
||||||
Kind::Points { .. } => v.as_array().map_or_else(|| crate::context::value_text(v), |a| plural(a.len(), "point")),
|
Kind::Points { columns, .. } => {
|
||||||
|
v.as_array().map_or_else(|| crate::context::value_text(v), |a| plural(a.len(), if columns == schema::XYZ { "point" } else { "row" }))
|
||||||
|
}
|
||||||
Kind::Multi(options) => {
|
Kind::Multi(options) => {
|
||||||
list(v, |x| options.iter().find(|(o, _)| fieldcraft_engine::schema::multi_value(o) == *x).map(|(_, l)| (*l).to_string()))
|
list(v, |x| options.iter().find(|(o, _)| fieldcraft_engine::schema::multi_value(o) == *x).map(|(_, l)| (*l).to_string()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ impl Form<'_> {
|
|||||||
}
|
}
|
||||||
Kind::Json => self.json(ui, id, value, f.hint),
|
Kind::Json => self.json(ui, id, value, f.hint),
|
||||||
Kind::Labels(of) => self.labels(ui, id, value, of, f.hint),
|
Kind::Labels(of) => self.labels(ui, id, value, of, f.hint),
|
||||||
Kind::Points { max, .. } => self.points(ui, id, value, f, max),
|
Kind::Points { columns, max, .. } => self.points(ui, id, value, f, columns, max),
|
||||||
Kind::Multi(options) => self.multi(ui, value, options),
|
Kind::Multi(options) => self.multi(ui, value, options),
|
||||||
Kind::NumberOrAuto { .. } => self.number_or_auto(ui, id, value, f),
|
Kind::NumberOrAuto { .. } => self.number_or_auto(ui, id, value, f),
|
||||||
Kind::Unit | Kind::Group(_) | Kind::Variants { .. } | Kind::Rows { .. } => None,
|
Kind::Unit | Kind::Group(_) | Kind::Variants { .. } | Kind::Rows { .. } => None,
|
||||||
@@ -584,7 +584,7 @@ impl Form<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A list of points: a row of x, y, z each, removable, and Add point.
|
/// 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, max: usize) -> Option<Value> {
|
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 {
|
let current: Vec<Value> = match value {
|
||||||
None | Some(Value::Null) => Vec::new(),
|
None | Some(Value::Null) => Vec::new(),
|
||||||
Some(Value::Array(a)) if a.iter().all(Value::is_array) => a.clone(),
|
Some(Value::Array(a)) if a.iter().all(Value::is_array) => a.clone(),
|
||||||
@@ -602,10 +602,10 @@ impl Form<'_> {
|
|||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
for (i, p) in current.iter().enumerate() {
|
for (i, p) in current.iter().enumerate() {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.small_button("×").on_hover_text("Remove this point").clicked() {
|
if ui.small_button("×").on_hover_text("Remove this row").clicked() {
|
||||||
remove = Some(i);
|
remove = Some(i);
|
||||||
}
|
}
|
||||||
if let Some(v) = self.vector(ui, id.with(i), Some(p), f, &["x", "y", "z"], false)
|
if let Some(v) = self.vector(ui, id.with(i), Some(p), f, columns, false)
|
||||||
&& let Some(slot) = next.get_mut(i)
|
&& let Some(slot) = next.get_mut(i)
|
||||||
{
|
{
|
||||||
*slot = v;
|
*slot = v;
|
||||||
@@ -613,8 +613,14 @@ impl Form<'_> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ui.add_enabled(current.len() < max.min(MAX_ROWS), egui::Button::new(RichText::new("Add point").small())).clicked() {
|
if ui
|
||||||
next.push(json!([0.0, 0.0, 0.0]));
|
.add_enabled(
|
||||||
|
current.len() < max.min(MAX_ROWS),
|
||||||
|
egui::Button::new(RichText::new(if columns == schema::XYZ { "Add point" } else { "Add row" }).small()),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
next.push(Value::Array(vec![json!(0.0); columns.len()]));
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user