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.")
|
.help("A large-strain rubber model (nonlinear steps), built on the isotropic constants.")
|
||||||
.hint("large strain in nonlinear steps, from 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(
|
const THERMAL: Field = group(
|
||||||
"thermal",
|
"thermal",
|
||||||
"Thermal",
|
"Thermal",
|
||||||
&[
|
&[
|
||||||
json("conductivity", "Conductivity")
|
per_axis("conductivity", "Conductivity", D::Conductivity, Some(0.0), AXES_123)
|
||||||
.help("Thermal conductivity, one value or one per axis. Steel ≈ 50 W/(m·K), i.e. 50 in mm-t-s-N.")
|
.help("Thermal conductivity, one value or one per material axis. Steel ≈ 50 W/(m·K), i.e. 50 in mm-t-s-N."),
|
||||||
.hint("k or [k1, k2, k3]"),
|
|
||||||
positive("specific_heat", "Specific heat", D::SpecificHeat)
|
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."),
|
.help("Heat that raises a unit mass by one degree. Steel ≈ 460 J/(kg·K), 4.6e8 in mm-t-s."),
|
||||||
json("expansion", "Expansion")
|
per_axis("expansion", "Expansion", D::Expansion, None, AXES_123)
|
||||||
.help("Thermal expansion coefficient, one value or one per axis, in 1/K. Steel ≈ 1.2e-5, aluminium ≈ 2.3e-5.")
|
.help("Thermal expansion coefficient, one value or one per material axis. Steel ≈ 1.2e-5, aluminium ≈ 2.3e-5."),
|
||||||
.hint("α or [α1, α2, α3], 1/K"),
|
|
||||||
num("reference_temperature", "Reference temperature", D::Temperature).help("The temperature at which there is no thermal strain.").def("0"),
|
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] = &[
|
pub const MASS: &[Field] = &[
|
||||||
NAME,
|
NAME,
|
||||||
TARGET,
|
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"])
|
vector("inertia", "Inertia", D::Inertia, &["I11", "I22", "I33", "I12", "I13", "I23"])
|
||||||
.help("Rotary inertia about the reference point: three moments and three products.")
|
.help("Rotary inertia about the reference point: three moments and three products.")
|
||||||
.hint("about the node; needs a reference point"),
|
.hint("about the node; needs a reference point"),
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ pub enum Kind {
|
|||||||
dim: Dim,
|
dim: Dim,
|
||||||
min: Option<f64>,
|
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.
|
/// Any JSON: what the schema does not describe.
|
||||||
Json,
|
Json,
|
||||||
}
|
}
|
||||||
@@ -390,7 +397,11 @@ impl Field {
|
|||||||
/// Its physical dimension, for a number or a vector of numbers.
|
/// Its physical dimension, for a number or a vector of numbers.
|
||||||
pub fn dim(&self) -> Option<Dim> {
|
pub fn dim(&self) -> Option<Dim> {
|
||||||
match self.kind {
|
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,
|
_ => 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 {
|
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 })
|
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 {
|
pub const fn json(key: &'static str, label: &'static str) -> Field {
|
||||||
field(key, label, Kind::Json)
|
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(_)) => Some(ProblemKind::Below(min.unwrap_or(0.0))),
|
||||||
_ => Some(ProblemKind::Invalid("a number or \"auto\"".into())),
|
_ => 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() {
|
Kind::Rows { fields, max } => match v.as_array() {
|
||||||
Some(a) if a.len() <= max => {
|
Some(a) if a.len() <= max => {
|
||||||
for (i, row) in a.iter().enumerate() {
|
for (i, row) in a.iter().enumerate() {
|
||||||
@@ -1172,6 +1199,18 @@ fn field_schema(f: &Field, depth: usize) -> Value {
|
|||||||
}
|
}
|
||||||
json!({"oneOf": [n, {"const": "auto"}]})
|
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!({}),
|
_ => json!({}),
|
||||||
};
|
};
|
||||||
s["title"] = json!(f.label);
|
s["title"] = json!(f.label);
|
||||||
@@ -1270,6 +1309,19 @@ mod tests {
|
|||||||
assert_eq!(s["properties"]["path"]["items"]["x-columns"], json!(["fraction", "factor"]));
|
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]
|
#[test]
|
||||||
fn prefill_switch_and_check() {
|
fn prefill_switch_and_check() {
|
||||||
let p = prefill(LOADISH, &json!({}));
|
let p = prefill(LOADISH, &json!({}));
|
||||||
|
|||||||
@@ -189,6 +189,14 @@ fn scalar(f: &Field, v: &Value, units: UnitSystem) -> String {
|
|||||||
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()))
|
||||||
}
|
}
|
||||||
|
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 {
|
Kind::NumberOrAuto { .. } => match v {
|
||||||
Value::String(s) => s.clone(),
|
Value::String(s) => s.clone(),
|
||||||
_ => v.as_f64().map_or_else(|| crate::context::value_text(v), |x| with_unit(x, unit.as_deref())),
|
_ => 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::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::PerAxis { labels, .. } => self.per_axis(ui, id, value, f, labels),
|
||||||
Kind::Unit | Kind::Group(_) | Kind::Variants { .. } | Kind::Rows { .. } => None,
|
Kind::Unit | Kind::Group(_) | Kind::Variants { .. } | Kind::Rows { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -715,6 +716,39 @@ impl Form<'_> {
|
|||||||
out
|
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.
|
/// A JSON value typed as text: kept while it does not parse.
|
||||||
/// Empty, it shows `hint` (the expected format) greyed.
|
/// Empty, it shows `hint` (the expected format) greyed.
|
||||||
fn json(&mut self, ui: &mut Ui, id: Id, value: Option<&Value>, hint: &str) -> Option<Value> {
|
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 {
|
pub fn has_system_units(fields: &[Field], depth: usize) -> bool {
|
||||||
depth <= MAX_DEPTH
|
depth <= MAX_DEPTH
|
||||||
&& fields.iter().any(|f| match f.kind {
|
&& 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::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)),
|
Kind::Variants { variants, .. } => variants.iter().any(|v| has_system_units(v.fields, depth + 1)),
|
||||||
_ => false,
|
_ => false,
|
||||||
|
|||||||
Reference in new issue
Block a user