forked from 1b-agent/fieldcraft-upstream
ui: one number or one per axis for conductivity, expansion and point mass
A PerAxis field kind: one value for every direction, or Per axis ticked for one per axis. Thermal conductivity and expansion and the point mass 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
b18195ff89
commit
e4d1f6c7da
4 files changed
+106
-9
No files matched your search
@@ -186,18 +186,19 @@ const HYPERELASTIC: Field = variants(
|
||||
.help("A large-strain rubber model (nonlinear steps), built on the isotropic constants.")
|
||||
.hint("large strain in nonlinear steps, from the isotropic constants");
|
||||
|
||||
/// The material axes 1, 2, 3 (the global X, Y, Z until material orientations land).
|
||||
const AXES_123: &[&str] = &["1", "2", "3"];
|
||||
|
||||
const THERMAL: Field = group(
|
||||
"thermal",
|
||||
"Thermal",
|
||||
&[
|
||||
json("conductivity", "Conductivity")
|
||||
.help("Thermal conductivity, one value or one per axis. Steel ≈ 50 W/(m·K), i.e. 50 in mm-t-s-N.")
|
||||
.hint("k or [k1, k2, k3]"),
|
||||
per_axis("conductivity", "Conductivity", D::Conductivity, Some(0.0), AXES_123)
|
||||
.help("Thermal conductivity, one value or one per material axis. Steel ≈ 50 W/(m·K), i.e. 50 in mm-t-s-N."),
|
||||
positive("specific_heat", "Specific heat", D::SpecificHeat)
|
||||
.help("Heat that raises a unit mass by one degree. Steel ≈ 460 J/(kg·K), 4.6e8 in mm-t-s."),
|
||||
json("expansion", "Expansion")
|
||||
.help("Thermal expansion coefficient, one value or one per axis, in 1/K. Steel ≈ 1.2e-5, aluminium ≈ 2.3e-5.")
|
||||
.hint("α or [α1, α2, α3], 1/K"),
|
||||
per_axis("expansion", "Expansion", D::Expansion, None, AXES_123)
|
||||
.help("Thermal expansion coefficient, one value or one per material axis. Steel ≈ 1.2e-5, aluminium ≈ 2.3e-5."),
|
||||
num("reference_temperature", "Reference temperature", D::Temperature).help("The temperature at which there is no thermal strain.").def("0"),
|
||||
],
|
||||
)
|
||||
@@ -371,7 +372,9 @@ pub const COUPLING: &[Field] = &[
|
||||
pub const MASS: &[Field] = &[
|
||||
NAME,
|
||||
TARGET,
|
||||
positive("mass", "Mass", D::Mass).help("A lumped mass at each node of the target.").hint("per node; [mx, my, mz] in JSON"),
|
||||
per_axis("mass", "Mass", D::Mass, Some(0.0), XYZ)
|
||||
.help("A lumped mass at each node of the target: one value, or one per direction X, Y, Z.")
|
||||
.hint("per node"),
|
||||
vector("inertia", "Inertia", D::Inertia, &["I11", "I22", "I33", "I12", "I13", "I23"])
|
||||
.help("Rotary inertia about the reference point: three moments and three products.")
|
||||
.hint("about the node; needs a reference point"),
|
||||
|
||||
@@ -112,6 +112,13 @@ pub enum Kind {
|
||||
dim: Dim,
|
||||
min: Option<f64>,
|
||||
},
|
||||
/// One number for every direction, or one per axis (`labels`, e.g. the material axes
|
||||
/// 1, 2, 3), each at least `min`.
|
||||
PerAxis {
|
||||
dim: Dim,
|
||||
min: Option<f64>,
|
||||
labels: &'static [&'static str],
|
||||
},
|
||||
/// Any JSON: what the schema does not describe.
|
||||
Json,
|
||||
}
|
||||
@@ -390,7 +397,11 @@ 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, .. } | Kind::Points { dim, .. } | Kind::NumberOrAuto { dim, .. } => Some(dim),
|
||||
Kind::Number { dim, .. }
|
||||
| Kind::Vector { dim, .. }
|
||||
| Kind::Points { dim, .. }
|
||||
| Kind::NumberOrAuto { dim, .. }
|
||||
| Kind::PerAxis { dim, .. } => Some(dim),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -509,6 +520,10 @@ pub const fn multi(key: &'static str, label: &'static str, options: &'static [(&
|
||||
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 })
|
||||
}
|
||||
/// One number at least `min`, or one per axis of `labels`.
|
||||
pub const fn per_axis(key: &'static str, label: &'static str, dim: Dim, min: Option<f64>, labels: &'static [&'static str]) -> Field {
|
||||
field(key, label, Kind::PerAxis { dim, min, labels })
|
||||
}
|
||||
pub const fn json(key: &'static str, label: &'static str) -> Field {
|
||||
field(key, label, Kind::Json)
|
||||
}
|
||||
@@ -1045,6 +1060,18 @@ fn value_problem(f: &'static Field, v: &Value, path: &str, out: &mut Vec<Problem
|
||||
(_, Some(_)) => Some(ProblemKind::Below(min.unwrap_or(0.0))),
|
||||
_ => Some(ProblemKind::Invalid("a number or \"auto\"".into())),
|
||||
},
|
||||
Kind::PerAxis { min, labels, .. } => {
|
||||
let values = match v {
|
||||
Value::Array(a) if a.len() == labels.len() => a.iter().map(|x| x.as_f64().filter(|x| x.is_finite())).collect::<Option<Vec<_>>>(),
|
||||
Value::Array(_) => None,
|
||||
_ => v.as_f64().filter(|x| x.is_finite()).map(|x| vec![x]),
|
||||
};
|
||||
match values {
|
||||
Some(xs) if xs.iter().all(|&x| min.is_none_or(|lo| x >= lo)) => None,
|
||||
Some(_) => Some(ProblemKind::Below(min.unwrap_or(0.0))),
|
||||
None => Some(ProblemKind::Invalid(format!("a number, or {} numbers (one per axis)", labels.len()))),
|
||||
}
|
||||
}
|
||||
Kind::Rows { fields, max } => match v.as_array() {
|
||||
Some(a) if a.len() <= max => {
|
||||
for (i, row) in a.iter().enumerate() {
|
||||
@@ -1172,6 +1199,18 @@ fn field_schema(f: &Field, depth: usize) -> Value {
|
||||
}
|
||||
json!({"oneOf": [n, {"const": "auto"}]})
|
||||
}
|
||||
Kind::PerAxis { dim, min, labels } => {
|
||||
let mut n = json!({"type": "number"});
|
||||
if let Some(lo) = min {
|
||||
n["minimum"] = json!(lo);
|
||||
}
|
||||
let mut s =
|
||||
json!({"oneOf": [n.clone(), {"type": "array", "items": n, "minItems": labels.len(), "maxItems": labels.len(), "x-labels": labels}]});
|
||||
if dim != Dim::None {
|
||||
s["x-dimension"] = json!(format!("{dim:?}").to_lowercase());
|
||||
}
|
||||
s
|
||||
}
|
||||
_ => json!({}),
|
||||
};
|
||||
s["title"] = json!(f.label);
|
||||
@@ -1270,6 +1309,19 @@ mod tests {
|
||||
assert_eq!(s["properties"]["path"]["items"]["x-columns"], json!(["fraction", "factor"]));
|
||||
}
|
||||
|
||||
/// A per-axis field takes one number or one per axis, each at least its minimum.
|
||||
#[test]
|
||||
fn per_axis_takes_one_number_or_one_per_axis() {
|
||||
const F: &[Field] = &[per_axis("k", "K", Dim::Conductivity, Some(0.0), &["1", "2", "3"])];
|
||||
assert!(problems(F, &json!({"k": 50})).is_empty());
|
||||
assert!(problems(F, &json!({"k": [50, 20, 20.5]})).is_empty());
|
||||
for bad in [json!({"k": -1}), json!({"k": [1, 2]}), json!({"k": [1, 2, -3]}), json!({"k": "x"}), json!({"k": [1, 2, "3"]})] {
|
||||
assert_eq!(problems(F, &bad).len(), 1, "{bad}");
|
||||
}
|
||||
let s = json_schema(F);
|
||||
assert_eq!(s["properties"]["k"]["oneOf"][1]["maxItems"], 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefill_switch_and_check() {
|
||||
let p = prefill(LOADISH, &json!({}));
|
||||
|
||||
@@ -189,6 +189,14 @@ fn scalar(f: &Field, v: &Value, units: UnitSystem) -> String {
|
||||
Kind::Multi(options) => {
|
||||
list(v, |x| options.iter().find(|(o, _)| fieldcraft_engine::schema::multi_value(o) == *x).map(|(_, l)| (*l).to_string()))
|
||||
}
|
||||
Kind::PerAxis { .. } => match v {
|
||||
Value::Array(a) => a
|
||||
.iter()
|
||||
.map(|x| x.as_f64().map_or_else(|| crate::context::value_text(x), |x| with_unit(x, unit.as_deref())))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
_ => v.as_f64().map_or_else(|| crate::context::value_text(v), |x| with_unit(x, unit.as_deref())),
|
||||
},
|
||||
Kind::NumberOrAuto { .. } => match v {
|
||||
Value::String(s) => s.clone(),
|
||||
_ => v.as_f64().map_or_else(|| crate::context::value_text(v), |x| with_unit(x, unit.as_deref())),
|
||||
|
||||
@@ -333,6 +333,7 @@ impl Form<'_> {
|
||||
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),
|
||||
Kind::PerAxis { labels, .. } => self.per_axis(ui, id, value, f, labels),
|
||||
Kind::Unit | Kind::Group(_) | Kind::Variants { .. } | Kind::Rows { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -715,6 +716,39 @@ impl Form<'_> {
|
||||
out
|
||||
}
|
||||
|
||||
/// One number, or with Per axis ticked one per axis: ticking it copies the number to every
|
||||
/// axis, unticking keeps the first axis's value.
|
||||
fn per_axis(&mut self, ui: &mut Ui, id: Id, value: Option<&Value>, f: &Field, labels: &[&str]) -> Option<Value> {
|
||||
let split = matches!(value, Some(Value::Array(_)));
|
||||
let mut on = split;
|
||||
let mut out = None;
|
||||
ui.vertical(|ui| {
|
||||
if ui.checkbox(&mut on, "Per axis").on_hover_text("A different value along each axis").changed() {
|
||||
out = Some(match (on, value) {
|
||||
(true, Some(v)) if v.is_number() => Value::Array(vec![v.clone(); labels.len()]),
|
||||
(true, _) => Value::Null,
|
||||
(false, Some(Value::Array(a))) => a.first().cloned().unwrap_or(Value::Null),
|
||||
(false, _) => Value::Null,
|
||||
});
|
||||
}
|
||||
if split {
|
||||
if let Some(v) = self.vector(ui, id.with("axes"), value, f, labels, false, None) {
|
||||
out = Some(v);
|
||||
}
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
let unit = f.unit_label(self.cx.units);
|
||||
let width = (fill(ui) - fields::unit_width(ui, unit.as_deref())).max(FIELD_MIN);
|
||||
if let Some(v) = self.number(ui, id, value, f.hint, false, width) {
|
||||
out = Some(v);
|
||||
}
|
||||
fields::unit(ui, &self.style, unit.as_deref());
|
||||
});
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// A JSON value typed as text: kept while it does not parse.
|
||||
/// Empty, it shows `hint` (the expected format) greyed.
|
||||
fn json(&mut self, ui: &mut Ui, id: Id, value: Option<&Value>, hint: &str) -> Option<Value> {
|
||||
@@ -844,7 +878,7 @@ fn caption(component: &str) -> String {
|
||||
pub fn has_system_units(fields: &[Field], depth: usize) -> bool {
|
||||
depth <= MAX_DEPTH
|
||||
&& fields.iter().any(|f| match f.kind {
|
||||
Kind::Number { dim, .. } | Kind::Vector { dim, .. } => dim.follows_system(),
|
||||
Kind::Number { dim, .. } | Kind::Vector { dim, .. } | Kind::PerAxis { dim, .. } => dim.follows_system(),
|
||||
Kind::Group(inner) | Kind::Rows { fields: inner, .. } | Kind::Either(inner) => has_system_units(inner, depth + 1),
|
||||
Kind::Variants { variants, .. } => variants.iter().any(|v| has_system_units(v.fields, depth + 1)),
|
||||
_ => false,
|
||||
|
||||
Reference in new issue
Block a user