graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: the row menu reads out the parameter's template default
A `Default:` header row after `Value:`, holding the template's text for
the row as written there — an expression shows as the expression.
State::template_default resolves it: a child of a subnet instance takes
the SUBNET template's override (the Embryo's sphere1 defaults its Radius
to chf("../Radius"), which is what the merge refreshes it from), any
other node its own template's parameter, and a parameter no template
names (one added over MCP) has no row. template_for is hoisted out of
merge_template_defs to module level so the lookup and the merge share
the one rule for which template an instance came from.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 6 ++++-
src/app.rs | 77 ++++++++++++++++++++++++++++++++++++++++++-------------------
src/main.rs | 36 +++++++++++++++++++++--------
3 files changed, 84 insertions(+), 35 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index cff34d9..2e5af34 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -721,7 +721,11 @@ kind the template gave it (`ParamKind::name` — slider, float3,
attribute…) and `Value:` what its text holds right now
(`ParamDef::value_type` — float, integer, vec3, boolean, enum, string,
expression, or invalid with the reason; a programmer's terms, by
-request), then `Range: lo..hi` with its step for a slider, float3 or
+request), `Default:` 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) and `Options: a, b, c` for a choice. Two lines because
diff --git a/src/app.rs b/src/app.rs
index ee7f246..0c661bf 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -606,6 +606,32 @@ pub fn flatten_node_templates(root: &FsNode) -> Vec<NodeTemplate> {
out
}
+/// The template an instance came from: a native node by TYPE, a subnet
+/// instance by NAME with its index stripped ("sphere3" → "Sphere",
+/// case-insensitively). The one rule the template merge, and everything
+/// that asks what a node's template says (the row menu's `Default:`),
+/// resolve by.
+pub fn template_for<'a>(node: &FsNode, templates: &'a [NodeTemplate]) -> Option<&'a FsNode> {
+ if node.node_type.eq_ignore_ascii_case("node") {
+ let base = node
+ .name
+ .trim_end_matches(|c: char| c.is_ascii_digit())
+ .trim_end_matches(|c: char| c == '_' || c.is_whitespace());
+ // Case-insensitively: the template is "Sphere", its instances
+ // are "sphere1".
+ templates.iter().map(|t| &t.node).find(|t| {
+ t.node_type.eq_ignore_ascii_case("node")
+ && (t.name.eq_ignore_ascii_case(&node.name)
+ || (!base.is_empty() && t.name.eq_ignore_ascii_case(base)))
+ })
+ } else {
+ templates.iter().map(|t| &t.node).find(|t| {
+ !t.node_type.eq_ignore_ascii_case("node")
+ && t.node_type.eq_ignore_ascii_case(&node.node_type)
+ })
+ }
+}
+
/// Strip the per-node `meta` (preferences) children from a loaded tree.
///
/// Every geometry node used to carry one, holding four display switches —
@@ -900,26 +926,6 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
}
nativize_kernel_subnets(root, templates);
- fn template_for<'a>(node: &FsNode, templates: &'a [NodeTemplate]) -> Option<&'a FsNode> {
- if node.node_type.eq_ignore_ascii_case("node") {
- let base = node
- .name
- .trim_end_matches(|c: char| c.is_ascii_digit())
- .trim_end_matches(|c: char| c == '_' || c.is_whitespace());
- // Case-insensitively: the template is "Sphere", its instances
- // are "sphere1".
- templates.iter().map(|t| &t.node).find(|t| {
- t.node_type.eq_ignore_ascii_case("node")
- && (t.name.eq_ignore_ascii_case(&node.name)
- || (!base.is_empty() && t.name.eq_ignore_ascii_case(base)))
- })
- } else {
- templates.iter().map(|t| &t.node).find(|t| {
- !t.node_type.eq_ignore_ascii_case("node")
- && t.node_type.eq_ignore_ascii_case(&node.node_type)
- })
- }
- }
fn merge_params(node: &mut FsNode, template: &FsNode) {
// A missing parameter goes where the TEMPLATE puts it — after the
// last template parameter the instance already has — not at the
@@ -4427,6 +4433,22 @@ impl State {
self.slots.param.as_any().downcast_ref::<ParametersBg>().map(|pb| pb.get_param_rects()).unwrap_or_default()
}
+ /// What the template says `pname` on `node` defaults to, where `dir` is
+ /// the level `node` sits in. A child of a subnet instance (the Embryo's
+ /// `sphere1`) takes the SUBNET template's word for it — its override
+ /// (`chf("../Radius")`) is the default that instance was built with,
+ /// and the merge refreshes the child from there — and any other node
+ /// its own template's. `None` for a parameter no template names, such
+ /// as one added over MCP.
+ pub fn template_default(&self, dir: &FsNode, node: &FsNode, pname: &str) -> Option<&ParamDef> {
+ let from_subnet = template_for(dir, &self.node_templates)
+ .and_then(|t| t.children.iter().find(|c| c.name.eq_ignore_ascii_case(&node.name)))
+ .and_then(|c| c.params.iter().find(|p| p.name == pname));
+ from_subnet.or_else(|| {
+ template_for(node, &self.node_templates).and_then(|t| t.params.iter().find(|p| p.name == pname))
+ })
+ }
+
/// The rows of a parameter's right-click menu: labels, the action each
/// runs, and how many leading rows are HEADERS. Split from the open so
/// a test reads them.
@@ -4439,17 +4461,24 @@ impl State {
/// the instance's, and they differ exactly when something is off — a
/// `Control: slider` over `Value: expression` is a row whose slider
/// cannot be shown, over `Value: invalid (…)` a load that kept a text
- /// the kind refuses. 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`.
+ /// the kind refuses. `Default:` is the template's value for the row
+ /// (`template_default`, as written there — an expression shows as the
+ /// 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`.
pub fn param_menu_rows(&self, slot: usize, pname: &str) -> (Vec<String>, Vec<ParamMenuAction>, usize) {
- let child = &self.param_editor_dir().children[slot];
+ let dir = self.param_editor_dir();
+ let child = &dir.children[slot];
let param = child.params.iter().find(|p| p.name == pname);
let is_expr = param.is_some_and(|p| p.is_expr());
let (control, value) = param
.map(|p| (p.kind().name().to_string(), p.value_type()))
.unwrap_or_else(|| ("?".to_string(), "?".to_string()));
let mut options = vec![format!("Control: {control}"), format!("Value: {value}")];
+ if let Some(d) = self.template_default(dir, child, pname) {
+ options.push(format!("Default: {}", d.text()));
+ }
if let Some((lo, hi, step)) = param.and_then(|p| p.range()) {
let fmt = |v: f32| crate::expr::fmt_num(v as f64);
let step = step.map(|s| format!(", step {}", fmt(s))).unwrap_or_default();
diff --git a/src/main.rs b/src/main.rs
index 917f140..d2e5ffa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7756,26 +7756,42 @@ mod tests {
assert!(!state.viewport_menu_open());
assert_eq!(
state.param_menu_actions,
- vec![ParamMenuAction::Info, ParamMenuAction::Info, ParamMenuAction::Info, ParamMenuAction::Separator, ParamMenuAction::CopyParameter, ParamMenuAction::Separator, ParamMenuAction::EditExpression],
+ vec![ParamMenuAction::Info, ParamMenuAction::Info, ParamMenuAction::Info, ParamMenuAction::Info, ParamMenuAction::Separator, ParamMenuAction::CopyParameter, ParamMenuAction::Separator, ParamMenuAction::EditExpression],
"nothing copied yet, and the row holds a value"
);
// The header rows read the parameter out: its control's kind, its
- // value's type in a programmer's terms, and the control's range.
- // Radius is a slider holding a float, clamped to the pane's default
- // 0..2 since the template declares no range.
+ // value's type in a programmer's terms, the template's default and
+ // the control's range. Radius is a slider holding a float, clamped
+ // to the pane's default 0..2 since the template declares no range.
let shown = cce_ui::widget::context_menu::options();
- assert_eq!(&shown[..4], &["Control: slider".to_string(), "Value: float".to_string(), "Range: 0..2".to_string(), "-".to_string()]);
+ assert_eq!(
+ &shown[..5],
+ &["Control: slider".to_string(), "Value: float".to_string(), "Default: 0.5".to_string(), "Range: 0..2".to_string(), "-".to_string()]
+ );
let (_, _, headers) = state.param_menu_rows(sphere, "Radius");
- assert_eq!(headers, 3);
+ assert_eq!(headers, 4);
// An inline range, a spinbox's range and step, a choice's options.
let (rows, _, h) = state.param_menu_rows(sphere, "Center X");
- assert_eq!((&rows[2], h), (&"Range: -2..2".to_string(), 3));
+ assert_eq!((&rows[3], h), (&"Range: -2..2".to_string(), 4));
let (rows, _, _) = state.param_menu_rows(sphere, "Rows");
- assert_eq!(rows[2], "Range: 2..128, step 1");
+ assert_eq!((&rows[2], &rows[3]), (&"Default: 16".to_string(), &"Range: 2..128, step 1".to_string()));
let (rows, _, h) = state.param_menu_rows(sphere, "Method");
- assert_eq!((&rows[2], h), (&"Options: UV, Icosphere, Cube".to_string(), 3));
+ assert_eq!((&rows[2], &rows[3], h), (&"Default: UV".to_string(), &"Options: UV, Icosphere, Cube".to_string(), 4));
let (rows, _, h) = state.param_menu_rows(sphere, "Color");
- assert_eq!((rows[2].as_str(), h), ("-", 2), "a toggle has neither a range nor options");
+ assert_eq!((rows[2].as_str(), rows[3].as_str(), h), ("Default: true", "-", 3), "a toggle has neither a range nor options");
+ // A parameter no template names has no default row.
+ state.apply_action(McpAction::AddParam { slot: sphere, name: "Extra".into(), param_type: "float".into(), default: "3".into() }, &mut redraw).unwrap();
+ let (rows, _, h) = state.param_menu_rows(sphere, "Extra");
+ assert_eq!((rows[2].as_str(), h), ("-", 2));
+ // Inside a subnet instance the SUBNET template's override is the
+ // default: the Embryo's sphere1 was built with an expression.
+ state.apply_action(McpAction::AddNode { template_name: "Embryo".into(), name: Some("embryo1".into()), x: 3.0, y: 8.0 }, &mut redraw).unwrap();
+ let embryo = slot_of(&state, "embryo1");
+ state.apply_action(McpAction::Enter { slot: embryo }, &mut redraw).unwrap();
+ let inner = slot_of(&state, "sphere1");
+ let (rows, _, _) = state.param_menu_rows(inner, "Radius");
+ assert_eq!(rows[2], "Default: chf(\"../Radius\")");
+ state.apply_action(McpAction::Up, &mut redraw).unwrap();
// A click on a header runs nothing.
state.run_param_action(&state.current_dir().children[sphere].id.clone(), "Radius", ParamMenuAction::Info);
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Right });