graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: the row menu reads out the declared min, max and step
`Min:` / `Max:` / `Step:` ahead of `Range:` for a slider, float3 or
spinbox — what the template DECLARES (ParamDef::declared_range, an
inline `slider:-2:2` counting as both ends), `none` where it declares
nothing and the pane's default stands in. Range stays the clamp the pane
applies, now derived from the same declared_range, so the three rows
above it say how much of that clamp the template chose.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 10 ++++++----
src/app.rs | 11 ++++++++++-
src/main.rs | 19 +++++++++++++------
src/param.rs | 33 ++++++++++++++++++++++-----------
4 files changed, 51 insertions(+), 22 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index eb0bc58..1606cf1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -731,10 +731,12 @@ the template's value as written there
(`State::template_default`, which takes a subnet template's override for
a child inside an instance — the Embryo's `sphere1` defaults its Radius to
`chf("../Radius")` — and is absent for a parameter no template names),
-then `Range: lo..hi` with its step for a slider, float3 or
-spinbox (`ParamDef::range`, the numbers `param_display` builds the pane's
-row from — an inline `slider:-2:2`, else the template's min/max, else
-the pane's defaults), `Options: a, b, c` for a choice, and `Shown when:`
+then for a slider, float3 or spinbox `Min:` / `Max:` / `Step:` as the
+template DECLARES them (`ParamDef::declared_range`, an inline
+`slider:-2:2` included, `none` where it says nothing) followed by
+`Range: lo..hi` with its step as the pane APPLIES it (`ParamDef::range`,
+the numbers `param_display` builds the pane's row from — the declared
+ends, else the pane's defaults), `Options: a, b, c` for a choice, and `Shown when:`
with the row's `show_when` condition when it has one. Control and Value
are two lines because
they answer different questions, the template's and the instance's, and
diff --git a/src/app.rs b/src/app.rs
index 9e3ec28..cf16271 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4466,7 +4466,11 @@ impl State {
/// expression), left out for a parameter no template names. A control
/// with a range adds `Range: lo..hi`, with its step when one is set
/// (`ParamDef::range`, the pane's own numbers); a choice adds
- /// `Options: a, b, c`. `Expression:` is the row's expression FLAG,
+ /// `Options: a, b, c`. Ahead of `Range:`, `Min:` / `Max:` / `Step:`
+ /// are what the template DECLARES (`ParamDef::declared_range`, an
+ /// inline `slider:-2:2` included), `none` where it declares nothing —
+ /// so Range is the clamp the pane applies and the three above it say
+ /// how much of that the template chose. `Expression:` is the row's expression FLAG,
/// `true` or `false` — the thing `ParamDef::expr` stores, which is
/// what Edit Expression sets and Delete Expression clears; `Value:`
/// already reads `expression` when it is set, and the flag row states
@@ -4496,6 +4500,11 @@ impl State {
}
if let Some((lo, hi, step)) = param.and_then(|p| p.range()) {
let fmt = |v: f32| crate::expr::fmt_num(v as f64);
+ let declared = |v: Option<f32>| v.map(fmt).unwrap_or_else(|| "none".to_string());
+ let (min, max, dstep) = param.map(|p| p.declared_range()).unwrap_or_default();
+ options.push(format!("Min: {}", declared(min)));
+ options.push(format!("Max: {}", declared(max)));
+ options.push(format!("Step: {}", declared(dstep)));
let step = step.map(|s| format!(", step {}", fmt(s))).unwrap_or_default();
options.push(format!("Range: {}..{}{step}", fmt(lo), fmt(hi)));
}
diff --git a/src/main.rs b/src/main.rs
index c26143c..e083ccb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7756,7 +7756,7 @@ mod tests {
assert!(!state.viewport_menu_open());
assert_eq!(
state.param_menu_actions,
- vec![ParamMenuAction::Info; 6].into_iter().chain([ParamMenuAction::Separator, ParamMenuAction::CopyParameter, ParamMenuAction::Separator, ParamMenuAction::EditExpression]).collect::<Vec<_>>(),
+ vec![ParamMenuAction::Info; 9].into_iter().chain([ParamMenuAction::Separator, ParamMenuAction::CopyParameter, ParamMenuAction::Separator, ParamMenuAction::EditExpression]).collect::<Vec<_>>(),
"nothing copied yet, and the row holds a value"
);
// The header rows read the parameter out: its name, its control's
@@ -7767,11 +7767,15 @@ mod tests {
// row appears.
let shown = cce_ui::widget::context_menu::options();
assert_eq!(
- &shown[..7],
- &["Name: Radius".to_string(), "Control: slider".to_string(), "Value: float".to_string(), "Expression: false".to_string(), "Default: 0.5".to_string(), "Range: 0..2".to_string(), "-".to_string()]
+ &shown[..10],
+ &[
+ "Name: Radius".to_string(), "Control: slider".to_string(), "Value: float".to_string(), "Expression: false".to_string(),
+ "Default: 0.5".to_string(), "Min: none".to_string(), "Max: none".to_string(), "Step: none".to_string(),
+ "Range: 0..2".to_string(), "-".to_string(),
+ ]
);
let (_, _, headers) = state.param_menu_rows(sphere, "Radius");
- assert_eq!(headers, 6);
+ assert_eq!(headers, 9);
// The headers are the rows before the separator; each one is
// looked up by its readout, not its position.
let headers_of = |state: &State, pname: &str| -> Vec<String> {
@@ -7781,9 +7785,12 @@ mod tests {
};
// An inline range, a spinbox's range and step, a choice's options,
// and a conditional row's condition.
- assert!(headers_of(&state, "Center X").contains(&"Range: -2..2".to_string()));
+ let rows = headers_of(&state, "Center X");
+ for want in ["Min: -2", "Max: 2", "Step: none", "Range: -2..2"] {
+ assert!(rows.contains(&want.to_string()), "{want} missing from {rows:?}");
+ }
let rows = headers_of(&state, "Rows");
- for want in ["Default: 16", "Range: 2..128, step 1", "Shown when: Method == UV"] {
+ for want in ["Default: 16", "Min: 2", "Max: 128", "Step: 1", "Range: 2..128, step 1", "Shown when: Method == UV"] {
assert!(rows.contains(&want.to_string()), "{want} missing from {rows:?}");
}
let rows = headers_of(&state, "Method");
diff --git a/src/param.rs b/src/param.rs
index 10deb1f..b1067b5 100644
--- a/src/param.rs
+++ b/src/param.rs
@@ -453,25 +453,36 @@ impl ParamDef {
/// row from this and the row menu's `Range:` reads it, so the two
/// cannot disagree. `None` for a kind with no range.
pub fn range(&self) -> Option<(f32, f32, Option<f32>)> {
- let inline = || {
- let parts: Vec<&str> = self.param_type.split(':').collect();
- match parts[..] {
- [_, lo, hi] => Some((lo.trim().parse::<f32>().ok()?, hi.trim().parse::<f32>().ok()?)),
- _ => None,
- }
- };
let (lo, hi) = match self.kind() {
ParamKind::Slider => (0.0, 2.0),
ParamKind::Float3 => (-10.0, 10.0),
ParamKind::Spin => (1.0, 10000.0),
_ => return None,
};
- let (lo, hi) = inline().unwrap_or((self.min.unwrap_or(lo), self.max.unwrap_or(hi)));
+ let (min, max, step) = self.declared_range();
let step = match self.kind() {
- ParamKind::Spin => Some(self.step.unwrap_or(1.0)),
- _ => self.step,
+ ParamKind::Spin => Some(step.unwrap_or(1.0)),
+ _ => step,
};
- Some((lo, hi, step))
+ Some((min.unwrap_or(lo), max.unwrap_or(hi), step))
+ }
+
+ /// The `(min, max, step)` the template DECLARES for this parameter —
+ /// an inline `slider:-2:2` counts as declaring both ends — each `None`
+ /// where it says nothing and the pane's default stands in
+ /// ([`ParamDef::range`] is the result). What the row menu's `Min:` /
+ /// `Max:` / `Step:` read, so a `none` there means the template left it
+ /// to the pane.
+ pub fn declared_range(&self) -> (Option<f32>, Option<f32>, Option<f32>) {
+ let parts: Vec<&str> = self.param_type.split(':').collect();
+ let inline = match parts[..] {
+ [_, lo, hi] => lo.trim().parse::<f32>().ok().zip(hi.trim().parse::<f32>().ok()),
+ _ => None,
+ };
+ match inline {
+ Some((lo, hi)) => (Some(lo), Some(hi), self.step),
+ None => (self.min, self.max, self.step),
+ }
}
/// Whether a value that READS as a reference should become an